diff --git a/.csharpierignore b/.csharpierignore new file mode 100644 index 0000000..8da2f97 --- /dev/null +++ b/.csharpierignore @@ -0,0 +1,4 @@ +# CSharpier formats only C# sources; project/config XML keeps its layout. +**/*.csproj +**/*.config +**/*.xml diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..036dc8e --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# Commits whose changes git blame should skip (whitespace-only sweeps). +# Enable locally: git config blame.ignoreRevsFile .git-blame-ignore-revs +aec052306234e3c4313c0ee8905e2557d3c3671b diff --git a/OpenNest.Api/NestRequest.cs b/OpenNest.Api/NestRequest.cs index 3004218..118ee3e 100644 --- a/OpenNest.Api/NestRequest.cs +++ b/OpenNest.Api/NestRequest.cs @@ -6,17 +6,20 @@ namespace OpenNest.Api; public class NestRequest { public IReadOnlyList Parts { get; init; } = []; + /// /// Explicit available physical stock. Null keeps the legacy unlimited SheetSize fallback; /// an empty list deliberately means no stock is available. /// public IReadOnlyList Plates { get; init; } public Size SheetSize { get; init; } = new(60, 120); + /// Built-in whole-job placement strategy. Explicit values take precedence over legacy Strategy. public string PlacementStrategy { get; init; } = "Default"; public string Material { get; init; } = "Steel, A1011 HR"; public double Thickness { get; init; } = 0.06; public double Spacing { get; init; } = 0.1; + /// Legacy compatibility setting; Auto maps to the Default whole-job strategy. public NestStrategy Strategy { get; init; } = NestStrategy.Auto; public CutParameters Cutting { get; init; } = CutParameters.Default; diff --git a/OpenNest.Api/NestRequestPlate.cs b/OpenNest.Api/NestRequestPlate.cs index da24995..6999ad3 100644 --- a/OpenNest.Api/NestRequestPlate.cs +++ b/OpenNest.Api/NestRequestPlate.cs @@ -7,6 +7,7 @@ public class NestRequestPlate { public string Id { get; init; } public Size Size { get; init; } + /// Available physical sheets; null means unlimited. public int? Quantity { get; init; } public double PartSpacing { get; init; } diff --git a/OpenNest.Api/NestResponse.cs b/OpenNest.Api/NestResponse.cs index 2aa829c..7b6f47a 100644 --- a/OpenNest.Api/NestResponse.cs +++ b/OpenNest.Api/NestResponse.cs @@ -25,10 +25,12 @@ public class NestResponse /// Zero identifies an archive written before response metadata was versioned. public int SchemaVersion { get; init; } = CurrentSchemaVersion; public int SheetCount { get; init; } + /// Placed-part area divided by total materialized physical-sheet area, as a 0.0–1.0 ratio. public double Utilization { get; init; } public TimeSpan CutTime { get; init; } public TimeSpan Elapsed { get; init; } + /// Null means an older archive did not record whole-job fulfillment status. public NestJobStatus? Status { get; init; } public NestJobStopReason? StopReason { get; init; } @@ -43,7 +45,7 @@ public class NestResponse PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true, IncludeFields = true, // Required for OpenNest.Geometry.Size and Spacing public fields. - Converters = { new JsonStringEnumConverter() } + Converters = { new JsonStringEnumConverter() }, }; public async Task SaveAsync(string path) @@ -61,19 +63,27 @@ public class NestResponse var responseEntry = zip.CreateEntry("response.json"); await using (var stream = responseEntry.Open()) { - await JsonSerializer.SerializeAsync(stream, new NestResponseArchiveDto - { - SchemaVersion = CurrentSchemaVersion, - SheetCount = SheetCount, - Utilization = Utilization, - CutTimeTicks = CutTime.Ticks, - ElapsedTicks = Elapsed.Ticks, - Status = Status, - StopReason = StopReason, - Fulfillment = Fulfillment is null ? [] : new List(Fulfillment), - StockUsage = StockUsage is null ? [] : new List(StockUsage), - PlateStockMappings = PlateStockMappings is null ? [] : new List(PlateStockMappings) - }, JsonOptions); + await JsonSerializer.SerializeAsync( + stream, + new NestResponseArchiveDto + { + SchemaVersion = CurrentSchemaVersion, + SheetCount = SheetCount, + Utilization = Utilization, + CutTimeTicks = CutTime.Ticks, + ElapsedTicks = Elapsed.Ticks, + Status = Status, + StopReason = StopReason, + Fulfillment = Fulfillment is null + ? [] + : new List(Fulfillment), + StockUsage = StockUsage is null ? [] : new List(StockUsage), + PlateStockMappings = PlateStockMappings is null + ? [] + : new List(PlateStockMappings), + }, + JsonOptions + ); } var nestEntry = zip.CreateEntry("nest.nest"); @@ -91,16 +101,19 @@ public class NestResponse using var fs = new FileStream(path, FileMode.Open, FileAccess.Read); using var zip = new ZipArchive(fs, ZipArchiveMode.Read); - var requestEntry = zip.GetEntry("request.json") + var requestEntry = + zip.GetEntry("request.json") ?? throw new InvalidOperationException("Missing request.json in .nestquote file"); NestRequest request; await using (var stream = requestEntry.Open()) { - request = await JsonSerializer.DeserializeAsync(stream, JsonOptions) + request = + await JsonSerializer.DeserializeAsync(stream, JsonOptions) ?? throw new InvalidOperationException("Invalid request.json in .nestquote file"); } - var responseEntry = zip.GetEntry("response.json") + var responseEntry = + zip.GetEntry("response.json") ?? throw new InvalidOperationException("Missing response.json in .nestquote file"); NestResponseArchiveDto archive; var hasSchemaVersion = false; @@ -110,16 +123,19 @@ public class NestResponse { var root = document.RootElement; hasSchemaVersion = root.TryGetProperty("schemaVersion", out _); - hasStatusMetadata = root.TryGetProperty("status", out _) || - root.TryGetProperty("stopReason", out _) || - root.TryGetProperty("fulfillment", out _) || - root.TryGetProperty("stockUsage", out _) || - root.TryGetProperty("plateStockMappings", out _); - archive = root.Deserialize(JsonOptions) + hasStatusMetadata = + root.TryGetProperty("status", out _) + || root.TryGetProperty("stopReason", out _) + || root.TryGetProperty("fulfillment", out _) + || root.TryGetProperty("stockUsage", out _) + || root.TryGetProperty("plateStockMappings", out _); + archive = + root.Deserialize(JsonOptions) ?? throw new InvalidOperationException("Invalid response.json in .nestquote file"); } - var nestEntry = zip.GetEntry("nest.nest") + var nestEntry = + zip.GetEntry("nest.nest") ?? throw new InvalidOperationException("Missing nest.nest in .nestquote file"); Nest nest; using (var nestMs = new MemoryStream()) @@ -145,7 +161,7 @@ public class NestResponse StockUsage = hasStatusMetadata ? archive.StockUsage ?? [] : [], PlateStockMappings = hasStatusMetadata ? archive.PlateStockMappings ?? [] : [], Nest = nest, - Request = request + Request = request, }; } diff --git a/OpenNest.Api/NestRunner.cs b/OpenNest.Api/NestRunner.cs index 4ad7eb7..2166277 100644 --- a/OpenNest.Api/NestRunner.cs +++ b/OpenNest.Api/NestRunner.cs @@ -16,10 +16,13 @@ public static class NestRunner public static Task RunAsync( NestRequest request, IProgress progress = null, - CancellationToken token = default) + CancellationToken token = default + ) { ArgumentNullException.ThrowIfNull(request); - var requestParts = request.Parts ?? throw new ArgumentException("Request parts must not be null.", nameof(request)); + var requestParts = + request.Parts + ?? throw new ArgumentException("Request parts must not be null.", nameof(request)); if (requestParts.Count == 0) throw new ArgumentException("Request must contain at least one part.", nameof(request)); @@ -32,22 +35,32 @@ public static class NestRunner { token.ThrowIfCancellationRequested(); if (!File.Exists(part.Request.DxfPath)) - throw new FileNotFoundException($"DXF file not found: {part.Request.DxfPath}", part.Request.DxfPath); + throw new FileNotFoundException( + $"DXF file not found: {part.Request.DxfPath}", + part.Request.DxfPath + ); if (!importedByPath.TryGetValue(part.Request.DxfPath, out var drawing)) { try { - drawing = CadImporter.ImportDrawing(part.Request.DxfPath, - new CadImportOptions { Quantity = part.Request.Quantity }); + drawing = CadImporter.ImportDrawing( + part.Request.DxfPath, + new CadImportOptions { Quantity = part.Request.Quantity } + ); } catch (Exception exception) { - throw new InvalidOperationException($"Failed to import DXF: {part.Request.DxfPath}", exception); + throw new InvalidOperationException( + $"Failed to import DXF: {part.Request.DxfPath}", + exception + ); } if (drawing.Program == null || drawing.Program.Codes.Count == 0) - throw new InvalidOperationException($"Failed to import DXF: {part.Request.DxfPath}"); + throw new InvalidOperationException( + $"Failed to import DXF: {part.Request.DxfPath}" + ); importedByPath.Add(part.Request.DxfPath, drawing); } @@ -56,8 +69,11 @@ public static class NestRunner jobParts.Add(DrawingJobMapper.FromDrawing(part.Id, drawing, part.Request.Quantity)); } - var job = new NestJob(jobParts, CreateStock(request), - new NestJobOptions(ResolvePlacementStrategy(request))); + var job = new NestJob( + jobParts, + CreateStock(request), + new NestJobOptions(ResolvePlacementStrategy(request)) + ); var jobProgress = progress == null ? null : new JobProgressBridge(progress); var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job, jobProgress, token); @@ -71,35 +87,56 @@ public static class NestRunner var cutTime = Timing.CalculateTime(timingInfo, request.Cutting); sw.Stop(); - return Task.FromResult(new NestResponse - { - SheetCount = nest.Plates.Count, - Utilization = CalculateUtilization(nest), - CutTime = cutTime, - Elapsed = sw.Elapsed, - Status = result.Status, - StopReason = result.StopReason, - Fulfillment = result.Fulfillment - .Select(value => new NestPartFulfillment(value.PartId, value.Requested, value.Placed, value.Unplaced)) - .ToArray(), - StockUsage = result.StockUsage - .Select(value => new NestStockUsage(value.StockId, value.Used, value.Remaining)) - .ToArray(), - PlateStockMappings = result.Plates - .Select(value => new NestPlateStockMapping(value.PlateIndex, value.StockId)) - .ToArray(), - Nest = nest, - Request = request - }); + return Task.FromResult( + new NestResponse + { + SheetCount = nest.Plates.Count, + Utilization = CalculateUtilization(nest), + CutTime = cutTime, + Elapsed = sw.Elapsed, + Status = result.Status, + StopReason = result.StopReason, + Fulfillment = result + .Fulfillment.Select(value => new NestPartFulfillment( + value.PartId, + value.Requested, + value.Placed, + value.Unplaced + )) + .ToArray(), + StockUsage = result + .StockUsage.Select(value => new NestStockUsage( + value.StockId, + value.Used, + value.Remaining + )) + .ToArray(), + PlateStockMappings = result + .Plates.Select(value => new NestPlateStockMapping( + value.PlateIndex, + value.StockId + )) + .ToArray(), + Nest = nest, + Request = request, + } + ); } - private static IReadOnlyList IdentifyParts(IReadOnlyList requestParts) + private static IReadOnlyList IdentifyParts( + IReadOnlyList requestParts + ) { var identified = new List(requestParts.Count); var ids = new HashSet(StringComparer.Ordinal); for (var index = 0; index < requestParts.Count; index++) { - var part = requestParts[index] ?? throw new ArgumentException("Request parts must not contain null entries.", nameof(requestParts)); + var part = + requestParts[index] + ?? throw new ArgumentException( + "Request parts must not contain null entries.", + nameof(requestParts) + ); var id = part.Id ?? $"part-{index}"; if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Part IDs must not be blank.", nameof(requestParts)); @@ -117,8 +154,12 @@ public static class NestRunner { return [ - new NestPlateStock(LegacyStockId, request.SheetSize, quantity: null, - partSpacing: request.Spacing) + new NestPlateStock( + LegacyStockId, + request.SheetSize, + quantity: null, + partSpacing: request.Spacing + ), ]; } @@ -126,9 +167,20 @@ public static class NestRunner foreach (var plate in request.Plates) { if (plate is null) - throw new ArgumentException("Request plates must not contain null entries.", nameof(request)); - stock.Add(new NestPlateStock(plate.Id, plate.Size, plate.Quantity, plate.PartSpacing, - plate.EdgeSpacing, plate.Quadrant)); + throw new ArgumentException( + "Request plates must not contain null entries.", + nameof(request) + ); + stock.Add( + new NestPlateStock( + plate.Id, + plate.Size, + plate.Quantity, + plate.PartSpacing, + plate.EdgeSpacing, + plate.Quadrant + ) + ); } return stock; @@ -147,25 +199,31 @@ public static class NestRunner } } - private static string ResolvePlacementStrategy(NestRequest request) => request.PlacementStrategy ?? request.Strategy switch - { - NestStrategy.Auto => "Default", - _ => throw new NotSupportedException($"Unknown legacy nesting strategy: {request.Strategy}.") - }; + private static string ResolvePlacementStrategy(NestRequest request) => + request.PlacementStrategy + ?? request.Strategy switch + { + NestStrategy.Auto => "Default", + _ => throw new NotSupportedException( + $"Unknown legacy nesting strategy: {request.Strategy}." + ), + }; private static double CalculateUtilization(Nest nest) { var sheetArea = nest.Plates.Sum(plate => plate.Area()); - if (sheetArea == 0) return 0; - var placedArea = nest.Plates.Sum(plate => plate.Parts - .Where(part => !part.BaseDrawing.IsCutOff) - .Sum(part => part.BaseDrawing.Area)); + if (sheetArea == 0) + return 0; + var placedArea = nest.Plates.Sum(plate => + plate.Parts.Where(part => !part.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area) + ); return placedArea / sheetArea; } private sealed record IdentifiedRequestPart(string Id, NestRequestPart Request); - private sealed class JobProgressBridge(IProgress progress) : IProgress + private sealed class JobProgressBridge(IProgress progress) + : IProgress { public void Report(NestJobProgress value) { diff --git a/OpenNest.Api/NestStrategy.cs b/OpenNest.Api/NestStrategy.cs index a1a6814..a316ac5 100644 --- a/OpenNest.Api/NestStrategy.cs +++ b/OpenNest.Api/NestStrategy.cs @@ -1,3 +1,6 @@ namespace OpenNest.Api; -public enum NestStrategy { Auto } +public enum NestStrategy +{ + Auto, +} diff --git a/OpenNest.Benchmark/BenchmarkJob.cs b/OpenNest.Benchmark/BenchmarkJob.cs index 9704114..7d4db2a 100644 --- a/OpenNest.Benchmark/BenchmarkJob.cs +++ b/OpenNest.Benchmark/BenchmarkJob.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; using System.Collections.Generic; using System.IO; using System.Linq; +using OpenNest.Geometry; namespace OpenNest.Benchmark { @@ -48,13 +48,28 @@ namespace OpenNest.Benchmark /// engine owns its own multi-plate/size strategy; this harness no /// longer picks plate sizes on the engine's behalf. /// - public NestJob BuildNestJob(int maxPlates, double salvageRate = 0, double minimumSalvageDimension = 0) + public NestJob BuildNestJob( + int maxPlates, + double salvageRate = 0, + double minimumSalvageDimension = 0 + ) { var parts = Requests.Select(r => - DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity)); - var stock = CandidateSizes.Select(size => - new NestPlateStock(size.ToString(1), size, null, PartSpacing, EdgeSpacing, Quadrant)); - return new NestJob(parts, stock, new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension)); + DrawingJobMapper.FromDrawing(r.Drawing.Id.ToString(), r.Drawing, r.Quantity) + ); + var stock = CandidateSizes.Select(size => new NestPlateStock( + size.ToString(1), + size, + null, + PartSpacing, + EdgeSpacing, + Quadrant + )); + return new NestJob( + parts, + stock, + new NestJobOptions("Default", maxPlates, salvageRate, minimumSalvageDimension) + ); } } } diff --git a/OpenNest.Benchmark/BenchmarkRunner.cs b/OpenNest.Benchmark/BenchmarkRunner.cs index 133a6a9..9ffb741 100644 --- a/OpenNest.Benchmark/BenchmarkRunner.cs +++ b/OpenNest.Benchmark/BenchmarkRunner.cs @@ -23,8 +23,13 @@ namespace OpenNest.Benchmark /// Wall-clock budget for one engine solving one job. private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5); - public static List Run(List jobs, IReadOnlyList engines, - double salvageRate = 0, double minimumSalvageDimension = 0, string outputDirectory = null) + public static List Run( + List jobs, + IReadOnlyList engines, + double salvageRate = 0, + double minimumSalvageDimension = 0, + string outputDirectory = null + ) { var results = new List(jobs.Count * engines.Count); @@ -32,15 +37,28 @@ namespace OpenNest.Benchmark { foreach (var engineInfo in engines) { - results.Add(RunOne(job, engineInfo, salvageRate, minimumSalvageDimension, outputDirectory)); + results.Add( + RunOne( + job, + engineInfo, + salvageRate, + minimumSalvageDimension, + outputDirectory + ) + ); } } return results; } - private static JobResult RunOne(BenchmarkJob job, NestingEngineInfo engineInfo, - double salvageRate, double minimumSalvageDimension, string outputDirectory) + private static JobResult RunOne( + BenchmarkJob job, + NestingEngineInfo engineInfo, + double salvageRate, + double minimumSalvageDimension, + string outputDirectory + ) { var requested = job.TotalRequestedQuantity; var sw = Stopwatch.StartNew(); @@ -53,18 +71,25 @@ namespace OpenNest.Benchmark var jobResult = engine.Solve(nestJob, null, cts.Token); var materialized = NestResultMaterializer.Materialize(nestJob, jobResult); - var plateRuns = materialized.Nest.Plates - .Select(plate => (Plate: plate, Parts: plate.Parts.ToList())) + var plateRuns = materialized + .Nest.Plates.Select(plate => (Plate: plate, Parts: plate.Parts.ToList())) .ToList(); - var requirements = job.Requests.ToDictionary( + var requirements = job.Requests.ToDictionary< + DrawingRequest, + Drawing, + (string Name, int Quantity) + >( r => materialized.DrawingsByPartId[r.Drawing.Id.ToString()], r => (r.Drawing.Name, r.Quantity), - ReferenceEqualityComparer.Instance); + ReferenceEqualityComparer.Instance + ); var validation = NestValidator.Validate(plateRuns, requirements); var totalPlaced = plateRuns.Sum(pr => pr.Parts.Count); - var placedArea = validation.Valid ? plateRuns.Sum(pr => pr.Parts.Sum(p => p.BaseDrawing.Area)) : 0; + var placedArea = validation.Valid + ? plateRuns.Sum(pr => pr.Parts.Sum(p => p.BaseDrawing.Area)) + : 0; var plateArea = plateRuns.Sum(pr => pr.Plate.Area()); var sizeBreakdown = plateRuns @@ -83,23 +108,48 @@ namespace OpenNest.Benchmark materialized.Nest.Thickness = source.Thickness; materialized.Nest.SalvageRate = salvageRate; foreach (var request in job.Requests) - materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request.Drawing.Name; - var path = System.IO.Path.Combine(outputDirectory, $"{job.Name}-{engineInfo.Name}.nest"); - if (System.IO.Path.GetFullPath(path) == System.IO.Path.GetFullPath(job.SourceFile)) - throw new InvalidOperationException("Output must not overwrite the source nest."); + materialized.DrawingsByPartId[request.Drawing.Id.ToString()].Name = request + .Drawing + .Name; + var path = System.IO.Path.Combine( + outputDirectory, + $"{job.Name}-{engineInfo.Name}.nest" + ); + if ( + System.IO.Path.GetFullPath(path) + == System.IO.Path.GetFullPath(job.SourceFile) + ) + throw new InvalidOperationException( + "Output must not overwrite the source nest." + ); new OpenNest.IO.NestWriter(materialized.Nest).Write(path); var report = new { - Source = job.SourceFile, Engine = engineInfo.Name, jobResult.Status, jobResult.StopReason, - Requested = requested, Placed = totalPlaced, SheetArea = plateArea, PlacedArea = placedArea, - SalvageRate = salvageRate, MinimumSalvageDimension = minimumSalvageDimension, - EstimatedNetArea = jobResult.Plates.Sum(p => StockLadderNestingEngine.EstimateNetArea(nestJob, p)), - Fulfillment = jobResult.Fulfillment, StockUsage = jobResult.StockUsage, - Plates = jobResult.Plates, validation.Violations + Source = job.SourceFile, + Engine = engineInfo.Name, + jobResult.Status, + jobResult.StopReason, + Requested = requested, + Placed = totalPlaced, + SheetArea = plateArea, + PlacedArea = placedArea, + SalvageRate = salvageRate, + MinimumSalvageDimension = minimumSalvageDimension, + EstimatedNetArea = jobResult.Plates.Sum(p => + StockLadderNestingEngine.EstimateNetArea(nestJob, p) + ), + Fulfillment = jobResult.Fulfillment, + StockUsage = jobResult.StockUsage, + Plates = jobResult.Plates, + validation.Violations, }; - System.IO.File.WriteAllText(System.IO.Path.ChangeExtension(path, ".json"), - System.Text.Json.JsonSerializer.Serialize(report, - new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); + System.IO.File.WriteAllText( + System.IO.Path.ChangeExtension(path, ".json"), + System.Text.Json.JsonSerializer.Serialize( + report, + new System.Text.Json.JsonSerializerOptions { WriteIndented = true } + ) + ); } sw.Stop(); diff --git a/OpenNest.Benchmark/JobLoader.cs b/OpenNest.Benchmark/JobLoader.cs index 7911c77..2346659 100644 --- a/OpenNest.Benchmark/JobLoader.cs +++ b/OpenNest.Benchmark/JobLoader.cs @@ -1,9 +1,9 @@ -using OpenNest.Geometry; -using OpenNest.IO; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using OpenNest.Geometry; +using OpenNest.IO; namespace OpenNest.Benchmark { @@ -17,8 +17,11 @@ namespace OpenNest.Benchmark /// public static class JobLoader { - public static List Load(string inputPath, IReadOnlyList sheetSizeOverrides = null, - double? partSpacingOverride = null) + public static List Load( + string inputPath, + IReadOnlyList sheetSizeOverrides = null, + double? partSpacingOverride = null + ) { var files = ResolveFiles(inputPath); var jobs = new List(); @@ -33,7 +36,9 @@ namespace OpenNest.Benchmark } catch (Exception ex) { - Console.Error.WriteLine($"[JobLoader] Skipping '{file}': failed to read ({ex.Message})"); + Console.Error.WriteLine( + $"[JobLoader] Skipping '{file}': failed to read ({ex.Message})" + ); continue; } @@ -41,24 +46,29 @@ namespace OpenNest.Benchmark if (requests.Count == 0) { - Console.Error.WriteLine($"[JobLoader] Skipping '{file}': no drawings with quantity > 0"); + Console.Error.WriteLine( + $"[JobLoader] Skipping '{file}': no drawings with quantity > 0" + ); continue; } var template = ResolvePlateTemplate(nest); - var sizes = sheetSizeOverrides != null && sheetSizeOverrides.Count > 0 - ? sheetSizeOverrides.ToList() - : ResolveSheetSizes(nest); + var sizes = + sheetSizeOverrides != null && sheetSizeOverrides.Count > 0 + ? sheetSizeOverrides.ToList() + : ResolveSheetSizes(nest); - jobs.Add(new BenchmarkJob - { - SourceFile = file, - CandidateSizes = sizes, - EdgeSpacing = template.EdgeSpacing, - PartSpacing = partSpacingOverride ?? template.PartSpacing, - Quadrant = template.Quadrant, - Requests = requests, - }); + jobs.Add( + new BenchmarkJob + { + SourceFile = file, + CandidateSizes = sizes, + EdgeSpacing = template.EdgeSpacing, + PartSpacing = partSpacingOverride ?? template.PartSpacing, + Quadrant = template.Quadrant, + Requests = requests, + } + ); } return jobs; @@ -68,7 +78,8 @@ namespace OpenNest.Benchmark { if (Directory.Exists(inputPath)) { - return Directory.GetFiles(inputPath, "*.nest", SearchOption.AllDirectories) + return Directory + .GetFiles(inputPath, "*.nest", SearchOption.AllDirectories) .OrderBy(f => f, StringComparer.OrdinalIgnoreCase) .ToList(); } @@ -92,21 +103,25 @@ namespace OpenNest.Benchmark var constraints = drawing.Constraints; - requests.Add(new DrawingRequest - { - Drawing = drawing, - Quantity = qty, - Priority = drawing.Priority, - StepAngle = constraints?.StepAngle ?? 0, - RotationStart = constraints?.StartAngle ?? 0, - RotationEnd = constraints?.EndAngle ?? 0, - }); + requests.Add( + new DrawingRequest + { + Drawing = drawing, + Quantity = qty, + Priority = drawing.Priority, + StepAngle = constraints?.StepAngle ?? 0, + RotationStart = constraints?.StartAngle ?? 0, + RotationEnd = constraints?.EndAngle ?? 0, + } + ); } return requests; } - private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate(Nest nest) + private static (Spacing EdgeSpacing, double PartSpacing, int Quadrant) ResolvePlateTemplate( + Nest nest + ) { var source = nest.Plates?.FirstOrDefault(); diff --git a/OpenNest.Benchmark/NestValidator.cs b/OpenNest.Benchmark/NestValidator.cs index add185f..44d49bd 100644 --- a/OpenNest.Benchmark/NestValidator.cs +++ b/OpenNest.Benchmark/NestValidator.cs @@ -1,8 +1,8 @@ +using System.Collections.Generic; +using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; -using System.Linq; namespace OpenNest.Benchmark { @@ -31,8 +31,10 @@ namespace OpenNest.Benchmark /// identity must never be inferred from Name, which is only incidentally seeded from the /// originating NestJobPart id) to its original quantity limit and display name. /// - public static ValidationResult Validate(List<(Plate Plate, List Parts)> plateRuns, - IReadOnlyDictionary requirements) + public static ValidationResult Validate( + List<(Plate Plate, List Parts)> plateRuns, + IReadOnlyDictionary requirements + ) { var result = new ValidationResult(); var allParts = plateRuns.SelectMany(pr => pr.Parts).ToList(); @@ -55,8 +57,11 @@ namespace OpenNest.Benchmark return result; } - private static void ValidateQuantities(List parts, - IReadOnlyDictionary requirements, ValidationResult result) + private static void ValidateQuantities( + List parts, + IReadOnlyDictionary requirements, + ValidationResult result + ) { var placedCounts = parts .GroupBy(p => p.BaseDrawing, ReferenceEqualityComparer.Instance) @@ -66,20 +71,27 @@ namespace OpenNest.Benchmark { if (!requirements.TryGetValue(drawing, out var requirement)) { - result.Violations.Add($"Placed drawing '{drawing.Name}' which was not requested for this job"); + result.Violations.Add( + $"Placed drawing '{drawing.Name}' which was not requested for this job" + ); continue; } if (placed > requirement.Quantity) { result.Violations.Add( - $"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested"); + $"'{requirement.Name}': placed {placed} across all plates but only {requirement.Quantity} were requested" + ); } } } - private static void ValidateBounds(List parts, Plate plate, - IReadOnlyDictionary requirements, ValidationResult result) + private static void ValidateBounds( + List parts, + Plate plate, + IReadOnlyDictionary requirements, + ValidationResult result + ) { var workArea = plate.WorkArea(); @@ -95,8 +107,9 @@ namespace OpenNest.Benchmark if (outLeft || outBottom || outRight || outTop) { result.Violations.Add( - $"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " + - $"of a {plate.Size} plate"); + $"'{DisplayName(part, requirements)}' at ({part.Location.X:F2},{part.Location.Y:F2}) falls outside the work area " + + $"of a {plate.Size} plate" + ); } } } @@ -110,7 +123,11 @@ namespace OpenNest.Benchmark /// to return false negatives on real, complex production geometry, so /// this check does not depend on it. /// - private static void ValidateAreaBudget(List parts, Plate plate, ValidationResult result) + private static void ValidateAreaBudget( + List parts, + Plate plate, + ValidationResult result + ) { var workArea = plate.WorkArea(); var budget = workArea.Width * workArea.Length; @@ -119,13 +136,18 @@ namespace OpenNest.Benchmark if (placedArea > budget + Tolerance.Epsilon) { result.Violations.Add( - $"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - " + - "parts must overlap even though the polygon overlap check did not flag a pair"); + $"Combined placed area ({placedArea:F2}) on a {plate.Size} plate exceeds its work area ({budget:F2}) - " + + "parts must overlap even though the polygon overlap check did not flag a pair" + ); } } - private static void ValidateSpacing(List parts, double spacing, - IReadOnlyDictionary requirements, ValidationResult result) + private static void ValidateSpacing( + List parts, + double spacing, + IReadOnlyDictionary requirements, + ValidationResult result + ) { var worldPolygons = new Polygon[parts.Count]; var inflatedPolygons = new Polygon[parts.Count]; @@ -133,7 +155,10 @@ namespace OpenNest.Benchmark for (var i = 0; i < parts.Count; i++) { worldPolygons[i] = WorldPolygon(parts[i], 0); - inflatedPolygons[i] = spacing > Tolerance.Epsilon ? WorldPolygon(parts[i], spacing) : worldPolygons[i]; + inflatedPolygons[i] = + spacing > Tolerance.Epsilon + ? WorldPolygon(parts[i], spacing) + : worldPolygons[i]; } for (var i = 0; i < parts.Count; i++) @@ -149,7 +174,8 @@ namespace OpenNest.Benchmark if (Collision.HasOverlap(inflatedPolygons[i], worldPolygons[j])) { result.Violations.Add( - $"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})"); + $"'{DisplayName(parts[i], requirements)}' and '{DisplayName(parts[j], requirements)}' are closer than the required spacing ({spacing:F3})" + ); } } } @@ -158,8 +184,13 @@ namespace OpenNest.Benchmark /// Friendly name for a violation message, falling back to the materialized /// Drawing's own Name (the raw partId string) if this part wasn't in requirements at all - /// that mismatch is already reported by ValidateQuantities, so this is display-only. - private static string DisplayName(Part part, IReadOnlyDictionary requirements) => - requirements.TryGetValue(part.BaseDrawing, out var requirement) ? requirement.Name : part.BaseDrawing.Name; + private static string DisplayName( + Part part, + IReadOnlyDictionary requirements + ) => + requirements.TryGetValue(part.BaseDrawing, out var requirement) + ? requirement.Name + : part.BaseDrawing.Name; /// /// Extracts a part's perimeter as a world-space polygon, optionally inflated @@ -168,7 +199,8 @@ namespace OpenNest.Benchmark /// private static Polygon WorldPolygon(Part part, double inflateBy) { - var entities = ConvertProgram.ToGeometry(part.Program) + var entities = ConvertProgram + .ToGeometry(part.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); diff --git a/OpenNest.Benchmark/Program.cs b/OpenNest.Benchmark/Program.cs index 0b1ded8..5bf5e02 100644 --- a/OpenNest.Benchmark/Program.cs +++ b/OpenNest.Benchmark/Program.cs @@ -1,10 +1,10 @@ -using OpenNest; -using OpenNest.Benchmark; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using OpenNest; +using OpenNest.Benchmark; +using OpenNest.Geometry; return BenchmarkConsole.Run(args); @@ -37,7 +37,9 @@ static class BenchmarkConsole if (jobs.Count == 0) { - Console.Error.WriteLine("No benchmark jobs found (no .nest files with any drawing quantity > 0)."); + Console.Error.WriteLine( + "No benchmark jobs found (no .nest files with any drawing quantity > 0)." + ); return 1; } @@ -49,13 +51,22 @@ static class BenchmarkConsole if (options.EngineNames.Count > 0) { engines = engines - .Where(e => options.EngineNames.Any(n => n.Equals(e.Name, StringComparison.OrdinalIgnoreCase))) + .Where(e => + options.EngineNames.Any(n => + n.Equals(e.Name, StringComparison.OrdinalIgnoreCase) + ) + ) .ToList(); if (engines.Count == 0) { - Console.Error.WriteLine("None of the requested engines are registered. Available: " + - string.Join(", ", NestingEngineRegistry.AvailableEngines.Select(e => e.Name))); + Console.Error.WriteLine( + "None of the requested engines are registered. Available: " + + string.Join( + ", ", + NestingEngineRegistry.AvailableEngines.Select(e => e.Name) + ) + ); return 1; } } @@ -65,12 +76,20 @@ static class BenchmarkConsole foreach (var job in jobs) { var sizes = string.Join(", ", job.CandidateSizes.Select(s => s.ToString(1))); - Console.WriteLine($" {job.Name}: {job.Requests.Count} drawing(s), {job.TotalRequestedQuantity} part(s) requested, candidate sizes: {sizes}"); + Console.WriteLine( + $" {job.Name}: {job.Requests.Count} drawing(s), {job.TotalRequestedQuantity} part(s) requested, candidate sizes: {sizes}" + ); } Console.WriteLine($"Engines: {string.Join(", ", engines.Select(e => e.Name))}"); - var results = BenchmarkRunner.Run(jobs, engines, options.SalvageRate, options.MinimumSalvageDimension, options.OutputDirectory); + var results = BenchmarkRunner.Run( + jobs, + engines, + options.SalvageRate, + options.MinimumSalvageDimension, + options.OutputDirectory + ); Report.PrintDetailed(results); Report.PrintSummary(results); @@ -103,7 +122,10 @@ static class BenchmarkConsole case "--engines" when i + 1 < args.Length: o.EngineNames = args[++i] - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ) .ToList(); break; @@ -112,10 +134,16 @@ static class BenchmarkConsole break; case "--salvage-rate" when i + 1 < args.Length: - o.SalvageRate = double.Parse(args[++i], System.Globalization.CultureInfo.InvariantCulture); + o.SalvageRate = double.Parse( + args[++i], + System.Globalization.CultureInfo.InvariantCulture + ); break; case "--min-salvage-dimension" when i + 1 < args.Length: - o.MinimumSalvageDimension = double.Parse(args[++i], System.Globalization.CultureInfo.InvariantCulture); + o.MinimumSalvageDimension = double.Parse( + args[++i], + System.Globalization.CultureInfo.InvariantCulture + ); break; case "--output" when i + 1 < args.Length: o.OutputDirectory = args[++i]; @@ -139,7 +167,12 @@ static class BenchmarkConsole { var sizes = new List(); - foreach (var token in arg.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + foreach ( + var token in arg.Split( + ',', + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries + ) + ) { if (Size.TryParse(token, out var size)) sizes.Add(size); @@ -152,29 +185,63 @@ static class BenchmarkConsole private static void PrintUsage() { - Console.Error.WriteLine("OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files"); + Console.Error.WriteLine( + "OpenNest.Benchmark - compare registered whole-job nesting engines on a set of .nest files" + ); Console.Error.WriteLine(); - Console.Error.WriteLine("For each .nest file, every drawing with quantity > 0 is nested (mixed together),"); - Console.Error.WriteLine("once per registered INestingEngine. Each engine is handed the full job - every"); - Console.Error.WriteLine("requested part and the whole pool of candidate sheet sizes - and owns its own"); - Console.Error.WriteLine("multi-plate/size strategy: how many plates it uses, of which sizes, and how"); - Console.Error.WriteLine("demand splits across them. Scoring: aggregate material utilization across every"); - Console.Error.WriteLine("plate used, then (if everything requested was placed) fewer plates as the"); - Console.Error.WriteLine("tie-break. An invalid layout (out of bounds, overlapping, or over-quantity), a"); - Console.Error.WriteLine("thrown exception, or a run exceeding its time budget all score zero."); + Console.Error.WriteLine( + "For each .nest file, every drawing with quantity > 0 is nested (mixed together)," + ); + Console.Error.WriteLine( + "once per registered INestingEngine. Each engine is handed the full job - every" + ); + Console.Error.WriteLine( + "requested part and the whole pool of candidate sheet sizes - and owns its own" + ); + Console.Error.WriteLine( + "multi-plate/size strategy: how many plates it uses, of which sizes, and how" + ); + Console.Error.WriteLine( + "demand splits across them. Scoring: aggregate material utilization across every" + ); + Console.Error.WriteLine( + "plate used, then (if everything requested was placed) fewer plates as the" + ); + Console.Error.WriteLine( + "tie-break. An invalid layout (out of bounds, overlapping, or over-quantity), a" + ); + Console.Error.WriteLine( + "thrown exception, or a run exceeding its time budget all score zero." + ); Console.Error.WriteLine(); Console.Error.WriteLine("Usage:"); Console.Error.WriteLine(" OpenNest.Benchmark [options]"); Console.Error.WriteLine(); Console.Error.WriteLine("Options:"); - Console.Error.WriteLine(" --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest"); - Console.Error.WriteLine(" (default: the distinct sizes already in each file)"); - Console.Error.WriteLine(" --spacing Override part spacing for every job"); - Console.Error.WriteLine(" --engines Name1,Name2,... Only benchmark these registered engines (default: all)"); - Console.Error.WriteLine(" --csv Write a flat CSV of all results"); - Console.Error.WriteLine(" --salvage-rate <0..1> Fraction of eligible offcut area credited (default 0)"); - Console.Error.WriteLine(" --min-salvage-dimension Both offcut dimensions must qualify; 0 disables credit"); - Console.Error.WriteLine(" --output Save valid layouts as .nest plus detailed JSON reports"); + Console.Error.WriteLine( + " --sheet-sizes W1xL1,W2xL2,... Candidate sheet-size pool for the whole nest" + ); + Console.Error.WriteLine( + " (default: the distinct sizes already in each file)" + ); + Console.Error.WriteLine( + " --spacing Override part spacing for every job" + ); + Console.Error.WriteLine( + " --engines Name1,Name2,... Only benchmark these registered engines (default: all)" + ); + Console.Error.WriteLine( + " --csv Write a flat CSV of all results" + ); + Console.Error.WriteLine( + " --salvage-rate <0..1> Fraction of eligible offcut area credited (default 0)" + ); + Console.Error.WriteLine( + " --min-salvage-dimension Both offcut dimensions must qualify; 0 disables credit" + ); + Console.Error.WriteLine( + " --output Save valid layouts as .nest plus detailed JSON reports" + ); Console.Error.WriteLine(" --help Show this message"); } diff --git a/OpenNest.Benchmark/Report.cs b/OpenNest.Benchmark/Report.cs index 12fcc21..722c099 100644 --- a/OpenNest.Benchmark/Report.cs +++ b/OpenNest.Benchmark/Report.cs @@ -28,19 +28,27 @@ namespace OpenNest.Benchmark var ranked = jobGroup.OrderBy(r => r, Comparer.Create(Compare)).ToList(); var best = ranked.Count > 0 ? ranked[0] : null; - Console.WriteLine($"{"Engine",-16} {"Result",-9} {"Parts",-10} {"Util%",-8} {"Plates",-18} {"Time(ms)",-9} Notes"); + Console.WriteLine( + $"{"Engine", -16} {"Result", -9} {"Parts", -10} {"Util%", -8} {"Plates", -18} {"Time(ms)", -9} Notes" + ); foreach (var r in ranked) { var isWinner = best != null && Compare(r, best) == 0 && r.Valid; var marker = isWinner ? "*" : " "; - var status = r.Crashed ? "CRASH" : r.Valid ? "ok" : "INVALID"; + var status = + r.Crashed ? "CRASH" + : r.Valid ? "ok" + : "INVALID"; var partsCol = $"{r.PartsPlaced}/{r.PartsRequested}"; var utilCol = r.Valid ? $"{r.Utilization * 100:F1}" : "-"; - var platesCol = r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-"; + var platesCol = + r.PlatesUsed > 0 ? $"{r.PlatesUsed} ({SizeSummary(r.SizeBreakdown)})" : "-"; var notes = r.Crashed ? r.Error : string.Join("; ", r.Violations.Take(2)); - Console.WriteLine($"{marker}{r.EngineName,-15} {status,-9} {partsCol,-10} {utilCol,-8} {platesCol,-18} {r.ElapsedMs,-9} {notes}"); + Console.WriteLine( + $"{marker}{r.EngineName, -15} {status, -9} {partsCol, -10} {utilCol, -8} {platesCol, -18} {r.ElapsedMs, -9} {notes}" + ); } } } @@ -68,30 +76,47 @@ namespace OpenNest.Benchmark var wins = CountWins(results); - Console.WriteLine($"{"Engine",-16} {"Jobs",-6} {"Valid",-7} {"Complete",-9} {"Wins",-6} {"AvgUtil%",-10} {"Plates",-8} {"TotalTime(ms)",-14}"); + Console.WriteLine( + $"{"Engine", -16} {"Jobs", -6} {"Valid", -7} {"Complete", -9} {"Wins", -6} {"AvgUtil%", -10} {"Plates", -8} {"TotalTime(ms)", -14}" + ); foreach (var e in byEngine) { var avgUtil = e.Jobs > 0 ? e.TotalUtilization / e.Jobs * 100 : 0; var winCount = wins.TryGetValue(e.Engine, out var w) ? w : 0; - Console.WriteLine($"{e.Engine,-16} {e.Jobs,-6} {e.Valid,-7} {e.FullyPlaced,-9} {winCount,-6} {avgUtil,-10:F1} {e.TotalPlates,-8} {e.TotalTimeMs,-14}"); + Console.WriteLine( + $"{e.Engine, -16} {e.Jobs, -6} {e.Valid, -7} {e.FullyPlaced, -9} {winCount, -6} {avgUtil, -10:F1} {e.TotalPlates, -8} {e.TotalTimeMs, -14}" + ); } } public static void WriteCsv(string path, List results) { var sb = new StringBuilder(); - sb.AppendLine("Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes"); + sb.AppendLine( + "Job,Engine,Valid,Crashed,FullyPlaced,PartsPlaced,PartsRequested,Utilization,PlatesUsed,SizeBreakdown,ElapsedMs,Notes" + ); foreach (var r in results) { var notes = r.Crashed ? r.Error : string.Join(" | ", r.Violations); - sb.AppendLine(string.Join(",", - Csv(r.JobName), Csv(r.EngineName), r.Valid, r.Crashed, r.FullyPlaced, - r.PartsPlaced, r.PartsRequested, - r.Utilization.ToString("F4", CultureInfo.InvariantCulture), - r.PlatesUsed, Csv(SizeSummary(r.SizeBreakdown)), - r.ElapsedMs, Csv(notes))); + sb.AppendLine( + string.Join( + ",", + Csv(r.JobName), + Csv(r.EngineName), + r.Valid, + r.Crashed, + r.FullyPlaced, + r.PartsPlaced, + r.PartsRequested, + r.Utilization.ToString("F4", CultureInfo.InvariantCulture), + r.PlatesUsed, + Csv(SizeSummary(r.SizeBreakdown)), + r.ElapsedMs, + Csv(notes) + ) + ); } File.WriteAllText(path, sb.ToString()); diff --git a/OpenNest.Console/Program.cs b/OpenNest.Console/Program.cs index 33917c3..7adfe0a 100644 --- a/OpenNest.Console/Program.cs +++ b/OpenNest.Console/Program.cs @@ -1,15 +1,15 @@ -using OpenNest; -using OpenNest.Geometry; -using OpenNest.IO; -using OpenNest.IO.Bending; -using System.Globalization; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Threading; +using OpenNest; +using OpenNest.Geometry; +using OpenNest.IO; +using OpenNest.IO.Bending; return NestConsole.Run(args); @@ -22,11 +22,19 @@ static class NestConsole if (options == null) return 0; // --help was requested - if (options.RepairBendsMillimeters.HasValue && - (options.CadUnits == BendRepairUnits.Unspecified || !double.IsFinite(options.RepairBendsMillimeters.Value) - || options.RepairBendsMillimeters <= 0.001 || options.RepairBendsMillimeters > 3.175)) + if ( + options.RepairBendsMillimeters.HasValue + && ( + options.CadUnits == BendRepairUnits.Unspecified + || !double.IsFinite(options.RepairBendsMillimeters.Value) + || options.RepairBendsMillimeters <= 0.001 + || options.RepairBendsMillimeters > 3.175 + ) + ) { - Console.Error.WriteLine("Error: --repair-bends-mm requires a limit > 0.001 and <= 3.175 mm and --cad-units inches|mm."); + Console.Error.WriteLine( + "Error: --repair-bends-mm requires a limit > 0.001 and <= 3.175 mm and --cad-units inches|mm." + ); return 1; } @@ -93,10 +101,24 @@ static class NestConsole switch (args[i]) { case "--repair-bends-mm": - o.RepairBendsMillimeters = i + 1 < args.Length && double.TryParse(args[++i], NumberStyles.Float, CultureInfo.InvariantCulture, out var limit) ? limit : double.NaN; + o.RepairBendsMillimeters = + i + 1 < args.Length + && double.TryParse( + args[++i], + NumberStyles.Float, + CultureInfo.InvariantCulture, + out var limit + ) + ? limit + : double.NaN; break; case "--cad-units" when i + 1 < args.Length: - o.CadUnits = args[++i] switch { "inches" => BendRepairUnits.Inches, "mm" => BendRepairUnits.Millimeters, _ => BendRepairUnits.Unspecified }; + o.CadUnits = args[++i] switch + { + "inches" => BendRepairUnits.Inches, + "mm" => BendRepairUnits.Millimeters, + _ => BendRepairUnits.Unspecified, + }; break; case "--drawing" when i + 1 < args.Length: o.DrawingName = args[++i]; @@ -165,10 +187,14 @@ static class NestConsole { var nestFile = options.InputFiles.FirstOrDefault(f => f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase) - || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)); - var dxfFiles = options.InputFiles.Where(f => - f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) || - f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)).ToList(); + || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) + ); + var dxfFiles = options + .InputFiles.Where(f => + f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) + ) + .ToList(); // If we have a nest file, load it and optionally add DXFs. if (nestFile != null) @@ -183,7 +209,9 @@ static class NestConsole if (options.PlateIndex >= nest.Plates.Count) { - Console.Error.WriteLine($"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})"); + Console.Error.WriteLine( + $"Error: plate index {options.PlateIndex} out of range (0-{nest.Plates.Count - 1})" + ); return null; } @@ -210,7 +238,9 @@ static class NestConsole if (!options.PlateSize.HasValue) { - Console.Error.WriteLine("Error: --size WxL is required when importing DXF files without a nest"); + Console.Error.WriteLine( + "Error: --size WxL is required when importing DXF files without a nest" + ); return null; } @@ -236,16 +266,23 @@ static class NestConsole { try { - var result = CadImporter.Import(path, new CadImportOptions - { - BendRepair = options.RepairBendsMillimeters.HasValue ? new BendRepairOptions + var result = CadImporter.Import( + path, + new CadImportOptions { - DrawingUnits = options.CadUnits, - MaxEndpointMovementMillimeters = options.RepairBendsMillimeters.Value - } : null - }); + BendRepair = options.RepairBendsMillimeters.HasValue + ? new BendRepairOptions + { + DrawingUnits = options.CadUnits, + MaxEndpointMovementMillimeters = options.RepairBendsMillimeters.Value, + } + : null, + } + ); foreach (var report in result.BendRepairReports) - Console.WriteLine($"Bend repair {Path.GetFileName(path)} #{report.BendIndex + 1}: {report.Status}: {report.Reason} ({report.OriginalStart} -> {report.Start}; {report.OriginalEnd} -> {report.End})"); + Console.WriteLine( + $"Bend repair {Path.GetFileName(path)} #{report.BendIndex + 1}: {report.Status}: {report.Reason} ({report.OriginalStart} -> {report.Start}; {report.OriginalEnd} -> {report.End})" + ); return CadImporter.BuildDrawing(result, result.Entities, result.Bends, 1, null, null); } catch (System.Exception ex) @@ -282,7 +319,8 @@ static class NestConsole // Only apply size override when it wasn't already used to create the plate. var hasDxfOnly = !options.InputFiles.Any(f => f.EndsWith(NestFormat.FileExtension, StringComparison.OrdinalIgnoreCase) - || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)); + || f.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) + ); if (options.PlateSize.HasValue && !hasDxfOnly) plate.Size = options.PlateSize.Value; @@ -290,33 +328,51 @@ static class NestConsole static Drawing ResolveDrawing(Nest nest, Options options) { - var drawing = options.DrawingName != null - ? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName) - : nest.Drawings.FirstOrDefault(); + var drawing = + options.DrawingName != null + ? nest.Drawings.FirstOrDefault(d => d.Name == options.DrawingName) + : nest.Drawings.FirstOrDefault(); if (drawing != null) return drawing; - Console.Error.WriteLine(options.DrawingName != null - ? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}" - : "Error: nest file contains no drawings"); + Console.Error.WriteLine( + options.DrawingName != null + ? $"Error: drawing '{options.DrawingName}' not found. Available: {string.Join(", ", nest.Drawings.Select(d => d.Name))}" + : "Error: nest file contains no drawings" + ); return null; } - static void PrintHeader(Nest nest, Plate plate, Drawing drawing, int existingCount, Options options) + static void PrintHeader( + Nest nest, + Plate plate, + Drawing drawing, + int existingCount, + Options options + ) { Console.WriteLine($"Nest: {nest.Name}"); var wa = plate.WorkArea(); - Console.WriteLine($"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}"); + Console.WriteLine( + $"Plate: {options.PlateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}, edge=({plate.EdgeSpacing.Left},{plate.EdgeSpacing.Bottom},{plate.EdgeSpacing.Right},{plate.EdgeSpacing.Top}), workArea={wa.Width:F1}x{wa.Length:F1}" + ); Console.WriteLine($"Drawing: {drawing.Name}"); - Console.WriteLine(options.KeepParts - ? $"Keeping {existingCount} existing parts" - : $"Cleared {existingCount} existing parts"); + Console.WriteLine( + options.KeepParts + ? $"Keeping {existingCount} existing parts" + : $"Cleared {existingCount} existing parts" + ); Console.WriteLine("---"); } - static (bool success, long elapsedMs) Fill(Nest nest, Plate plate, Drawing drawing, Options options) + static (bool success, long elapsedMs) Fill( + Nest nest, + Plate plate, + Drawing drawing, + Options options + ) { var sw = Stopwatch.StartNew(); bool success; @@ -336,7 +392,9 @@ static class NestConsole nestItems.Add(new NestItem { Drawing = d, Quantity = qty }); } - Console.WriteLine($"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts"); + Console.WriteLine( + $"AutoNest: {nestItems.Count} drawing(s), {nestItems.Sum(i => i.Quantity)} total parts" + ); var engine = NestEngineRegistry.Create(plate); var nestParts = engine.Nest(nestItems, null, CancellationToken.None); @@ -360,9 +418,11 @@ static class NestConsole return 0; var hasOverlaps = plate.HasOverlappingParts(out var overlapPts); - Console.WriteLine(hasOverlaps - ? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points" - : "Overlap check: PASS"); + Console.WriteLine( + hasOverlaps + ? $"OVERLAPS DETECTED: {overlapPts.Count} intersection points" + : "Overlap check: PASS" + ); return overlapPts.Count; } @@ -381,9 +441,12 @@ static class NestConsole return; var firstInput = options.InputFiles[0]; - var outputFile = options.OutputFile ?? Path.Combine( - Path.GetDirectoryName(firstInput), - $"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}"); + var outputFile = + options.OutputFile + ?? Path.Combine( + Path.GetDirectoryName(firstInput), + $"{Path.GetFileNameWithoutExtension(firstInput)}-result{NestFormat.FileExtension}" + ); new NestWriter(nest).Write(outputFile); Console.WriteLine($"Saved: {outputFile}"); @@ -394,8 +457,8 @@ static class NestConsole if (options.PostsDir != null) return options.PostsDir; - var exePath = Assembly.GetEntryAssembly()?.Location - ?? typeof(NestConsole).Assembly.Location; + var exePath = + Assembly.GetEntryAssembly()?.Location ?? typeof(NestConsole).Assembly.Location; return Path.Combine(Path.GetDirectoryName(exePath), "Posts"); } @@ -414,7 +477,11 @@ static class NestConsole foreach (var type in assembly.GetTypes()) { - if (!typeof(IPostProcessor).IsAssignableFrom(type) || type.IsInterface || type.IsAbstract) + if ( + !typeof(IPostProcessor).IsAssignableFrom(type) + || type.IsInterface + || type.IsAbstract + ) continue; if (Activator.CreateInstance(type) is IPostProcessor processor) @@ -423,7 +490,9 @@ static class NestConsole } catch (Exception ex) { - Console.Error.WriteLine($"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}"); + Console.Error.WriteLine( + $"Warning: failed to load post processor from {Path.GetFileName(file)}: {ex.Message}" + ); } } @@ -444,7 +513,7 @@ static class NestConsole Console.WriteLine($"Post processors ({postsDir}):"); foreach (var p in processors) - Console.WriteLine($" {p.Name,-30} {p.Description}"); + Console.WriteLine($" {p.Name, -30} {p.Description}"); } static void PostProcess(Nest nest, Options options) @@ -455,14 +524,17 @@ static class NestConsole var postsDir = ResolvePostsDir(options); var processors = LoadPostProcessors(postsDir); var post = processors.FirstOrDefault(p => - p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase)); + p.Name.Equals(options.PostName, StringComparison.OrdinalIgnoreCase) + ); if (post == null) { Console.Error.WriteLine($"Error: post processor '{options.PostName}' not found"); if (processors.Count > 0) - Console.Error.WriteLine($"Available: {string.Join(", ", processors.Select(p => p.Name))}"); + Console.Error.WriteLine( + $"Available: {string.Join(", ", processors.Select(p => p.Name))}" + ); else Console.Error.WriteLine($"No post processors found in: {postsDir}"); @@ -476,7 +548,8 @@ static class NestConsole var firstInput = options.InputFiles[0]; outputFile = Path.Combine( Path.GetDirectoryName(firstInput), - $"{Path.GetFileNameWithoutExtension(firstInput)}.cnc"); + $"{Path.GetFileNameWithoutExtension(firstInput)}.cnc" + ); } post.Post(nest, outputFile); @@ -488,30 +561,58 @@ static class NestConsole Console.Error.WriteLine("Usage: OpenNest.Console [options]"); Console.Error.WriteLine(); Console.Error.WriteLine("Arguments:"); - Console.Error.WriteLine(" input-files One or more .nest nest files or .dxf/.dwg drawing files"); + Console.Error.WriteLine( + " input-files One or more .nest nest files or .dxf/.dwg drawing files" + ); Console.Error.WriteLine(); Console.Error.WriteLine("Modes:"); Console.Error.WriteLine(" Load nest and fill (existing behavior)"); Console.Error.WriteLine(" --size WxL Import DXF, create plate, and fill"); - Console.Error.WriteLine(" Load nest and add imported DXF drawings"); + Console.Error.WriteLine( + " Load nest and add imported DXF drawings" + ); Console.Error.WriteLine(); Console.Error.WriteLine("Options:"); - Console.Error.WriteLine(" --repair-bends-mm Opt-in endpoint/tick repair, limit >0.001 to 3.175 physical mm"); - Console.Error.WriteLine(" --cad-units inches|mm Explicit source coordinate units required for bend repair"); - Console.Error.WriteLine(" --drawing Drawing name to fill with (default: first drawing)"); + Console.Error.WriteLine( + " --repair-bends-mm Opt-in endpoint/tick repair, limit >0.001 to 3.175 physical mm" + ); + Console.Error.WriteLine( + " --cad-units inches|mm Explicit source coordinate units required for bend repair" + ); + Console.Error.WriteLine( + " --drawing Drawing name to fill with (default: first drawing)" + ); Console.Error.WriteLine(" --plate Plate index to fill (default: 0)"); - Console.Error.WriteLine(" --quantity Max parts to place (default: 0 = unlimited)"); + Console.Error.WriteLine( + " --quantity Max parts to place (default: 0 = unlimited)" + ); Console.Error.WriteLine(" --spacing Override part spacing"); - Console.Error.WriteLine(" --size Override plate size (e.g. 60x120); required for DXF-only mode"); - Console.Error.WriteLine(" --output Output nest file path (default: -result.nest)"); - Console.Error.WriteLine(" --template Nest template for plate defaults (thickness, quadrant, material, spacing)"); - Console.Error.WriteLine(" --autonest Use NFP-based mixed-part autonesting instead of linear fill"); - Console.Error.WriteLine(" --keep-parts Don't clear existing parts before filling"); - Console.Error.WriteLine(" --check-overlaps Run overlap detection after fill (exit code 1 if found)"); + Console.Error.WriteLine( + " --size Override plate size (e.g. 60x120); required for DXF-only mode" + ); + Console.Error.WriteLine( + " --output Output nest file path (default: -result.nest)" + ); + Console.Error.WriteLine( + " --template Nest template for plate defaults (thickness, quadrant, material, spacing)" + ); + Console.Error.WriteLine( + " --autonest Use NFP-based mixed-part autonesting instead of linear fill" + ); + Console.Error.WriteLine( + " --keep-parts Don't clear existing parts before filling" + ); + Console.Error.WriteLine( + " --check-overlaps Run overlap detection after fill (exit code 1 if found)" + ); Console.Error.WriteLine(" --no-save Skip saving output file"); Console.Error.WriteLine(" --post Run a post processor after nesting"); - Console.Error.WriteLine(" --post-output Output file for post processor (default: .cnc)"); - Console.Error.WriteLine(" --posts-dir Directory containing post processor DLLs (default: Posts/)"); + Console.Error.WriteLine( + " --post-output Output file for post processor (default: .cnc)" + ); + Console.Error.WriteLine( + " --posts-dir Directory containing post processor DLLs (default: Posts/)" + ); Console.Error.WriteLine(" --list-posts List available post processors and exit"); Console.Error.WriteLine(" -h, --help Show this help"); } diff --git a/OpenNest.Core/Align.cs b/OpenNest.Core/Align.cs index c419614..0808c1c 100644 --- a/OpenNest.Core/Align.cs +++ b/OpenNest.Core/Align.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest { @@ -7,7 +7,10 @@ namespace OpenNest { public static void Vertically(Entity fixedEntity, Entity movableEntity) { - movableEntity.Offset(fixedEntity.BoundingBox.Center.X - movableEntity.BoundingBox.Center.X, 0); + movableEntity.Offset( + fixedEntity.BoundingBox.Center.X - movableEntity.BoundingBox.Center.X, + 0 + ); } public static void Vertically(Entity fixedEntity, List entities) @@ -17,7 +20,10 @@ namespace OpenNest public static void Vertically(Part fixedPart, Part movablePart) { - movablePart.Offset(fixedPart.BoundingBox.Center.X - movablePart.BoundingBox.Center.X, 0); + movablePart.Offset( + fixedPart.BoundingBox.Center.X - movablePart.BoundingBox.Center.X, + 0 + ); } public static void Vertically(Part fixedPart, List parts) @@ -27,7 +33,10 @@ namespace OpenNest public static void Horizontally(Entity fixedEntity, Entity movableEntity) { - movableEntity.Offset(0, fixedEntity.BoundingBox.Center.Y - movableEntity.BoundingBox.Center.Y); + movableEntity.Offset( + 0, + fixedEntity.BoundingBox.Center.Y - movableEntity.BoundingBox.Center.Y + ); } public static void Horizontally(Entity fixedEntity, List entities) @@ -37,7 +46,10 @@ namespace OpenNest public static void Horizontally(Part fixedPart, Part movablePart) { - movablePart.Offset(0, fixedPart.BoundingBox.Center.Y - movablePart.BoundingBox.Center.Y); + movablePart.Offset( + 0, + fixedPart.BoundingBox.Center.Y - movablePart.BoundingBox.Center.Y + ); } public static void Horizontally(Part fixedPart, List parts) @@ -67,7 +79,10 @@ namespace OpenNest public static void Right(Entity fixedEntity, Entity movableEntity) { - movableEntity.Offset(fixedEntity.BoundingBox.Right - movableEntity.BoundingBox.Right, 0); + movableEntity.Offset( + fixedEntity.BoundingBox.Right - movableEntity.BoundingBox.Right, + 0 + ); } public static void Right(Entity fixedEntity, List entities) @@ -107,7 +122,10 @@ namespace OpenNest public static void Bottom(Entity fixedEntity, Entity movableEntity) { - movableEntity.Offset(0, fixedEntity.BoundingBox.Bottom - movableEntity.BoundingBox.Bottom); + movableEntity.Offset( + 0, + fixedEntity.BoundingBox.Bottom - movableEntity.BoundingBox.Bottom + ); } public static void Bottom(Entity fixedEntity, List entities) @@ -137,14 +155,19 @@ namespace OpenNest return; var list = new List(parts); - list.Sort((p1, p2) => horizontal - ? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X) - : p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y)); + list.Sort( + (p1, p2) => + horizontal + ? p1.BoundingBox.Center.X.CompareTo(p2.BoundingBox.Center.X) + : p1.BoundingBox.Center.Y.CompareTo(p2.BoundingBox.Center.Y) + ); var lastIndex = list.Count - 1; var start = horizontal ? list[0].BoundingBox.Center.X : list[0].BoundingBox.Center.Y; - var end = horizontal ? list[lastIndex].BoundingBox.Center.X : list[lastIndex].BoundingBox.Center.Y; + var end = horizontal + ? list[lastIndex].BoundingBox.Center.X + : list[lastIndex].BoundingBox.Center.Y; var spacing = (end - start) / lastIndex; diff --git a/OpenNest.Core/AlignType.cs b/OpenNest.Core/AlignType.cs index c56bfd1..5f87d61 100644 --- a/OpenNest.Core/AlignType.cs +++ b/OpenNest.Core/AlignType.cs @@ -1,5 +1,4 @@ - -namespace OpenNest +namespace OpenNest { public enum AlignType { @@ -10,6 +9,6 @@ namespace OpenNest Horizontally, Vertically, EvenlySpaceHorizontally, - EvenlySpaceVertically + EvenlySpaceVertically, } } diff --git a/OpenNest.Core/Bending/Bend.cs b/OpenNest.Core/Bending/Bend.cs index 5ed454b..50c8b58 100644 --- a/OpenNest.Core/Bending/Bend.cs +++ b/OpenNest.Core/Bending/Bend.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Generic; using System.Drawing; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Bending { @@ -10,7 +10,7 @@ namespace OpenNest.Bending public static readonly Layer EtchLayer = new Layer("ETCH") { Color = Color.Green, - IsVisible = true + IsVisible = true, }; private const double DefaultEtchLength = 1.0; @@ -32,9 +32,8 @@ namespace OpenNest.Bending public double Length => StartPoint.DistanceTo(EndPoint); - public double AngleRadians => Angle.HasValue - ? OpenNest.Math.Angle.ToRadians(Angle.Value) - : 0; + public double AngleRadians => + Angle.HasValue ? OpenNest.Math.Angle.ToRadians(Angle.Value) : 0; public Line ToLine() => new Line(StartPoint, EndPoint); @@ -66,7 +65,9 @@ namespace OpenNest.Bending var dx = System.Math.Cos(angle) * etchLength; var dy = System.Math.Sin(angle) * etchLength; - result.Add(CreateEtchLine(StartPoint, new Vector(StartPoint.X + dx, StartPoint.Y + dy))); + result.Add( + CreateEtchLine(StartPoint, new Vector(StartPoint.X + dx, StartPoint.Y + dy)) + ); result.Add(CreateEtchLine(new Vector(EndPoint.X - dx, EndPoint.Y - dy), EndPoint)); } @@ -79,7 +80,8 @@ namespace OpenNest.Bending public static void UpdateEtchEntities(List entities, List bends) { entities.RemoveAll(e => e.Tag == BendEtchTag); - if (bends == null) return; + if (bends == null) + return; foreach (var bend in bends) entities.AddRange(bend.GetEtchEntities()); @@ -87,7 +89,12 @@ namespace OpenNest.Bending private static Line CreateEtchLine(Vector start, Vector end) { - return new Line(start, end) { Layer = EtchLayer, Color = Color.Green, Tag = BendEtchTag }; + return new Line(start, end) + { + Layer = EtchLayer, + Color = Color.Green, + Tag = BendEtchTag, + }; } public override string ToString() diff --git a/OpenNest.Core/Bending/BendDirection.cs b/OpenNest.Core/Bending/BendDirection.cs index 2ac60d7..cbcd65b 100644 --- a/OpenNest.Core/Bending/BendDirection.cs +++ b/OpenNest.Core/Bending/BendDirection.cs @@ -4,6 +4,6 @@ namespace OpenNest.Bending { Unknown, Up, - Down + Down, } } diff --git a/OpenNest.Core/CNC/ArcMove.cs b/OpenNest.Core/CNC/ArcMove.cs index dd369c3..4a5434f 100644 --- a/OpenNest.Core/CNC/ArcMove.cs +++ b/OpenNest.Core/CNC/ArcMove.cs @@ -5,16 +5,22 @@ namespace OpenNest.CNC { public class ArcMove : Motion { - public ArcMove() - { - } + public ArcMove() { } - public ArcMove(double x, double y, double i, double j, RotationType rotation = RotationType.CCW) - : this(new Vector(x, y), new Vector(i, j), rotation) - { - } + public ArcMove( + double x, + double y, + double i, + double j, + RotationType rotation = RotationType.CCW + ) + : this(new Vector(x, y), new Vector(i, j), rotation) { } - public ArcMove(Vector endPoint, Vector centerPoint, RotationType rotation = RotationType.CCW) + public ArcMove( + Vector endPoint, + Vector centerPoint, + RotationType rotation = RotationType.CCW + ) { EndPoint = endPoint; CenterPoint = centerPoint; @@ -68,7 +74,8 @@ namespace OpenNest.CNC { Layer = Layer, Suppressed = Suppressed, - VariableRefs = VariableRefs != null ? new Dictionary(VariableRefs) : null + VariableRefs = + VariableRefs != null ? new Dictionary(VariableRefs) : null, }; } @@ -85,9 +92,9 @@ namespace OpenNest.CNC var i = CenterPoint.X.ToString(dp); var j = CenterPoint.Y.ToString(dp); - return Rotation == RotationType.CW ? - string.Format("G02 X{0} Y{1} I{2} J{3}", x, y, i, j) : - string.Format("G03 X{0} Y{1} I{2} J{3}", x, y, i, j); + return Rotation == RotationType.CW + ? string.Format("G02 X{0} Y{1} I{2} J{3}", x, y, i, j) + : string.Format("G03 X{0} Y{1} I{2} J{3}", x, y, i, j); } } } diff --git a/OpenNest.Core/CNC/CodeType.cs b/OpenNest.Core/CNC/CodeType.cs index c2962ef..2146499 100644 --- a/OpenNest.Core/CNC/CodeType.cs +++ b/OpenNest.Core/CNC/CodeType.cs @@ -1,5 +1,4 @@ - -namespace OpenNest.CNC +namespace OpenNest.CNC { public enum CodeType { @@ -9,6 +8,6 @@ namespace OpenNest.CNC RapidMove, SetFeedrate, SetKerf, - SubProgramCall + SubProgramCall, } } diff --git a/OpenNest.Core/CNC/Comment.cs b/OpenNest.Core/CNC/Comment.cs index b2aa4dd..73e68eb 100644 --- a/OpenNest.Core/CNC/Comment.cs +++ b/OpenNest.Core/CNC/Comment.cs @@ -2,9 +2,7 @@ { public class Comment : ICode { - public Comment() - { - } + public Comment() { } public Comment(string value) { diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs index d19a713..454d62f 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Generic; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.CNC.CuttingStrategy { @@ -48,7 +48,12 @@ namespace OpenNest.CNC.CuttingStrategy for (var iter = 0; iter < 3; iter++) { var lastCutoutPt = cutoutEntries[cutoutEntries.Count - 1].Point; - perimeterSeed = FindPerimeterIntersection(profile.Perimeter, lastCutoutPt, nextPartStart, out _); + perimeterSeed = FindPerimeterIntersection( + profile.Perimeter, + lastCutoutPt, + nextPartStart, + out _ + ); orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterSeed); orderedCutouts.Reverse(); @@ -56,7 +61,12 @@ namespace OpenNest.CNC.CuttingStrategy } var finalLastCutout = cutoutEntries[cutoutEntries.Count - 1].Point; - perimeterPt = FindPerimeterIntersection(profile.Perimeter, finalLastCutout, nextPartStart, out perimeterEntity); + perimeterPt = FindPerimeterIntersection( + profile.Perimeter, + finalLastCutout, + nextPartStart, + out perimeterEntity + ); } else { @@ -79,18 +89,25 @@ namespace OpenNest.CNC.CuttingStrategy if (!profile.Perimeter.IsClosed()) EmitRawContour(result, profile.Perimeter); else - EmitContour(result, profile.Perimeter, perimeterPt, perimeterEntity, ContourType.External); + EmitContour( + result, + profile.Perimeter, + perimeterPt, + perimeterEntity, + ContourType.External + ); result.Mode = Mode.Incremental; - return new CuttingResult - { - Program = result, - LastCutPoint = perimeterPt - }; + return new CuttingResult { Program = result, LastCutPoint = perimeterPt }; } - public CuttingResult ApplySingle(Program partProgram, Vector point, Entity entity, ContourType contourType) + public CuttingResult ApplySingle( + Program partProgram, + Vector point, + Entity entity, + ContourType contourType + ) { var entities = partProgram.ToGeometry(); entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid); @@ -141,14 +158,14 @@ namespace OpenNest.CNC.CuttingStrategy result.Mode = Mode.Incremental; - return new CuttingResult - { - Program = result, - LastCutPoint = point - }; + return new CuttingResult { Program = result, LastCutPoint = point }; } - private static (Shape Shape, Entity Entity) FindTargetShape(ShapeProfile profile, Vector point, Entity clickedEntity) + private static (Shape Shape, Entity Entity) FindTargetShape( + ShapeProfile profile, + Vector point, + Entity clickedEntity + ) { var matched = FindMatchingEntity(profile.Perimeter, clickedEntity); if (matched != null) @@ -190,20 +207,26 @@ namespace OpenNest.CNC.CuttingStrategy if (shapeEntity is Line sLine && clickedEntity is Line cLine) { - if (sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon - && sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon) + if ( + sLine.StartPoint.DistanceTo(cLine.StartPoint) < Math.Tolerance.Epsilon + && sLine.EndPoint.DistanceTo(cLine.EndPoint) < Math.Tolerance.Epsilon + ) return shapeEntity; } else if (shapeEntity is Arc sArc && clickedEntity is Arc cArc) { - if (System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon - && sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon) + if ( + System.Math.Abs(sArc.Radius - cArc.Radius) < Math.Tolerance.Epsilon + && sArc.Center.DistanceTo(cArc.Center) < Math.Tolerance.Epsilon + ) return shapeEntity; } else if (shapeEntity is Circle sCircle && clickedEntity is Circle cCircle) { - if (System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon - && sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon) + if ( + System.Math.Abs(sCircle.Radius - cCircle.Radius) < Math.Tolerance.Epsilon + && sCircle.Center.DistanceTo(cCircle.Center) < Math.Tolerance.Epsilon + ) return shapeEntity; } } @@ -218,7 +241,10 @@ namespace OpenNest.CNC.CuttingStrategy program.Codes.AddRange(ConvertShapeToMoves(shape, startPoint)); } - private static List ResolveLeadInPoints(List cutouts, Vector startPoint) + private static List ResolveLeadInPoints( + List cutouts, + Vector startPoint + ) { var entries = new ContourEntry[cutouts.Count]; var currentPoint = startPoint; @@ -235,7 +261,12 @@ namespace OpenNest.CNC.CuttingStrategy return new List(entries); } - private static Vector FindPerimeterIntersection(Shape perimeter, Vector lastCutout, Vector nextPartStart, out Entity entity) + private static Vector FindPerimeterIntersection( + Shape perimeter, + Vector lastCutout, + Vector nextPartStart, + out Entity entity + ) { var ray = new Line(lastCutout, nextPartStart); @@ -269,7 +300,13 @@ namespace OpenNest.CNC.CuttingStrategy return HashCode.Combine(r, a); } - private void EmitContour(Program program, Shape shape, Vector point, Entity entity, ContourType? forceType = null) + private void EmitContour( + Program program, + Shape shape, + Vector point, + Entity entity, + ContourType? forceType = null + ) { var contourType = forceType ?? DetectContourType(shape); var winding = DetermineWinding(shape); @@ -289,7 +326,8 @@ namespace OpenNest.CNC.CuttingStrategy var outwardAngle = normal - System.Math.PI; point = new Vector( circle.Center.X + circle.Radius * System.Math.Cos(outwardAngle), - circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle)); + circle.Center.Y + circle.Radius * System.Math.Sin(outwardAngle) + ); } leadIn = ClampLeadInForCircle(leadIn, circle, point, normal); @@ -297,7 +335,10 @@ namespace OpenNest.CNC.CuttingStrategy // Build hole sub-program relative to (0,0) var holeCenter = circle.Center; var relativePoint = new Vector(point.X - holeCenter.X, point.Y - holeCenter.Y); - var relativeCircle = new Circle(new Vector(0, 0), circle.Radius) { Rotation = circle.Rotation }; + var relativeCircle = new Circle(new Vector(0, 0), circle.Radius) + { + Rotation = circle.Rotation, + }; var relativeShape = new Shape(); relativeShape.Entities.Add(relativeCircle); @@ -314,12 +355,14 @@ namespace OpenNest.CNC.CuttingStrategy if (!program.SubPrograms.ContainsKey(key)) program.SubPrograms[key] = subPgm; - program.Codes.Add(new SubProgramCall - { - Id = key, - Program = program.SubPrograms[key], - Offset = holeCenter - }); + program.Codes.Add( + new SubProgramCall + { + Id = key, + Program = program.SubPrograms[key], + Offset = holeCenter, + } + ); return; } @@ -328,7 +371,11 @@ namespace OpenNest.CNC.CuttingStrategy var reindexedShape = shape.ReindexAt(point, entity); - if (Parameters.TabsEnabled && Parameters.TabConfig != null && contourType == ContourType.External) + if ( + Parameters.TabsEnabled + && Parameters.TabConfig != null + && contourType == ContourType.External + ) reindexedShape = TrimShapeForTab(reindexedShape, point, Parameters.TabConfig.Size); program.Codes.AddRange(ConvertShapeToMoves(reindexedShape, point)); @@ -337,7 +384,8 @@ namespace OpenNest.CNC.CuttingStrategy private void EmitScribeContours(Program program, List scribeEntities) { - if (scribeEntities.Count == 0) return; + if (scribeEntities.Count == 0) + return; var shapes = ShapeBuilder.GetShapes(scribeEntities); foreach (var shape in shapes) @@ -388,8 +436,12 @@ namespace OpenNest.CNC.CuttingStrategy return ContourType.Internal; } - public static double ComputeNormal(Vector point, Entity entity, ContourType contourType, - RotationType winding = RotationType.CW) + public static double ComputeNormal( + Vector point, + Entity entity, + ContourType contourType, + RotationType winding = RotationType.CW + ) { double normal; @@ -442,7 +494,12 @@ namespace OpenNest.CNC.CuttingStrategy return polygon.RotationDirection(); } - private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle) + private LeadIn ClampLeadInForCircle( + LeadIn leadIn, + Circle circle, + Vector contourPoint, + double normalAngle + ) { if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0) return leadIn; @@ -492,7 +549,7 @@ namespace OpenNest.CNC.CuttingStrategy { ContourType.ArcCircle => Parameters.ArcCircleLeadIn ?? Parameters.InternalLeadIn, ContourType.Internal => Parameters.InternalLeadIn, - _ => Parameters.ExternalLeadIn + _ => Parameters.ExternalLeadIn, }; } @@ -502,7 +559,7 @@ namespace OpenNest.CNC.CuttingStrategy { ContourType.ArcCircle => Parameters.ArcCircleLeadOut ?? Parameters.InternalLeadOut, ContourType.Internal => Parameters.InternalLeadOut, - _ => Parameters.ExternalLeadOut + _ => Parameters.ExternalLeadOut, }; } @@ -565,12 +622,18 @@ namespace OpenNest.CNC.CuttingStrategy private static Vector EntityStartPoint(Entity entity) { - if (entity is Line line) return line.StartPoint; - if (entity is Arc arc) return arc.StartPoint(); + if (entity is Line line) + return line.StartPoint; + if (entity is Arc arc) + return arc.StartPoint(); return Vector.Zero; } - private List ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display) + private List ConvertShapeToMoves( + Shape shape, + Vector startPoint, + LayerType layer = LayerType.Display + ) { var moves = new List(); @@ -582,15 +645,28 @@ namespace OpenNest.CNC.CuttingStrategy } else if (entity is Arc arc) { - moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW) { Layer = layer }); + moves.Add( + new ArcMove( + arc.EndPoint(), + arc.Center, + arc.IsReversed ? RotationType.CW : RotationType.CCW + ) + { + Layer = layer, + } + ); } else if (entity is Circle circle) { - moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer }); + moves.Add( + new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer } + ); } else { - throw new System.InvalidOperationException($"Unsupported entity type: {entity.Type}"); + throw new System.InvalidOperationException( + $"Unsupported entity type: {entity.Type}" + ); } } @@ -600,9 +676,12 @@ namespace OpenNest.CNC.CuttingStrategy private static Vector GetShapeStartPoint(Shape shape) { var first = shape.Entities[0]; - if (first is Line line) return line.StartPoint; - if (first is Arc arc) return arc.StartPoint(); - if (first is Circle circle) return new Vector(circle.Center.X + circle.Radius, circle.Center.Y); + if (first is Line line) + return line.StartPoint; + if (first is Arc arc) + return arc.StartPoint(); + if (first is Circle circle) + return new Vector(circle.Center.X + circle.Radius, circle.Center.Y); return Vector.Zero; } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/ContourType.cs b/OpenNest.Core/CNC/CuttingStrategy/ContourType.cs index 9205913..015132d 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/ContourType.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/ContourType.cs @@ -4,6 +4,6 @@ namespace OpenNest.CNC.CuttingStrategy { External, Internal, - ArcCircle + ArcCircle, } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs b/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs index 15a1e09..512423f 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/CuttingParameters.cs @@ -15,7 +15,8 @@ namespace OpenNest.CNC.CuttingStrategy public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn(); public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut(); - public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }; + public LeadIn InternalLeadIn { get; set; } = + new LineLeadIn { Length = 0.125, ApproachAngle = 90 }; public LeadOut InternalLeadOut { get; set; } = new NoLeadOut(); public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn(); diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs index cdb6c32..10eaf16 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/ArcLeadIn.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { @@ -7,19 +7,23 @@ namespace OpenNest.CNC.CuttingStrategy { public double Radius { get; set; } - public override List Generate(Vector contourStartPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourStartPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var arcCenter = new Vector( contourStartPoint.X + Radius * System.Math.Cos(contourNormalAngle), - contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle)); + contourStartPoint.Y + Radius * System.Math.Sin(contourNormalAngle) + ); return new List { new RapidMove(piercePoint), - new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } + new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }, }; } @@ -30,10 +34,10 @@ namespace OpenNest.CNC.CuttingStrategy return new Vector( arcCenterX + Radius * System.Math.Cos(contourNormalAngle), - arcCenterY + Radius * System.Math.Sin(contourNormalAngle)); + arcCenterY + Radius * System.Math.Sin(contourNormalAngle) + ); } - public override LeadIn Scale(double factor) => - new ArcLeadIn { Radius = Radius * factor }; + public override LeadIn Scale(double factor) => new ArcLeadIn { Radius = Radius * factor }; } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs index ef79235..e09d3ac 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/CleanHoleLeadIn.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { @@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy public double ArcRadius { get; set; } public double Kerf { get; set; } - public override List Generate(Vector contourStartPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourStartPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); @@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy var lineAngle = contourNormalAngle + Angle.ToRadians(135.0); var arcStart = new Vector( arcCenterX + ArcRadius * System.Math.Cos(lineAngle), - arcCenterY + ArcRadius * System.Math.Sin(lineAngle)); + arcCenterY + ArcRadius * System.Math.Sin(lineAngle) + ); return new List { new RapidMove(piercePoint), new LinearMove(arcStart) { Layer = LayerType.Leadin }, - new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } + new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }, }; } @@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy return new Vector( arcStartX + LineLength * System.Math.Cos(lineAngle), - arcStartY + LineLength * System.Math.Sin(lineAngle)); + arcStartY + LineLength * System.Math.Sin(lineAngle) + ); } public override LeadIn Scale(double factor) => - new CleanHoleLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, Kerf = Kerf }; + new CleanHoleLeadIn + { + LineLength = LineLength * factor, + ArcRadius = ArcRadius * factor, + Kerf = Kerf, + }; } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs index 3dc0323..8ffc9af 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LeadIn.cs @@ -1,12 +1,15 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public abstract class LeadIn { - public abstract List Generate(Vector contourStartPoint, double contourNormalAngle, - RotationType winding = RotationType.CW); + public abstract List Generate( + Vector contourStartPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ); public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle); diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs index 1afde38..fdf620b 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineArcLeadIn.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { @@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy public double ApproachAngle { get; set; } = 135.0; public double ArcRadius { get; set; } - public override List Generate(Vector contourStartPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourStartPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); @@ -22,13 +25,14 @@ namespace OpenNest.CNC.CuttingStrategy var lineAngle = contourNormalAngle + Angle.ToRadians(ApproachAngle); var arcStart = new Vector( arcCenterX + ArcRadius * System.Math.Cos(lineAngle), - arcCenterY + ArcRadius * System.Math.Sin(lineAngle)); + arcCenterY + ArcRadius * System.Math.Sin(lineAngle) + ); return new List { new RapidMove(piercePoint), new LinearMove(arcStart) { Layer = LayerType.Leadin }, - new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin } + new ArcMove(contourStartPoint, arcCenter, winding) { Layer = LayerType.Leadin }, }; } @@ -43,10 +47,16 @@ namespace OpenNest.CNC.CuttingStrategy return new Vector( arcStartX + LineLength * System.Math.Cos(lineAngle), - arcStartY + LineLength * System.Math.Sin(lineAngle)); + arcStartY + LineLength * System.Math.Sin(lineAngle) + ); } public override LeadIn Scale(double factor) => - new LineArcLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, ApproachAngle = ApproachAngle }; + new LineArcLeadIn + { + LineLength = LineLength * factor, + ArcRadius = ArcRadius * factor, + ApproachAngle = ApproachAngle, + }; } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs index c87caef..8e9a4ef 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLeadIn.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { @@ -9,15 +9,18 @@ namespace OpenNest.CNC.CuttingStrategy public double Length { get; set; } public double ApproachAngle { get; set; } = 90.0; - public override List Generate(Vector contourStartPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourStartPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); return new List { new RapidMove(piercePoint), - new LinearMove(contourStartPoint) { Layer = LayerType.Leadin } + new LinearMove(contourStartPoint) { Layer = LayerType.Leadin }, }; } @@ -26,7 +29,8 @@ namespace OpenNest.CNC.CuttingStrategy var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle); return new Vector( contourStartPoint.X + Length * System.Math.Cos(approachAngle), - contourStartPoint.Y + Length * System.Math.Sin(approachAngle)); + contourStartPoint.Y + Length * System.Math.Sin(approachAngle) + ); } public override LeadIn Scale(double factor) => diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs index 8db3b2c..8fca358 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/LineLineLeadIn.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { @@ -11,21 +11,25 @@ namespace OpenNest.CNC.CuttingStrategy public double Length2 { get; set; } public double ApproachAngle2 { get; set; } = 90.0; - public override List Generate(Vector contourStartPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourStartPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle); var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1); var midPoint = new Vector( contourStartPoint.X + Length2 * System.Math.Cos(secondAngle), - contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle)); + contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle) + ); return new List { new RapidMove(piercePoint), new LinearMove(midPoint) { Layer = LayerType.Leadin }, - new LinearMove(contourStartPoint) { Layer = LayerType.Leadin } + new LinearMove(contourStartPoint) { Layer = LayerType.Leadin }, }; } @@ -38,10 +42,17 @@ namespace OpenNest.CNC.CuttingStrategy var firstAngle = secondAngle + Angle.ToRadians(ApproachAngle2); return new Vector( midX + Length1 * System.Math.Cos(firstAngle), - midY + Length1 * System.Math.Sin(firstAngle)); + midY + Length1 * System.Math.Sin(firstAngle) + ); } public override LeadIn Scale(double factor) => - new LineLineLeadIn { Length1 = Length1 * factor, ApproachAngle1 = ApproachAngle1, Length2 = Length2 * factor, ApproachAngle2 = ApproachAngle2 }; + new LineLineLeadIn + { + Length1 = Length1 * factor, + ApproachAngle1 = ApproachAngle1, + Length2 = Length2 * factor, + ApproachAngle2 = ApproachAngle2, + }; } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs index a751727..7ad1084 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadIns/NoLeadIn.cs @@ -1,17 +1,17 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class NoLeadIn : LeadIn { - public override List Generate(Vector contourStartPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourStartPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { - return new List - { - new RapidMove(contourStartPoint) - }; + return new List { new RapidMove(contourStartPoint) }; } public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle) diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs index 927876c..b729a8e 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/ArcLeadOut.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { @@ -7,8 +7,11 @@ namespace OpenNest.CNC.CuttingStrategy { public double Radius { get; set; } - public override List Generate(Vector contourEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var arcCenterX = contourEndPoint.X + Radius * System.Math.Cos(contourNormalAngle); var arcCenterY = contourEndPoint.Y + Radius * System.Math.Sin(contourNormalAngle); @@ -16,11 +19,12 @@ namespace OpenNest.CNC.CuttingStrategy var endPoint = new Vector( arcCenterX + Radius * System.Math.Cos(contourNormalAngle + System.Math.PI / 2), - arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2)); + arcCenterY + Radius * System.Math.Sin(contourNormalAngle + System.Math.PI / 2) + ); return new List { - new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout } + new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout }, }; } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs index 804427b..17fa222 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LeadOut.cs @@ -1,11 +1,14 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public abstract class LeadOut { - public abstract List Generate(Vector contourEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW); + public abstract List Generate( + Vector contourEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ); } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs index 70fa9d2..a7bafa6 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/LineLeadOut.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.CNC.CuttingStrategy { @@ -9,18 +9,19 @@ namespace OpenNest.CNC.CuttingStrategy public double Length { get; set; } public double ApproachAngle { get; set; } = 90.0; - public override List Generate(Vector contourEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var overcutAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle); var endPoint = new Vector( contourEndPoint.X + Length * System.Math.Cos(overcutAngle), - contourEndPoint.Y + Length * System.Math.Sin(overcutAngle)); + contourEndPoint.Y + Length * System.Math.Sin(overcutAngle) + ); - return new List - { - new LinearMove(endPoint) { Layer = LayerType.Leadout } - }; + return new List { new LinearMove(endPoint) { Layer = LayerType.Leadout } }; } } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs index 0e3b53f..6fae051 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/LeadOuts/NoLeadOut.cs @@ -1,12 +1,15 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { public class NoLeadOut : LeadOut { - public override List Generate(Vector contourEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + public override List Generate( + Vector contourEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { return new List(); } diff --git a/OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs b/OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs index 910d333..75c32bb 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/SequenceParameters.cs @@ -9,7 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy BottomSide = 4, EdgeStart = 5, LeftSide = 7, - RightSideAlt = 8 + RightSideAlt = 8, } public class SequenceParameters diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs index bb306a6..3f7b986 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/BreakerTab.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { @@ -10,8 +10,11 @@ namespace OpenNest.CNC.CuttingStrategy public double BreakerAngle { get; set; } public override List Generate( - Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + Vector tabStartPoint, + Vector tabEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var codes = new List(); @@ -21,7 +24,8 @@ namespace OpenNest.CNC.CuttingStrategy var scoreAngle = contourNormalAngle + System.Math.PI; var scoreEnd = new Vector( tabStartPoint.X + BreakerDepth * System.Math.Cos(scoreAngle), - tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle)); + tabStartPoint.Y + BreakerDepth * System.Math.Sin(scoreAngle) + ); codes.Add(new LinearMove(scoreEnd)); codes.Add(new RapidMove(tabEndPoint)); diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs index 10d2954..4c5b63d 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/MachineTab.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { @@ -8,13 +8,13 @@ namespace OpenNest.CNC.CuttingStrategy public int MachineTabId { get; set; } public override List Generate( - Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + Vector tabStartPoint, + Vector tabEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { - return new List - { - new RapidMove(tabEndPoint) - }; + return new List { new RapidMove(tabEndPoint) }; } } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs index 908746a..3465166 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/NormalTab.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { @@ -11,8 +11,11 @@ namespace OpenNest.CNC.CuttingStrategy public double CutoutMaxHeight { get; set; } public override List Generate( - Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW) + Vector tabStartPoint, + Vector tabEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ) { var codes = new List(); @@ -29,8 +32,10 @@ namespace OpenNest.CNC.CuttingStrategy public bool AppliesToCutout(double cutoutWidth, double cutoutHeight) { - return cutoutWidth >= CutoutMinWidth && cutoutWidth <= CutoutMaxWidth - && cutoutHeight >= CutoutMinHeight && cutoutHeight <= CutoutMaxHeight; + return cutoutWidth >= CutoutMinWidth + && cutoutWidth <= CutoutMaxWidth + && cutoutHeight >= CutoutMinHeight + && cutoutHeight <= CutoutMaxHeight; } } } diff --git a/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs b/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs index 87a8d85..0c1c490 100644 --- a/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs +++ b/OpenNest.Core/CNC/CuttingStrategy/Tabs/Tab.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC.CuttingStrategy { @@ -10,7 +10,10 @@ namespace OpenNest.CNC.CuttingStrategy public LeadOut TabLeadOut { get; set; } public abstract List Generate( - Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle, - RotationType winding = RotationType.CW); + Vector tabStartPoint, + Vector tabEndPoint, + double contourNormalAngle, + RotationType winding = RotationType.CW + ); } } diff --git a/OpenNest.Core/CNC/Feedrate.cs b/OpenNest.Core/CNC/Feedrate.cs index aee5956..fae3809 100644 --- a/OpenNest.Core/CNC/Feedrate.cs +++ b/OpenNest.Core/CNC/Feedrate.cs @@ -6,9 +6,7 @@ public const int UseMax = -2; - public Feedrate() - { - } + public Feedrate() { } public Feedrate(double value) { diff --git a/OpenNest.Core/CNC/KerfType.cs b/OpenNest.Core/CNC/KerfType.cs index 8341cfa..f34d5b6 100644 --- a/OpenNest.Core/CNC/KerfType.cs +++ b/OpenNest.Core/CNC/KerfType.cs @@ -1,10 +1,9 @@ - -namespace OpenNest.CNC +namespace OpenNest.CNC { public enum KerfType { None, Left, - Right + Right, } } diff --git a/OpenNest.Core/CNC/LayerType.cs b/OpenNest.Core/CNC/LayerType.cs index 1d0c0c0..7d4cb77 100644 --- a/OpenNest.Core/CNC/LayerType.cs +++ b/OpenNest.Core/CNC/LayerType.cs @@ -1,5 +1,4 @@ - -namespace OpenNest.CNC +namespace OpenNest.CNC { public enum LayerType { @@ -7,6 +6,6 @@ namespace OpenNest.CNC Scribe, Cut, Leadin, - Leadout + Leadout, } } diff --git a/OpenNest.Core/CNC/LinearMove.cs b/OpenNest.Core/CNC/LinearMove.cs index 7b47721..86116b9 100644 --- a/OpenNest.Core/CNC/LinearMove.cs +++ b/OpenNest.Core/CNC/LinearMove.cs @@ -6,14 +6,10 @@ namespace OpenNest.CNC public class LinearMove : Motion { public LinearMove() - : this(new Vector()) - { - } + : this(new Vector()) { } public LinearMove(double x, double y) - : this(new Vector(x, y)) - { - } + : this(new Vector(x, y)) { } public LinearMove(Vector endPoint) { @@ -34,7 +30,8 @@ namespace OpenNest.CNC { Layer = Layer, Suppressed = Suppressed, - VariableRefs = VariableRefs != null ? new Dictionary(VariableRefs) : null + VariableRefs = + VariableRefs != null ? new Dictionary(VariableRefs) : null, }; } diff --git a/OpenNest.Core/CNC/Mode.cs b/OpenNest.Core/CNC/Mode.cs index eb59145..aea17de 100644 --- a/OpenNest.Core/CNC/Mode.cs +++ b/OpenNest.Core/CNC/Mode.cs @@ -1,9 +1,8 @@ - -namespace OpenNest.CNC +namespace OpenNest.CNC { public enum Mode { Absolute, - Incremental + Incremental, } } diff --git a/OpenNest.Core/CNC/Program.cs b/OpenNest.Core/CNC/Program.cs index c4a7103..3dfb0a3 100644 --- a/OpenNest.Core/CNC/Program.cs +++ b/OpenNest.Core/CNC/Program.cs @@ -1,8 +1,8 @@ +using System; +using System.Collections.Generic; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; -using System; -using System.Collections.Generic; namespace OpenNest.CNC { @@ -10,7 +10,8 @@ namespace OpenNest.CNC { public List Codes; - public Dictionary Variables { get; } = new(StringComparer.OrdinalIgnoreCase); + public Dictionary Variables { get; } = + new(StringComparer.OrdinalIgnoreCase); public Dictionary SubPrograms { get; } = new(); @@ -66,9 +67,17 @@ namespace OpenNest.CNC { if (code is Motion m) { - var cmd = m is RapidMove ? "G00" : (m is ArcMove am ? (am.Rotation == RotationType.CW ? "G02" : "G03") : "G01"); + var cmd = + m is RapidMove + ? "G00" + : ( + m is ArcMove am + ? (am.Rotation == RotationType.CW ? "G02" : "G03") + : "G01" + ); sb.Append($"{cmd}X{m.EndPoint.X:F4}Y{m.EndPoint.Y:F4}"); - if (m is ArcMove arc) sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}"); + if (m is ArcMove arc) + sb.Append($"I{arc.CenterPoint.X:F4}J{arc.CenterPoint.Y:F4}"); sb.AppendLine(); } } @@ -97,7 +106,8 @@ namespace OpenNest.CNC var dy = subpgm.Offset.Y - origin.Y; subpgm.Offset = new Geometry.Vector( origin.X + dx * cos - dy * sin, - origin.Y + dx * sin + dy * cos); + origin.Y + dx * sin + dy * cos + ); } if (subpgm.Program != null) @@ -130,8 +140,7 @@ namespace OpenNest.CNC if (code is SubProgramCall subpgm) { - subpgm.Offset = new Geometry.Vector( - subpgm.Offset.X + x, subpgm.Offset.Y + y); + subpgm.Offset = new Geometry.Vector(subpgm.Offset.X + x, subpgm.Offset.Y + y); } if (code is Motion == false) @@ -159,7 +168,9 @@ namespace OpenNest.CNC if (code is SubProgramCall subpgm) { subpgm.Offset = new Geometry.Vector( - subpgm.Offset.X + voffset.X, subpgm.Offset.Y + voffset.Y); + subpgm.Offset.X + voffset.X, + subpgm.Offset.Y + voffset.Y + ); } if (code is Motion == false) @@ -258,35 +269,37 @@ namespace OpenNest.CNC switch (Mode) { case Mode.Absolute: + { + for (int i = Codes.Count; i >= 0; --i) { - for (int i = Codes.Count; i >= 0; --i) - { - var code = Codes[i]; - var motion = code as Motion; + var code = Codes[i]; + var motion = code as Motion; - if (motion == null) continue; + if (motion == null) + continue; - return motion.EndPoint; - } - break; + return motion.EndPoint; } + break; + } case Mode.Incremental: + { + var pos = new Vector(0, 0); + + for (int i = 0; i < Codes.Count; ++i) { - var pos = new Vector(0, 0); + var code = Codes[i]; + var motion = code as Motion; - for (int i = 0; i < Codes.Count; ++i) - { - var code = Codes[i]; - var motion = code as Motion; + if (motion == null) + continue; - if (motion == null) continue; - - pos += motion.EndPoint; - } - - return pos; + pos += motion.EndPoint; } + + return pos; + } } return new Vector(0, 0); @@ -316,161 +329,163 @@ namespace OpenNest.CNC switch (code.Type) { case CodeType.LinearMove: - { - var line = (LinearMove)code; - var pt = Mode == Mode.Absolute ? - frameOrigin + line.EndPoint : - line.EndPoint + pos; - - if (pt.X > maxX) - maxX = pt.X; - else if (pt.X < minX) - minX = pt.X; - - if (pt.Y > maxY) - maxY = pt.Y; - else if (pt.Y < minY) - minY = pt.Y; - - pos = pt; - - break; - } - - case CodeType.RapidMove: - { - var line = (RapidMove)code; - var pt = Mode == Mode.Absolute + { + var line = (LinearMove)code; + var pt = + Mode == Mode.Absolute ? frameOrigin + line.EndPoint : line.EndPoint + pos; - if (pt.X > maxX) - maxX = pt.X; - else if (pt.X < minX) - minX = pt.X; + if (pt.X > maxX) + maxX = pt.X; + else if (pt.X < minX) + minX = pt.X; - if (pt.Y > maxY) - maxY = pt.Y; - else if (pt.Y < minY) - minY = pt.Y; + if (pt.Y > maxY) + maxY = pt.Y; + else if (pt.Y < minY) + minY = pt.Y; - pos = pt; + pos = pt; - break; - } + break; + } + + case CodeType.RapidMove: + { + var line = (RapidMove)code; + var pt = + Mode == Mode.Absolute + ? frameOrigin + line.EndPoint + : line.EndPoint + pos; + + if (pt.X > maxX) + maxX = pt.X; + else if (pt.X < minX) + minX = pt.X; + + if (pt.Y > maxY) + maxY = pt.Y; + else if (pt.Y < minY) + minY = pt.Y; + + pos = pt; + + break; + } case CodeType.ArcMove: + { + var arc = (ArcMove)code; + var radius = arc.CenterPoint.DistanceTo(arc.EndPoint); + + Vector endpt; + Vector centerpt; + + if (Mode == Mode.Incremental) { - var arc = (ArcMove)code; - var radius = arc.CenterPoint.DistanceTo(arc.EndPoint); - - Vector endpt; - Vector centerpt; - - if (Mode == Mode.Incremental) - { - endpt = arc.EndPoint + pos; - centerpt = arc.CenterPoint + pos; - } - else - { - endpt = frameOrigin + arc.EndPoint; - centerpt = frameOrigin + arc.CenterPoint; - } - - double minX1; - double minY1; - double maxX1; - double maxY1; - - if (pos.X < endpt.X) - { - minX1 = pos.X; - maxX1 = endpt.X; - } - else - { - minX1 = endpt.X; - maxX1 = pos.X; - } - - if (pos.Y < endpt.Y) - { - minY1 = pos.Y; - maxY1 = endpt.Y; - } - else - { - minY1 = endpt.Y; - maxY1 = pos.Y; - } - - var startAngle = pos.AngleFrom(centerpt); - var endAngle = endpt.AngleFrom(centerpt); - - // switch the angle to counter clockwise. - if (arc.Rotation == RotationType.CW) - Generic.Swap(ref startAngle, ref endAngle); - - startAngle = Angle.NormalizeRad(startAngle); - endAngle = Angle.NormalizeRad(endAngle); - - if (Angle.IsBetweenRad(Angle.HalfPI, startAngle, endAngle)) - maxY1 = centerpt.Y + radius; - - if (Angle.IsBetweenRad(System.Math.PI, startAngle, endAngle)) - minX1 = centerpt.X - radius; - - const double oneHalfPI = System.Math.PI * 1.5; - - if (Angle.IsBetweenRad(oneHalfPI, startAngle, endAngle)) - minY1 = centerpt.Y - radius; - - if (Angle.IsBetweenRad(Angle.TwoPI, startAngle, endAngle)) - maxX1 = centerpt.X + radius; - - if (maxX1 > maxX) - maxX = maxX1; - - if (minX1 < minX) - minX = minX1; - - if (maxY1 > maxY) - maxY = maxY1; - - if (minY1 < minY) - minY = minY1; - - pos = endpt; - - break; + endpt = arc.EndPoint + pos; + centerpt = arc.CenterPoint + pos; } + else + { + endpt = frameOrigin + arc.EndPoint; + centerpt = frameOrigin + arc.CenterPoint; + } + + double minX1; + double minY1; + double maxX1; + double maxY1; + + if (pos.X < endpt.X) + { + minX1 = pos.X; + maxX1 = endpt.X; + } + else + { + minX1 = endpt.X; + maxX1 = pos.X; + } + + if (pos.Y < endpt.Y) + { + minY1 = pos.Y; + maxY1 = endpt.Y; + } + else + { + minY1 = endpt.Y; + maxY1 = pos.Y; + } + + var startAngle = pos.AngleFrom(centerpt); + var endAngle = endpt.AngleFrom(centerpt); + + // switch the angle to counter clockwise. + if (arc.Rotation == RotationType.CW) + Generic.Swap(ref startAngle, ref endAngle); + + startAngle = Angle.NormalizeRad(startAngle); + endAngle = Angle.NormalizeRad(endAngle); + + if (Angle.IsBetweenRad(Angle.HalfPI, startAngle, endAngle)) + maxY1 = centerpt.Y + radius; + + if (Angle.IsBetweenRad(System.Math.PI, startAngle, endAngle)) + minX1 = centerpt.X - radius; + + const double oneHalfPI = System.Math.PI * 1.5; + + if (Angle.IsBetweenRad(oneHalfPI, startAngle, endAngle)) + minY1 = centerpt.Y - radius; + + if (Angle.IsBetweenRad(Angle.TwoPI, startAngle, endAngle)) + maxX1 = centerpt.X + radius; + + if (maxX1 > maxX) + maxX = maxX1; + + if (minX1 < minX) + minX = minX1; + + if (maxY1 > maxY) + maxY = maxY1; + + if (minY1 < minY) + minY = minY1; + + pos = endpt; + + break; + } case CodeType.SubProgramCall: - { - var subpgm = (SubProgramCall)code; - if (subpgm.Program == null) - break; - - // Sub-program frame origin in this program's frame - // is frameOrigin + Offset, regardless of current pos. - pos = frameOrigin + subpgm.Offset; - var box = subpgm.Program.BoundingBox(ref pos); - - if (box.Left < minX) - minX = box.Left; - - if (box.Right > maxX) - maxX = box.Right; - - if (box.Bottom < minY) - minY = box.Bottom; - - if (box.Top > maxY) - maxY = box.Top; - + { + var subpgm = (SubProgramCall)code; + if (subpgm.Program == null) break; - } + + // Sub-program frame origin in this program's frame + // is frameOrigin + Offset, regardless of current pos. + pos = frameOrigin + subpgm.Offset; + var box = subpgm.Program.BoundingBox(ref pos); + + if (box.Left < minX) + minX = box.Left; + + if (box.Right > maxX) + maxX = box.Right; + + if (box.Bottom < minY) + minY = box.Bottom; + + if (box.Top > maxY) + maxY = box.Top; + + break; + } } } @@ -479,11 +494,7 @@ namespace OpenNest.CNC public object Clone() { - var pgm = new Program() - { - mode = this.mode, - Rotation = this.Rotation - }; + var pgm = new Program() { mode = this.mode, Rotation = this.Rotation }; var codes = new ICode[Length]; diff --git a/OpenNest.Core/CNC/ProgramVariableManager.cs b/OpenNest.Core/CNC/ProgramVariableManager.cs index b3d57fb..e4af1e9 100644 --- a/OpenNest.Core/CNC/ProgramVariableManager.cs +++ b/OpenNest.Core/CNC/ProgramVariableManager.cs @@ -20,8 +20,8 @@ namespace OpenNest.CNC public List EmitDeclarations() { - return _variables.Values - .Where(v => v.Expression != null) + return _variables + .Values.Where(v => v.Expression != null) .OrderBy(v => v.Number) .Select(v => $"{v.Reference}={v.Expression} ({FormatComment(v.Name)})") .ToList(); diff --git a/OpenNest.Core/CNC/RapidEnumerator.cs b/OpenNest.Core/CNC/RapidEnumerator.cs index 365e45d..b080e5e 100644 --- a/OpenNest.Core/CNC/RapidEnumerator.cs +++ b/OpenNest.Core/CNC/RapidEnumerator.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.CNC { @@ -36,7 +36,13 @@ namespace OpenNest.CNC return basePos; } - private static void Walk(Program pgm, Vector basePos, ref Vector pos, bool skipFirst, List results) + private static void Walk( + Program pgm, + Vector basePos, + ref Vector pos, + bool skipFirst, + List results + ) { var skipped = !skipFirst; @@ -60,9 +66,10 @@ namespace OpenNest.CNC } else if (code is Motion motion) { - var endpt = pgm.Mode == Mode.Incremental - ? motion.EndPoint + pos - : motion.EndPoint + basePos; + var endpt = + pgm.Mode == Mode.Incremental + ? motion.EndPoint + pos + : motion.EndPoint + basePos; if (code.Type == CodeType.RapidMove) { diff --git a/OpenNest.Core/CNC/RapidMove.cs b/OpenNest.Core/CNC/RapidMove.cs index 0585630..c3f4ddd 100644 --- a/OpenNest.Core/CNC/RapidMove.cs +++ b/OpenNest.Core/CNC/RapidMove.cs @@ -30,7 +30,8 @@ namespace OpenNest.CNC return new RapidMove(EndPoint) { Suppressed = Suppressed, - VariableRefs = VariableRefs != null ? new Dictionary(VariableRefs) : null + VariableRefs = + VariableRefs != null ? new Dictionary(VariableRefs) : null, }; } diff --git a/OpenNest.Core/CNC/SubProgramCall.cs b/OpenNest.Core/CNC/SubProgramCall.cs index 647ac44..24c908c 100644 --- a/OpenNest.Core/CNC/SubProgramCall.cs +++ b/OpenNest.Core/CNC/SubProgramCall.cs @@ -9,9 +9,7 @@ namespace OpenNest.CNC private double rotation; private Program program; - public SubProgramCall() - { - } + public SubProgramCall() { } public SubProgramCall(Program program, double rotation) { diff --git a/OpenNest.Core/CNC/VariableDefinition.cs b/OpenNest.Core/CNC/VariableDefinition.cs index d74d87f..9e3e353 100644 --- a/OpenNest.Core/CNC/VariableDefinition.cs +++ b/OpenNest.Core/CNC/VariableDefinition.cs @@ -8,8 +8,13 @@ namespace OpenNest.CNC public bool Inline { get; } public bool Global { get; } - public VariableDefinition(string name, string expression, double value, - bool inline = false, bool global = false) + public VariableDefinition( + string name, + string expression, + double value, + bool inline = false, + bool global = false + ) { Name = name; Expression = expression; diff --git a/OpenNest.Core/CanonicalAngle.cs b/OpenNest.Core/CanonicalAngle.cs index 31d1314..27493b9 100644 --- a/OpenNest.Core/CanonicalAngle.cs +++ b/OpenNest.Core/CanonicalAngle.cs @@ -1,6 +1,6 @@ +using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; -using System.Linq; namespace OpenNest { @@ -44,7 +44,8 @@ namespace OpenNest if (drawing?.Program == null) return 0.0; - var entities = ConvertProgram.ToGeometry(drawing.Program) + var entities = ConvertProgram + .ToGeometry(drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); diff --git a/OpenNest.Core/Collections/DrawingCollection.cs b/OpenNest.Core/Collections/DrawingCollection.cs index 3b7969b..91c63b7 100644 --- a/OpenNest.Core/Collections/DrawingCollection.cs +++ b/OpenNest.Core/Collections/DrawingCollection.cs @@ -2,7 +2,5 @@ namespace OpenNest.Collections { - public class DrawingCollection : HashSet - { - } + public class DrawingCollection : HashSet { } } diff --git a/OpenNest.Core/Converters/ContourInfo.cs b/OpenNest.Core/Converters/ContourInfo.cs index 21ca8bb..ace08f7 100644 --- a/OpenNest.Core/Converters/ContourInfo.cs +++ b/OpenNest.Core/Converters/ContourInfo.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; namespace OpenNest.Converters { @@ -10,7 +10,7 @@ namespace OpenNest.Converters Perimeter, Hole, Etch, - Open + Open, } public sealed class ContourInfo @@ -91,7 +91,8 @@ namespace OpenNest.Converters // Non-perimeter shapes first (matches CNC cut order: holes before perimeter) for (var i = 0; i < shapes.Count; i++) { - if (i == perimeterIndex) continue; + if (i == perimeterIndex) + continue; var shape = shapes[i]; var type = ClassifyShape(shape); @@ -116,7 +117,13 @@ namespace OpenNest.Converters } // Perimeter last - result.Add(new ContourInfo(shapes[perimeterIndex], ContourClassification.Perimeter, "Perimeter")); + result.Add( + new ContourInfo( + shapes[perimeterIndex], + ContourClassification.Perimeter, + "Perimeter" + ) + ); return result; } @@ -124,8 +131,12 @@ namespace OpenNest.Converters private static ContourClassification ClassifyShape(Shape shape) { // Check etch layer — all entities must be on ETCH layer - if (shape.Entities.Count > 0 && - shape.Entities.All(e => string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase))) + if ( + shape.Entities.Count > 0 + && shape.Entities.All(e => + string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase) + ) + ) return ContourClassification.Etch; if (shape.IsClosed()) diff --git a/OpenNest.Core/Converters/ConvertGeometry.cs b/OpenNest.Core/Converters/ConvertGeometry.cs index 69643ea..bae7b16 100644 --- a/OpenNest.Core/Converters/ConvertGeometry.cs +++ b/OpenNest.Core/Converters/ConvertGeometry.cs @@ -1,7 +1,7 @@ -using OpenNest.CNC; +using System.Collections.Generic; +using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Converters { diff --git a/OpenNest.Core/Converters/ConvertProgram.cs b/OpenNest.Core/Converters/ConvertProgram.cs index 2fef701..76f805a 100644 --- a/OpenNest.Core/Converters/ConvertProgram.cs +++ b/OpenNest.Core/Converters/ConvertProgram.cs @@ -1,7 +1,7 @@ -using OpenNest.CNC; +using System.Collections.Generic; +using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Converters { @@ -18,7 +18,12 @@ namespace OpenNest.Converters return geometry; } - private static void AddProgram(Program program, ref Mode mode, ref Vector curpos, ref List geometry) + private static void AddProgram( + Program program, + ref Mode mode, + ref Vector curpos, + ref List geometry + ) { // Capture the frame origin at entry. Sub-program Offsets are relative // to this fixed origin, not to the current tool position. @@ -49,7 +54,10 @@ namespace OpenNest.Converters // The sub-program's frame origin in this program's frame is // frameOrigin + Offset — independent of current tool position. - curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y); + curpos = new Vector( + frameOrigin.X + subpgm.Offset.X, + frameOrigin.Y + subpgm.Offset.Y + ); AddProgram(subpgm.Program, ref mode, ref curpos, ref geometry); mode = savedMode; @@ -58,7 +66,12 @@ namespace OpenNest.Converters } } - private static void AddLinearMove(LinearMove linearMove, ref Mode mode, ref Vector curpos, ref List geometry) + private static void AddLinearMove( + LinearMove linearMove, + ref Mode mode, + ref Vector curpos, + ref List geometry + ) { var pt = linearMove.EndPoint; @@ -66,16 +79,17 @@ namespace OpenNest.Converters pt += curpos; var layer = ConvertLayer(linearMove.Layer); - var line = new Line(curpos, pt) - { - Layer = layer, - Color = layer.Color - }; + var line = new Line(curpos, pt) { Layer = layer, Color = layer.Color }; geometry.Add(line); curpos = pt; } - private static void AddRapidMove(RapidMove rapidMove, ref Mode mode, ref Vector curpos, ref List geometry) + private static void AddRapidMove( + RapidMove rapidMove, + ref Mode mode, + ref Vector curpos, + ref List geometry + ) { var pt = rapidMove.EndPoint; @@ -85,13 +99,18 @@ namespace OpenNest.Converters var line = new Line(curpos, pt) { Layer = SpecialLayers.Rapid, - Color = SpecialLayers.Rapid.Color + Color = SpecialLayers.Rapid.Color, }; geometry.Add(line); curpos = pt; } - private static void AddArcMove(ArcMove arcMove, ref Mode mode, ref Vector curpos, ref List geometry) + private static void AddArcMove( + ArcMove arcMove, + ref Mode mode, + ref Vector curpos, + ref List geometry + ) { var center = arcMove.CenterPoint; var endpt = arcMove.EndPoint; @@ -112,9 +131,28 @@ namespace OpenNest.Converters var layer = ConvertLayer(arcMove.Layer); if (startAngle.IsEqualTo(endAngle)) - geometry.Add(new Circle(center, radius) { Layer = layer, Color = layer.Color, Rotation = arcMove.Rotation }); + geometry.Add( + new Circle(center, radius) + { + Layer = layer, + Color = layer.Color, + Rotation = arcMove.Rotation, + } + ); else - geometry.Add(new Arc(center, radius, startAngle, endAngle, arcMove.Rotation == RotationType.CW) { Layer = layer, Color = layer.Color }); + geometry.Add( + new Arc( + center, + radius, + startAngle, + endAngle, + arcMove.Rotation == RotationType.CW + ) + { + Layer = layer, + Color = layer.Color, + } + ); curpos = endpt; } diff --git a/OpenNest.Core/CutOff.cs b/OpenNest.Core/CutOff.cs index 571feab..e7f513f 100644 --- a/OpenNest.Core/CutOff.cs +++ b/OpenNest.Core/CutOff.cs @@ -1,14 +1,14 @@ -using OpenNest.CNC; -using OpenNest.Geometry; using System.Collections.Generic; using System.Linq; +using OpenNest.CNC; +using OpenNest.Geometry; namespace OpenNest { public enum CutOffAxis { Horizontal, - Vertical + Vertical, } public class CutOff @@ -26,7 +26,11 @@ namespace OpenNest Drawing = new Drawing(GetName()) { IsCutOff = true }; } - public void Regenerate(Plate plate, CutOffSettings settings, Dictionary cache = null) + public void Regenerate( + Plate plate, + CutOffSettings settings, + Dictionary cache = null + ) { var segments = ComputeSegments(plate, settings, cache); var program = BuildProgram(segments, settings); @@ -40,11 +44,17 @@ namespace OpenNest return $"CutOff-{axisChar}-{coord:F2}"; } - private List<(double Start, double End)> ComputeSegments(Plate plate, CutOffSettings settings, Dictionary cache) + private List<(double Start, double End)> ComputeSegments( + Plate plate, + CutOffSettings settings, + Dictionary cache + ) { var bounds = plate.BoundingBox(includeParts: false); - double lineStart, lineEnd, cutPosition; + double lineStart, + lineEnd, + cutPosition; if (Axis == CutOffAxis.Vertical) { @@ -68,7 +78,14 @@ namespace OpenNest Entity perimeter = null; cache?.TryGetValue(part, out perimeter); - var partExclusions = GetPartExclusions(part, perimeter, cutPosition, lineStart, lineEnd, settings.PartClearance); + var partExclusions = GetPartExclusions( + part, + perimeter, + cutPosition, + lineStart, + lineEnd, + settings.PartClearance + ); exclusions.AddRange(partExclusions); } @@ -107,7 +124,13 @@ namespace OpenNest private static readonly List<(double Start, double End)> EmptyExclusions = new(); private List<(double Start, double End)> GetPartExclusions( - Part part, Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance) + Part part, + Entity perimeter, + double cutPosition, + double lineStart, + double lineEnd, + double clearance + ) { var bb = part.BoundingBox; var (partMin, partMax) = AxisBounds(bb, clearance); @@ -118,7 +141,13 @@ namespace OpenNest if (perimeter != null) { - var perimeterExclusions = IntersectPerimeter(perimeter, cutPosition, lineStart, lineEnd, clearance); + var perimeterExclusions = IntersectPerimeter( + perimeter, + cutPosition, + lineStart, + lineEnd, + clearance + ); if (perimeterExclusions != null) return perimeterExclusions; } @@ -127,17 +156,24 @@ namespace OpenNest } private List<(double Start, double End)> IntersectPerimeter( - Entity perimeter, double cutPosition, double lineStart, double lineEnd, double clearance) + Entity perimeter, + double cutPosition, + double lineStart, + double lineEnd, + double clearance + ) { var target = OffsetOutward(perimeter, clearance) ?? perimeter; var usedOffset = target != perimeter; - var cutLine = new Line(MakePoint(cutPosition, lineStart), MakePoint(cutPosition, lineEnd)); + var cutLine = new Line( + MakePoint(cutPosition, lineStart), + MakePoint(cutPosition, lineEnd) + ); if (!target.Intersects(cutLine, out var pts) || pts.Count < 2) return null; - var coords = pts - .Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X) + var coords = pts.Select(pt => Axis == CutOffAxis.Vertical ? pt.Y : pt.X) .OrderBy(c => c) .ToList(); @@ -184,7 +220,10 @@ namespace OpenNest ? (bb.Y - clearance, bb.Y + bb.Width + clearance) : (bb.X - clearance, bb.X + bb.Length + clearance); - private Program BuildProgram(List<(double Start, double End)> segments, CutOffSettings settings) + private Program BuildProgram( + List<(double Start, double End)> segments, + CutOffSettings settings + ) { var program = new Program(); diff --git a/OpenNest.Core/CutOffSettings.cs b/OpenNest.Core/CutOffSettings.cs index d514c00..ef7da3a 100644 --- a/OpenNest.Core/CutOffSettings.cs +++ b/OpenNest.Core/CutOffSettings.cs @@ -3,7 +3,7 @@ namespace OpenNest public enum CutDirection { TowardOrigin, - AwayFromOrigin + AwayFromOrigin, } public class CutOffSettings diff --git a/OpenNest.Core/CutParameters.cs b/OpenNest.Core/CutParameters.cs index bf751f6..a3c5ff4 100644 --- a/OpenNest.Core/CutParameters.cs +++ b/OpenNest.Core/CutParameters.cs @@ -11,11 +11,12 @@ public class CutParameters public string PostProcessor { get; set; } public Units Units { get; set; } - public static CutParameters Default => new() - { - Feedrate = 100, - RapidTravelRate = 300, - PierceTime = TimeSpan.FromSeconds(0.5), - Units = OpenNest.Units.Inches - }; + public static CutParameters Default => + new() + { + Feedrate = 100, + RapidTravelRate = 300, + PierceTime = TimeSpan.FromSeconds(0.5), + Units = OpenNest.Units.Inches, + }; } diff --git a/OpenNest.Core/Drawing.cs b/OpenNest.Core/Drawing.cs index 54da9f7..54c8b55 100644 --- a/OpenNest.Core/Drawing.cs +++ b/OpenNest.Core/Drawing.cs @@ -1,12 +1,12 @@ -using OpenNest.Bending; -using OpenNest.CNC; -using OpenNest.Converters; -using OpenNest.Geometry; -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Threading; +using OpenNest.Bending; +using OpenNest.CNC; +using OpenNest.Converters; +using OpenNest.Geometry; namespace OpenNest { @@ -18,18 +18,18 @@ namespace OpenNest public static Color[] PartColors = new Color[] { - Color.FromArgb(205, 92, 92), // Indian Red - Color.FromArgb(148, 103, 189), // Medium Purple - Color.FromArgb(75, 180, 175), // Teal - Color.FromArgb(210, 190, 75), // Goldenrod - Color.FromArgb(190, 85, 175), // Orchid - Color.FromArgb(185, 115, 85), // Sienna - Color.FromArgb(120, 100, 190), // Slate Blue - Color.FromArgb(200, 100, 140), // Rose - Color.FromArgb(80, 175, 155), // Sea Green - Color.FromArgb(195, 160, 85), // Dark Khaki - Color.FromArgb(175, 95, 160), // Plum - Color.FromArgb(215, 130, 130), // Light Coral + Color.FromArgb(205, 92, 92), // Indian Red + Color.FromArgb(148, 103, 189), // Medium Purple + Color.FromArgb(75, 180, 175), // Teal + Color.FromArgb(210, 190, 75), // Goldenrod + Color.FromArgb(190, 85, 175), // Orchid + Color.FromArgb(185, 115, 85), // Sienna + Color.FromArgb(120, 100, 190), // Slate Blue + Color.FromArgb(200, 100, 140), // Rose + Color.FromArgb(80, 175, 155), // Sea Green + Color.FromArgb(195, 160, 85), // Dark Khaki + Color.FromArgb(175, 95, 160), // Plum + Color.FromArgb(215, 130, 130), // Light Coral }; public static Color GetNextColor() @@ -40,14 +40,10 @@ namespace OpenNest } public Drawing() - : this(string.Empty, new Program()) - { - } + : this(string.Empty, new Program()) { } public Drawing(string name) - : this(name, new Program()) - { - } + : this(name, new Program()) { } public Drawing(string name, Program pgm) { @@ -127,7 +123,9 @@ namespace OpenNest public void UpdateArea() { - var geometry = ConvertProgram.ToGeometry(Program).Where(entity => entity.Layer != SpecialLayers.Rapid); + var geometry = ConvertProgram + .ToGeometry(Program) + .Where(entity => entity.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(geometry); if (shapes.Count == 0) diff --git a/OpenNest.Core/Geometry/Arc.cs b/OpenNest.Core/Geometry/Arc.cs index e556ffe..b361763 100644 --- a/OpenNest.Core/Geometry/Arc.cs +++ b/OpenNest.Core/Geometry/Arc.cs @@ -1,6 +1,6 @@ -using OpenNest.Math; -using System; +using System; using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -12,16 +12,18 @@ namespace OpenNest.Geometry private Vector center; private bool reversed; - public Arc() - { - } + public Arc() { } public Arc(double x, double y, double r, double a1, double a2, bool reversed = false) - : this(new Vector(x, y), r, a1, a2, reversed) - { - } + : this(new Vector(x, y), r, a1, a2, reversed) { } - public Arc(Vector center, double radius, double startAngle, double endAngle, bool reversed = false) + public Arc( + Vector center, + double radius, + double startAngle, + double endAngle, + bool reversed = false + ) { this.center = center; this.radius = radius; @@ -93,8 +95,7 @@ namespace OpenNest.Geometry } } - public bool IsFullCircle() => - SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon; + public bool IsFullCircle() => SweepAngle() >= Angle.TwoPI - Tolerance.Epsilon; /// /// Angle in radians between start and end angles. @@ -130,10 +131,7 @@ namespace OpenNest.Geometry public RotationType Rotation { get { return IsReversed ? RotationType.CW : RotationType.CCW; } - set - { - IsReversed = (value == RotationType.CW); - } + set { IsReversed = (value == RotationType.CW); } } /// @@ -144,7 +142,8 @@ namespace OpenNest.Geometry { return new Vector( Center.X + Radius * System.Math.Cos(StartAngle), - Center.Y + Radius * System.Math.Sin(StartAngle)); + Center.Y + Radius * System.Math.Sin(StartAngle) + ); } /// @@ -155,7 +154,8 @@ namespace OpenNest.Geometry { return new Vector( Center.X + Radius * System.Math.Cos(EndAngle), - Center.Y + Radius * System.Math.Sin(EndAngle)); + Center.Y + Radius * System.Math.Sin(EndAngle) + ); } /// @@ -166,7 +166,8 @@ namespace OpenNest.Geometry var midAngle = StartAngle + (IsReversed ? -SweepAngle() / 2 : SweepAngle() / 2); return new Vector( Center.X + Radius * System.Math.Cos(midAngle), - Center.Y + Radius * System.Math.Sin(midAngle)); + Center.Y + Radius * System.Math.Sin(midAngle) + ); } /// @@ -231,7 +232,10 @@ namespace OpenNest.Geometry return 1; var maxAngle = 2.0 * System.Math.Acos(1.0 - tolerance / Radius); - return System.Math.Max(1, (int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle)); + return System.Math.Max( + 1, + (int)System.Math.Ceiling(System.Math.Abs(SweepAngle()) / maxAngle) + ); } /// @@ -242,21 +246,23 @@ namespace OpenNest.Geometry public List ToPoints(int segments = 1000, bool circumscribe = false) { var points = new List(); - var stepAngle = reversed - ? -SweepAngle() / segments - : SweepAngle() / segments; + var stepAngle = reversed ? -SweepAngle() / segments : SweepAngle() / segments; - var r = circumscribe && segments > 0 - ? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0) - : Radius; + var r = + circumscribe && segments > 0 + ? Radius / System.Math.Cos(System.Math.Abs(stepAngle) / 2.0) + : Radius; for (int i = 0; i <= segments; ++i) { var angle = stepAngle * i + StartAngle; - points.Add(new Vector( - System.Math.Cos(angle) * r + Center.X, - System.Math.Sin(angle) * r + Center.Y)); + points.Add( + new Vector( + System.Math.Cos(angle) * r + Center.X, + System.Math.Sin(angle) * r + Center.Y + ) + ); } return points; @@ -470,7 +476,8 @@ namespace OpenNest.Geometry { return new Vector( System.Math.Cos(angle) * Radius + Center.X, - System.Math.Sin(angle) * Radius + Center.Y); + System.Math.Sin(angle) * Radius + Center.Y + ); } else { @@ -500,7 +507,8 @@ namespace OpenNest.Geometry /// public override bool Intersects(Arc arc, out List pts) { - return Intersect.Intersects(this, arc, out pts); ; + return Intersect.Intersects(this, arc, out pts); + ; } /// diff --git a/OpenNest.Core/Geometry/BoundingBox.cs b/OpenNest.Core/Geometry/BoundingBox.cs index c8e9d1c..aed3fa4 100644 --- a/OpenNest.Core/Geometry/BoundingBox.cs +++ b/OpenNest.Core/Geometry/BoundingBox.cs @@ -17,10 +17,14 @@ namespace OpenNest.Geometry foreach (var box in boxes) { - if (box.Left < minX) minX = box.Left; - if (box.Right > maxX) maxX = box.Right; - if (box.Bottom < minY) minY = box.Bottom; - if (box.Top > maxY) maxY = box.Top; + if (box.Left < minX) + minX = box.Left; + if (box.Right > maxX) + maxX = box.Right; + if (box.Bottom < minY) + minY = box.Bottom; + if (box.Top > maxY) + maxY = box.Top; } return new Box(minX, minY, maxX - minX, maxY - minY); @@ -41,11 +45,15 @@ namespace OpenNest.Geometry { var vertex = pts[i]; - if (vertex.X < minX) minX = vertex.X; - else if (vertex.X > maxX) maxX = vertex.X; + if (vertex.X < minX) + minX = vertex.X; + else if (vertex.X > maxX) + maxX = vertex.X; - if (vertex.Y < minY) minY = vertex.Y; - else if (vertex.Y > maxY) maxY = vertex.Y; + if (vertex.Y < minY) + minY = vertex.Y; + else if (vertex.Y > maxY) + maxY = vertex.Y; } return new Box(minX, minY, maxX - minX, maxY - minY); @@ -65,10 +73,14 @@ namespace OpenNest.Geometry foreach (var box in items) { - if (box.Left < left) left = box.Left; - if (box.Right > right) right = box.Right; - if (box.Bottom < bottom) bottom = box.Bottom; - if (box.Top > top) top = box.Top; + if (box.Left < left) + left = box.Left; + if (box.Right > right) + right = box.Right; + if (box.Bottom < bottom) + bottom = box.Bottom; + if (box.Top > top) + top = box.Top; } return new Box(left, bottom, right - left, top - bottom); diff --git a/OpenNest.Core/Geometry/Box.cs b/OpenNest.Core/Geometry/Box.cs index 5d3c3bf..a7642ce 100644 --- a/OpenNest.Core/Geometry/Box.cs +++ b/OpenNest.Core/Geometry/Box.cs @@ -8,9 +8,7 @@ namespace OpenNest.Geometry public static readonly Box Empty = new Box(); public Box() - : this(0, 0, 0, 0) - { - } + : this(0, 0, 0, 0) { } public Box(double x, double y, double w, double h) { @@ -117,10 +115,14 @@ namespace OpenNest.Geometry public bool Intersects(Box box) { - if (Left >= box.Right) return false; - if (Right <= box.Left) return false; - if (Top <= box.Bottom) return false; - if (Bottom >= box.Top) return false; + if (Left >= box.Right) + return false; + if (Right <= box.Left) + return false; + if (Top <= box.Bottom) + return false; + if (Bottom >= box.Top) + return false; return true; } @@ -146,18 +148,24 @@ namespace OpenNest.Geometry public bool Contains(Box box) { - if (box.Top > Top) return false; - if (box.Left < Left) return false; - if (box.Right > Right) return false; - if (box.Bottom < Bottom) return false; + if (box.Top > Top) + return false; + if (box.Left < Left) + return false; + if (box.Right > Right) + return false; + if (box.Bottom < Bottom) + return false; return true; } public bool Contains(Vector pt) { - return pt.X >= Left - Tolerance.Epsilon && pt.X <= Right + Tolerance.Epsilon - && pt.Y >= Bottom - Tolerance.Epsilon && pt.Y <= Top + Tolerance.Epsilon; + return pt.X >= Left - Tolerance.Epsilon + && pt.X <= Right + Tolerance.Epsilon + && pt.Y >= Bottom - Tolerance.Epsilon + && pt.Y <= Top + Tolerance.Epsilon; } public bool IsHorizontalTo(Box box) diff --git a/OpenNest.Core/Geometry/Circle.cs b/OpenNest.Core/Geometry/Circle.cs index ac25197..e89a62b 100644 --- a/OpenNest.Core/Geometry/Circle.cs +++ b/OpenNest.Core/Geometry/Circle.cs @@ -1,5 +1,5 @@ -using OpenNest.Math; -using System.Collections.Generic; +using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -8,14 +8,10 @@ namespace OpenNest.Geometry private Vector center; private double radius; - public Circle() - { - } + public Circle() { } public Circle(double x, double y, double radius) - : this(new Vector(x, y), radius) - { - } + : this(new Vector(x, y), radius) { } public Circle(Vector center, double radius) { @@ -137,21 +133,22 @@ namespace OpenNest.Geometry public List ToPoints(int segments = 1000, bool circumscribe = false) { var points = new List(); - var stepAngle = Rotation == RotationType.CW - ? -Angle.TwoPI / segments - : Angle.TwoPI / segments; + var stepAngle = + Rotation == RotationType.CW ? -Angle.TwoPI / segments : Angle.TwoPI / segments; - var r = circumscribe && segments > 0 - ? Radius / System.Math.Cos(stepAngle / 2.0) - : Radius; + var r = + circumscribe && segments > 0 ? Radius / System.Math.Cos(stepAngle / 2.0) : Radius; for (int i = 0; i <= segments; ++i) { var angle = stepAngle * i; - points.Add(new Vector( - System.Math.Cos(angle) * r + Center.X, - System.Math.Sin(angle) * r + Center.Y)); + points.Add( + new Vector( + System.Math.Cos(angle) * r + Center.X, + System.Math.Sin(angle) * r + Center.Y + ) + ); } return points; @@ -278,11 +275,9 @@ namespace OpenNest.Geometry { if (side == OffsetSide.Left && Rotation == RotationType.CCW) { - return Radius <= distance ? null : new Circle(center, Radius - distance) - { - Layer = Layer, - Rotation = Rotation - }; + return Radius <= distance + ? null + : new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation }; } else { @@ -294,11 +289,9 @@ namespace OpenNest.Geometry { if (ContainsPoint(pt)) { - return Radius <= distance ? null : new Circle(center, Radius - distance) - { - Layer = Layer, - Rotation = Rotation - }; + return Radius <= distance + ? null + : new Circle(center, Radius - distance) { Layer = Layer, Rotation = Rotation }; } else { @@ -317,7 +310,8 @@ namespace OpenNest.Geometry return new Vector( System.Math.Cos(angle) * Radius + Center.X, - System.Math.Sin(angle) * Radius + Center.Y); + System.Math.Sin(angle) * Radius + Center.Y + ); } /// @@ -350,7 +344,9 @@ namespace OpenNest.Geometry public override bool Intersects(Circle circle) { var dist = Center.DistanceTo(circle.Center); - return (dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius)); + return ( + dist < (Radius + circle.Radius) && dist > System.Math.Abs(Radius - circle.Radius) + ); } /// diff --git a/OpenNest.Core/Geometry/Collision.cs b/OpenNest.Core/Geometry/Collision.cs index d48ecf5..331f04d 100644 --- a/OpenNest.Core/Geometry/Collision.cs +++ b/OpenNest.Core/Geometry/Collision.cs @@ -1,12 +1,16 @@ -using OpenNest.Math; using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Geometry { public static class Collision { - public static CollisionResult Check(Polygon a, Polygon b, - List holesA = null, List holesB = null) + public static CollisionResult Check( + Polygon a, + Polygon b, + List holesA = null, + List holesB = null + ) { // Step 1: Bounding box pre-filter if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox)) @@ -46,8 +50,12 @@ namespace OpenNest.Geometry return new CollisionResult(true, regions, intersectionPoints); } - public static bool HasOverlap(Polygon a, Polygon b, - List holesA = null, List holesB = null) + public static bool HasOverlap( + Polygon a, + Polygon b, + List holesA = null, + List holesB = null + ) { if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox)) return false; @@ -57,8 +65,10 @@ namespace OpenNest.Geometry return Check(a, b, holesA, holesB).Overlaps; } - public static List CheckAll(List polygons, - List> holes = null) + public static List CheckAll( + List polygons, + List> holes = null + ) { var results = new List(); @@ -78,8 +88,7 @@ namespace OpenNest.Geometry return results; } - public static bool HasAnyOverlap(List polygons, - List> holes = null) + public static bool HasAnyOverlap(List polygons, List> holes = null) { for (var i = 0; i < polygons.Count; i++) { @@ -98,10 +107,8 @@ namespace OpenNest.Geometry private static bool BoundingBoxesOverlap(Box a, Box b) { - var overlapX = System.Math.Min(a.Right, b.Right) - - System.Math.Max(a.Left, b.Left); - var overlapY = System.Math.Min(a.Top, b.Top) - - System.Math.Max(a.Bottom, b.Bottom); + var overlapX = System.Math.Min(a.Right, b.Right) - System.Math.Max(a.Left, b.Left); + var overlapY = System.Math.Min(a.Top, b.Top) - System.Math.Max(a.Bottom, b.Bottom); return overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon; } @@ -164,13 +171,19 @@ namespace OpenNest.Geometry var output = new List(subject.Vertices); // Remove closing vertex if present - if (output.Count > 1 && output[0].X == output[output.Count - 1].X - && output[0].Y == output[output.Count - 1].Y) + if ( + output.Count > 1 + && output[0].X == output[output.Count - 1].X + && output[0].Y == output[output.Count - 1].Y + ) output.RemoveAt(output.Count - 1); var clipVerts = new List(clip.Vertices); - if (clipVerts.Count > 1 && clipVerts[0].X == clipVerts[clipVerts.Count - 1].X - && clipVerts[0].Y == clipVerts[clipVerts.Count - 1].Y) + if ( + clipVerts.Count > 1 + && clipVerts[0].X == clipVerts[clipVerts.Count - 1].X + && clipVerts[0].Y == clipVerts[clipVerts.Count - 1].Y + ) clipVerts.RemoveAt(clipVerts.Count - 1); for (var i = 0; i < clipVerts.Count; i++) @@ -231,7 +244,7 @@ namespace OpenNest.Geometry private static double Cross(Vector edgeStart, Vector edgeEnd, Vector point) { return (edgeEnd.X - edgeStart.X) * (point.Y - edgeStart.Y) - - (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X); + - (edgeEnd.Y - edgeStart.Y) * (point.X - edgeStart.X); } /// @@ -255,12 +268,17 @@ namespace OpenNest.Geometry /// /// Subtracts holes from overlap regions. /// - private static List SubtractHoles(List regions, - List holesA, List holesB) + private static List SubtractHoles( + List regions, + List holesA, + List holesB + ) { var allHoles = new List(); - if (holesA != null) allHoles.AddRange(holesA); - if (holesB != null) allHoles.AddRange(holesB); + if (holesA != null) + allHoles.AddRange(holesA); + if (holesB != null) + allHoles.AddRange(holesB); if (allHoles.Count == 0) return regions; @@ -313,9 +331,16 @@ namespace OpenNest.Geometry var holeCount = holeTri.IsClosed() ? holeVerts.Count - 1 : holeVerts.Count; var survived = false; for (var i = 0; i < holeCount; i++) - survived |= AddIfPositiveArea(next, - ClipOutsideHalfSpace(pieceTri, holeVerts[i], holeVerts[(i + 1) % holeCount])); - if (!survived) continue; // piece lies entirely within the hole + survived |= AddIfPositiveArea( + next, + ClipOutsideHalfSpace( + pieceTri, + holeVerts[i], + holeVerts[(i + 1) % holeCount] + ) + ); + if (!survived) + continue; // piece lies entirely within the hole } } @@ -329,7 +354,11 @@ namespace OpenNest.Geometry /// Sutherland-Hodgman clip of a convex polygon to the strict outside of the /// infinite line edgeStart->edgeEnd of a CCW hole edge (Cross < -Epsilon). /// - private static List ClipOutsideHalfSpace(Polygon piece, Vector edgeStart, Vector edgeEnd) + private static List ClipOutsideHalfSpace( + Polygon piece, + Vector edgeStart, + Vector edgeEnd + ) { var verts = piece.Vertices; var count = piece.IsClosed() ? verts.Count - 1 : verts.Count; @@ -340,22 +369,27 @@ namespace OpenNest.Geometry var next = verts[(i + 1) % count]; var currentInside = Cross(edgeStart, edgeEnd, current) >= -Tolerance.Epsilon; var nextInside = Cross(edgeStart, edgeEnd, next) >= -Tolerance.Epsilon; - if (!currentInside) kept.Add(current); - if (currentInside == nextInside) continue; + if (!currentInside) + kept.Add(current); + if (currentInside == nextInside) + continue; var intersection = LineIntersection(edgeStart, edgeEnd, current, next); - if (intersection.IsValid()) kept.Add(intersection); + if (intersection.IsValid()) + kept.Add(intersection); } return kept; } private static bool AddIfPositiveArea(List polygons, List vertices) { - if (vertices.Count < 3) return false; + if (vertices.Count < 3) + return false; var polygon = new Polygon(); polygon.Vertices.AddRange(vertices); polygon.Close(); polygon.UpdateBounds(); - if (polygon.Area() <= Tolerance.Epsilon) return false; + if (polygon.Area() <= Tolerance.Epsilon) + return false; polygons.Add(polygon); return true; } diff --git a/OpenNest.Core/Geometry/CollisionResult.cs b/OpenNest.Core/Geometry/CollisionResult.cs index 927e7c1..6d561cc 100644 --- a/OpenNest.Core/Geometry/CollisionResult.cs +++ b/OpenNest.Core/Geometry/CollisionResult.cs @@ -5,9 +5,17 @@ namespace OpenNest.Geometry { public class CollisionResult { - public static readonly CollisionResult None = new(false, new List(), new List()); + public static readonly CollisionResult None = new( + false, + new List(), + new List() + ); - public CollisionResult(bool overlaps, List overlapRegions, List intersectionPoints) + public CollisionResult( + bool overlaps, + List overlapRegions, + List intersectionPoints + ) { Overlaps = overlaps; OverlapRegions = overlapRegions; diff --git a/OpenNest.Core/Geometry/ConvexDecomposition.cs b/OpenNest.Core/Geometry/ConvexDecomposition.cs index 13026ba..f2e51a6 100644 --- a/OpenNest.Core/Geometry/ConvexDecomposition.cs +++ b/OpenNest.Core/Geometry/ConvexDecomposition.cs @@ -19,8 +19,11 @@ namespace OpenNest.Geometry var verts = new List(polygon.Vertices); // Remove closing vertex if polygon is closed. - if (verts.Count > 1 && verts[0].X == verts[verts.Count - 1].X - && verts[0].Y == verts[verts.Count - 1].Y) + if ( + verts.Count > 1 + && verts[0].X == verts[verts.Count - 1].X + && verts[0].Y == verts[verts.Count - 1].Y + ) verts.RemoveAt(verts.Count - 1); if (verts.Count < 3) @@ -84,8 +87,14 @@ namespace OpenNest.Geometry /// Tests whether the vertex at curr forms an ear (a convex vertex whose /// triangle contains no other polygon vertices). /// - private static bool IsEar(Vector prev, Vector curr, Vector next, - List verts, List indices, int n) + private static bool IsEar( + Vector prev, + Vector curr, + Vector next, + List verts, + List indices, + int n + ) { // Must be convex (CCW turn). if (Cross(prev, curr, next) <= 0) diff --git a/OpenNest.Core/Geometry/ConvexHull.cs b/OpenNest.Core/Geometry/ConvexHull.cs index 062b20b..d9eeed7 100644 --- a/OpenNest.Core/Geometry/ConvexHull.cs +++ b/OpenNest.Core/Geometry/ConvexHull.cs @@ -20,7 +20,10 @@ namespace OpenNest.Geometry foreach (var p in sorted) { - while (lower.Count >= 2 && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0) + while ( + lower.Count >= 2 + && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0 + ) lower.RemoveAt(lower.Count - 1); lower.Add(p); @@ -32,7 +35,10 @@ namespace OpenNest.Geometry { var p = sorted[i]; - while (upper.Count >= 2 && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0) + while ( + upper.Count >= 2 + && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0 + ) upper.RemoveAt(upper.Count - 1); upper.Add(p); diff --git a/OpenNest.Core/Geometry/EllipseConverter.cs b/OpenNest.Core/Geometry/EllipseConverter.cs index 005d0fd..0163fbb 100644 --- a/OpenNest.Core/Geometry/EllipseConverter.cs +++ b/OpenNest.Core/Geometry/EllipseConverter.cs @@ -1,6 +1,6 @@ -using OpenNest.Math; using System; using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -9,7 +9,13 @@ namespace OpenNest.Geometry private const int MaxSubdivisionDepth = 12; private const int DeviationSamples = 20; - internal static Vector EvaluatePoint(double semiMajor, double semiMinor, double rotation, Vector center, double t) + internal static Vector EvaluatePoint( + double semiMajor, + double semiMinor, + double rotation, + Vector center, + double t + ) { var x = semiMajor * System.Math.Cos(t); var y = semiMinor * System.Math.Sin(t); @@ -17,12 +23,15 @@ namespace OpenNest.Geometry var cos = System.Math.Cos(rotation); var sin = System.Math.Sin(rotation); - return new Vector( - center.X + x * cos - y * sin, - center.Y + x * sin + y * cos); + return new Vector(center.X + x * cos - y * sin, center.Y + x * sin + y * cos); } - internal static Vector EvaluateTangent(double semiMajor, double semiMinor, double rotation, double t) + internal static Vector EvaluateTangent( + double semiMajor, + double semiMinor, + double rotation, + double t + ) { var tx = -semiMajor * System.Math.Sin(t); var ty = semiMinor * System.Math.Cos(t); @@ -30,12 +39,15 @@ namespace OpenNest.Geometry var cos = System.Math.Cos(rotation); var sin = System.Math.Sin(rotation); - return new Vector( - tx * cos - ty * sin, - tx * sin + ty * cos); + return new Vector(tx * cos - ty * sin, tx * sin + ty * cos); } - internal static Vector EvaluateNormal(double semiMajor, double semiMinor, double rotation, double t) + internal static Vector EvaluateNormal( + double semiMajor, + double semiMinor, + double rotation, + double t + ) { // Inward normal: perpendicular to tangent, pointing toward center of curvature. // In local coords: N(t) = (-b*cos(t), -a*sin(t)) @@ -45,9 +57,7 @@ namespace OpenNest.Geometry var cos = System.Math.Cos(rotation); var sin = System.Math.Sin(rotation); - return new Vector( - nx * cos - ny * sin, - nx * sin + ny * cos); + return new Vector(nx * cos - ny * sin, nx * sin + ny * cos); } internal static Vector IntersectNormals(Vector p1, Vector n1, Vector p2, Vector n2) @@ -83,11 +93,21 @@ namespace OpenNest.Geometry return new Vector(ux + c.X, uy + c.Y); } - public static List Convert(Vector center, double semiMajor, double semiMinor, - double rotation, double startParam, double endParam, double tolerance = 0.001) + public static List Convert( + Vector center, + double semiMajor, + double semiMinor, + double rotation, + double startParam, + double endParam, + double tolerance = 0.001 + ) { if (tolerance <= 0) - throw new ArgumentOutOfRangeException(nameof(tolerance), "Tolerance must be positive."); + throw new ArgumentOutOfRangeException( + nameof(tolerance), + "Tolerance must be positive." + ); if (semiMajor <= 0 || semiMinor <= 0) throw new ArgumentOutOfRangeException("Semi-axis lengths must be positive."); @@ -102,14 +122,28 @@ namespace OpenNest.Geometry var entities = new List(); for (var i = 0; i < splits.Count - 1; i++) - FitSegment(center, semiMajor, semiMinor, rotation, - splits[i], splits[i + 1], tolerance, entities, 0); + FitSegment( + center, + semiMajor, + semiMinor, + rotation, + splits[i], + splits[i + 1], + tolerance, + entities, + 0 + ); return entities; } - private static List ConvertCircle(Vector center, double radius, - double rotation, double startParam, double endParam) + private static List ConvertCircle( + Vector center, + double radius, + double rotation, + double startParam, + double endParam + ) { var sweep = endParam - startParam; var isFull = System.Math.Abs(sweep - Angle.TwoPI) < 0.01; @@ -123,7 +157,7 @@ namespace OpenNest.Geometry return new List { new Arc(center, radius, startAngle1, midAngle, false), - new Arc(center, radius, midAngle, endAngle2, false) + new Arc(center, radius, midAngle, endAngle2, false), }; } @@ -136,7 +170,8 @@ namespace OpenNest.Geometry { var splits = new List { startParam }; - var firstQuadrant = System.Math.Ceiling(startParam / (System.Math.PI / 2)) * (System.Math.PI / 2); + var firstQuadrant = + System.Math.Ceiling(startParam / (System.Math.PI / 2)) * (System.Math.PI / 2); for (var q = firstQuadrant; q < endParam; q += System.Math.PI / 2) { if (q > startParam + 1e-10 && q < endParam - 1e-10) @@ -147,8 +182,17 @@ namespace OpenNest.Geometry return splits; } - private static void FitSegment(Vector center, double semiMajor, double semiMinor, - double rotation, double t0, double t1, double tolerance, List results, int depth) + private static void FitSegment( + Vector center, + double semiMajor, + double semiMinor, + double rotation, + double t0, + double t1, + double tolerance, + List results, + int depth + ) { var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t0); var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, center, t1); @@ -168,12 +212,29 @@ namespace OpenNest.Geometry } var radius = p0.DistanceTo(arcCenter); - var maxDev = MeasureDeviation(center, semiMajor, semiMinor, rotation, - t0, t1, arcCenter, radius); + var maxDev = MeasureDeviation( + center, + semiMajor, + semiMinor, + rotation, + t0, + t1, + arcCenter, + radius + ); if (maxDev <= tolerance) { - var arc = CreateArc(arcCenter, radius, center, semiMajor, semiMinor, rotation, t0, t1); + var arc = CreateArc( + arcCenter, + radius, + center, + semiMajor, + semiMinor, + rotation, + t0, + t1 + ); if (arc.SweepAngle() < Tolerance.Epsilon) results.Add(new Line(p0, p1)); else @@ -182,13 +243,41 @@ namespace OpenNest.Geometry else { var tMid = (t0 + t1) / 2.0; - FitSegment(center, semiMajor, semiMinor, rotation, t0, tMid, tolerance, results, depth + 1); - FitSegment(center, semiMajor, semiMinor, rotation, tMid, t1, tolerance, results, depth + 1); + FitSegment( + center, + semiMajor, + semiMinor, + rotation, + t0, + tMid, + tolerance, + results, + depth + 1 + ); + FitSegment( + center, + semiMajor, + semiMinor, + rotation, + tMid, + t1, + tolerance, + results, + depth + 1 + ); } } - private static double MeasureDeviation(Vector center, double semiMajor, double semiMinor, - double rotation, double t0, double t1, Vector arcCenter, double radius) + private static double MeasureDeviation( + Vector center, + double semiMajor, + double semiMinor, + double rotation, + double t0, + double t1, + Vector arcCenter, + double radius + ) { var maxDev = 0.0; for (var i = 1; i <= DeviationSamples; i++) @@ -197,14 +286,22 @@ namespace OpenNest.Geometry var p = EvaluatePoint(semiMajor, semiMinor, rotation, center, t); var dist = p.DistanceTo(arcCenter); var dev = System.Math.Abs(dist - radius); - if (dev > maxDev) maxDev = dev; + if (dev > maxDev) + maxDev = dev; } return maxDev; } - private static Arc CreateArc(Vector arcCenter, double radius, - Vector ellipseCenter, double semiMajor, double semiMinor, double rotation, - double t0, double t1) + private static Arc CreateArc( + Vector arcCenter, + double radius, + Vector ellipseCenter, + double semiMajor, + double semiMinor, + double rotation, + double t0, + double t1 + ) { var p0 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t0); var p1 = EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1); @@ -225,8 +322,10 @@ namespace OpenNest.Geometry var points = new List { p0, pMid, p1 }; var isReversed = SumSignedAngles(arcCenter, points) < 0; - if (startAngle < 0) startAngle += Angle.TwoPI; - if (endAngle < 0) endAngle += Angle.TwoPI; + if (startAngle < 0) + startAngle += Angle.TwoPI; + if (endAngle < 0) + endAngle += Angle.TwoPI; return new Arc(arcCenter, radius, startAngle, endAngle, isReversed); } @@ -239,8 +338,10 @@ namespace OpenNest.Geometry var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X); var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X); var da = a2 - a1; - while (da > System.Math.PI) da -= Angle.TwoPI; - while (da < -System.Math.PI) da += Angle.TwoPI; + while (da > System.Math.PI) + da -= Angle.TwoPI; + while (da < -System.Math.PI) + da += Angle.TwoPI; total += da; } return total; diff --git a/OpenNest.Core/Geometry/Entity.cs b/OpenNest.Core/Geometry/Entity.cs index 723ddc6..e71d350 100644 --- a/OpenNest.Core/Geometry/Entity.cs +++ b/OpenNest.Core/Geometry/Entity.cs @@ -1,7 +1,7 @@ -using OpenNest.Math; -using System; +using System; using System.Collections.Generic; using System.Drawing; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -331,7 +331,11 @@ namespace OpenNest.Geometry return points; } - public static BoundingRectangleResult FindBestRotation(this List entities, double startAngle = 0, double endAngle = Angle.TwoPI) + public static BoundingRectangleResult FindBestRotation( + this List entities, + double startAngle = 0, + double endAngle = Angle.TwoPI + ) { // Check for Shape entity first (recursive case returns early) foreach (var entity in entities) diff --git a/OpenNest.Core/Geometry/EntityType.cs b/OpenNest.Core/Geometry/EntityType.cs index f0cb14e..402c662 100644 --- a/OpenNest.Core/Geometry/EntityType.cs +++ b/OpenNest.Core/Geometry/EntityType.cs @@ -1,5 +1,4 @@ - -namespace OpenNest.Geometry +namespace OpenNest.Geometry { public enum EntityType { @@ -7,6 +6,6 @@ namespace OpenNest.Geometry Circle, Line, Shape, - Polygon + Polygon, } } diff --git a/OpenNest.Core/Geometry/GeometryOptimizer.cs b/OpenNest.Core/Geometry/GeometryOptimizer.cs index 97595f9..c2b6070 100644 --- a/OpenNest.Core/Geometry/GeometryOptimizer.cs +++ b/OpenNest.Core/Geometry/GeometryOptimizer.cs @@ -1,21 +1,25 @@ -using OpenNest.Math; using System; using System.Collections.Generic; using System.Threading.Tasks; +using OpenNest.Math; namespace OpenNest.Geometry { public static class GeometryOptimizer { public static void Optimize(IList arcs) => - MergePass(arcs, + MergePass( + arcs, (list, item, i) => list.GetCoradialArs(item, i), - (Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined)); + (Arc a, Arc b, out Arc joined) => TryJoinArcs(a, b, out joined) + ); public static void Optimize(IList lines) => - MergePass(lines, + MergePass( + lines, (list, item, i) => list.GetCollinearLines(item, i), - (Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined)); + (Line a, Line b, out Line joined) => TryJoinLines(a, b, out joined) + ); public static void Deduplicate(IList circles) { @@ -23,8 +27,10 @@ namespace OpenNest.Geometry { for (var j = i - 1; j >= 0; j--) { - if (circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon - && circles[i].Radius.IsEqualTo(circles[j].Radius)) + if ( + circles[i].Center.DistanceTo(circles[j].Center) <= Tolerance.Epsilon + && circles[i].Radius.IsEqualTo(circles[j].Radius) + ) { circles.RemoveAt(i); break; @@ -39,9 +45,11 @@ namespace OpenNest.Geometry { for (var j = arcs.Count - 1; j >= 0; j--) { - if (arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon + if ( + arcs[j].Center.DistanceTo(circles[i].Center) <= Tolerance.Epsilon && arcs[j].Radius.IsEqualTo(circles[i].Radius) - && arcs[j].IsFullCircle()) + && arcs[j].IsFullCircle() + ) { arcs.RemoveAt(j); } @@ -51,9 +59,12 @@ namespace OpenNest.Geometry private delegate bool TryJoin(T a, T b, out T joined); - private static void MergePass(IList items, + private static void MergePass( + IList items, Func, T, int, List> findCandidates, - TryJoin tryJoin) where T : class + TryJoin tryJoin + ) + where T : class { for (var i = 0; i < items.Count; ++i) { @@ -117,10 +128,14 @@ namespace OpenNest.Geometry if (!onPoint) { - if (t1 < b2 - Tolerance.Epsilon) return false; - if (b1 > t2 + Tolerance.Epsilon) return false; - if (l1 > r2 + Tolerance.Epsilon) return false; - if (r1 < l2 - Tolerance.Epsilon) return false; + if (t1 < b2 - Tolerance.Epsilon) + return false; + if (b1 > t2 + Tolerance.Epsilon) + return false; + if (l1 > r2 + Tolerance.Epsilon) + return false; + if (r1 < l2 - Tolerance.Epsilon) + return false; } var l = l1 < l2 ? l1 : l2; @@ -129,9 +144,17 @@ namespace OpenNest.Geometry var b = b1 < b2 ? b1 : b2; if (!line1.IsVertical() && line1.Slope() < 0) - lineOut = new Line(new Vector(l, t), new Vector(r, b)) { Layer = line1.Layer, Color = line1.Color }; + lineOut = new Line(new Vector(l, t), new Vector(r, b)) + { + Layer = line1.Layer, + Color = line1.Color, + }; else - lineOut = new Line(new Vector(l, b), new Vector(r, t)) { Layer = line1.Layer, Color = line1.Color }; + lineOut = new Line(new Vector(l, b), new Vector(r, t)) + { + Layer = line1.Layer, + Color = line1.Color, + }; return true; } @@ -177,33 +200,47 @@ namespace OpenNest.Geometry if (sweep >= Angle.TwoPI - Tolerance.Epsilon) return false; - if (startAngle < 0) startAngle += Angle.TwoPI; - if (endAngle < 0) endAngle += Angle.TwoPI; + if (startAngle < 0) + startAngle += Angle.TwoPI; + if (endAngle < 0) + endAngle += Angle.TwoPI; - arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) { Layer = arc1.Layer, Color = arc1.Color }; + arcOut = new Arc(arc1.Center, arc1.Radius, startAngle, endAngle) + { + Layer = arc1.Layer, + Color = arc1.Color, + }; return true; } - private static List GetCollinearLines(this IList lines, Line line, int startIndex) + private static List GetCollinearLines( + this IList lines, + Line line, + int startIndex + ) { var collinearLines = new List(); - Parallel.For(startIndex, lines.Count, index => - { - var compareLine = lines[index]; - - if (Object.ReferenceEquals(line, compareLine)) - return; - - if (!line.IsCollinearTo(compareLine)) - return; - - lock (collinearLines) + Parallel.For( + startIndex, + lines.Count, + index => { - collinearLines.Add(compareLine); + var compareLine = lines[index]; + + if (Object.ReferenceEquals(line, compareLine)) + return; + + if (!line.IsCollinearTo(compareLine)) + return; + + lock (collinearLines) + { + collinearLines.Add(compareLine); + } } - }); + ); return collinearLines; } @@ -212,21 +249,25 @@ namespace OpenNest.Geometry { var coradialArcs = new List(); - Parallel.For(startIndex, arcs.Count, index => - { - var compareArc = arcs[index]; - - if (Object.ReferenceEquals(arc, compareArc)) - return; - - if (!arc.IsCoradialTo(compareArc)) - return; - - lock (coradialArcs) + Parallel.For( + startIndex, + arcs.Count, + index => { - coradialArcs.Add(compareArc); + var compareArc = arcs[index]; + + if (Object.ReferenceEquals(arc, compareArc)) + return; + + if (!arc.IsCoradialTo(compareArc)) + return; + + lock (coradialArcs) + { + coradialArcs.Add(compareArc); + } } - }); + ); return coradialArcs; } diff --git a/OpenNest.Core/Geometry/IBoundable.cs b/OpenNest.Core/Geometry/IBoundable.cs index 7d1724e..dc7529a 100644 --- a/OpenNest.Core/Geometry/IBoundable.cs +++ b/OpenNest.Core/Geometry/IBoundable.cs @@ -1,5 +1,4 @@ - -namespace OpenNest.Geometry +namespace OpenNest.Geometry { public interface IBoundable { diff --git a/OpenNest.Core/Geometry/InnerFitPolygon.cs b/OpenNest.Core/Geometry/InnerFitPolygon.cs index b4c9f68..b51e8b4 100644 --- a/OpenNest.Core/Geometry/InnerFitPolygon.cs +++ b/OpenNest.Core/Geometry/InnerFitPolygon.cs @@ -28,10 +28,14 @@ namespace OpenNest.Geometry for (var i = 1; i < verts.Count; i++) { - if (verts[i].X < minX) minX = verts[i].X; - if (verts[i].X > maxX) maxX = verts[i].X; - if (verts[i].Y < minY) minY = verts[i].Y; - if (verts[i].Y > maxY) maxY = verts[i].Y; + if (verts[i].X < minX) + minX = verts[i].X; + if (verts[i].X > maxX) + maxX = verts[i].X; + if (verts[i].Y < minY) + minY = verts[i].Y; + if (verts[i].Y > maxY) + maxY = verts[i].Y; } // The IFP is the work area shrunk inward by the part's extent in each direction. diff --git a/OpenNest.Core/Geometry/Intersect.cs b/OpenNest.Core/Geometry/Intersect.cs index 1a2ceb7..1cfc6d6 100644 --- a/OpenNest.Core/Geometry/Intersect.cs +++ b/OpenNest.Core/Geometry/Intersect.cs @@ -1,6 +1,6 @@ -using OpenNest.Math; using System.Collections.Generic; using System.Linq; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -18,8 +18,19 @@ namespace OpenNest.Geometry } pts = pts.Where(pt => - Angle.IsBetweenRad(arc1.Center.AngleTo(pt), arc1.StartAngle, arc1.EndAngle, arc1.IsReversed) && - Angle.IsBetweenRad(arc2.Center.AngleTo(pt), arc2.StartAngle, arc2.EndAngle, arc2.IsReversed)) + Angle.IsBetweenRad( + arc1.Center.AngleTo(pt), + arc1.StartAngle, + arc1.EndAngle, + arc1.IsReversed + ) + && Angle.IsBetweenRad( + arc2.Center.AngleTo(pt), + arc2.StartAngle, + arc2.EndAngle, + arc2.IsReversed + ) + ) .ToList(); return pts.Count > 0; @@ -35,11 +46,15 @@ namespace OpenNest.Geometry return false; } - pts = pts.Where(pt => Angle.IsBetweenRad( - arc.Center.AngleTo(pt), - arc.StartAngle, - arc.EndAngle, - arc.IsReversed)).ToList(); + pts = pts.Where(pt => + Angle.IsBetweenRad( + arc.Center.AngleTo(pt), + arc.StartAngle, + arc.EndAngle, + arc.IsReversed + ) + ) + .ToList(); return pts.Count > 0; } @@ -54,11 +69,15 @@ namespace OpenNest.Geometry return false; } - pts = pts.Where(pt => Angle.IsBetweenRad( - arc.Center.AngleTo(pt), - arc.StartAngle, - arc.EndAngle, - arc.IsReversed)).ToList(); + pts = pts.Where(pt => + Angle.IsBetweenRad( + arc.Center.AngleTo(pt), + arc.StartAngle, + arc.EndAngle, + arc.IsReversed + ) + ) + .ToList(); return pts.Count > 0; } @@ -74,11 +93,15 @@ namespace OpenNest.Geometry pts2.AddRange(pts3); } - pts = pts2.Where(pt => Angle.IsBetweenRad( - arc.Center.AngleTo(pt), - arc.StartAngle, - arc.EndAngle, - arc.IsReversed)).ToList(); + pts = pts2.Where(pt => + Angle.IsBetweenRad( + arc.Center.AngleTo(pt), + arc.StartAngle, + arc.EndAngle, + arc.IsReversed + ) + ) + .ToList(); return pts.Count > 0; } @@ -95,11 +118,15 @@ namespace OpenNest.Geometry pts2.AddRange(pts3); } - pts = pts2.Where(pt => Angle.IsBetweenRad( - arc.Center.AngleTo(pt), - arc.StartAngle, - arc.EndAngle, - arc.IsReversed)).ToList(); + pts = pts2.Where(pt => + Angle.IsBetweenRad( + arc.Center.AngleTo(pt), + arc.StartAngle, + arc.EndAngle, + arc.IsReversed + ) + ) + .ToList(); return pts.Count > 0; } @@ -123,20 +150,22 @@ namespace OpenNest.Geometry } var d = circle2.Center - circle1.Center; - var a = (circle1.Radius * circle1.Radius - circle2.Radius * circle2.Radius + distance * distance) / (2.0 * distance); + var a = + ( + circle1.Radius * circle1.Radius + - circle2.Radius * circle2.Radius + + distance * distance + ) / (2.0 * distance); var h = System.Math.Sqrt(circle1.Radius * circle1.Radius - a * a); var pt = new Vector( circle1.Center.X + (a * d.X) / distance, - circle1.Center.Y + (a * d.Y) / distance); + circle1.Center.Y + (a * d.Y) / distance + ); - var i1 = new Vector( - pt.X + (h * d.Y) / distance, - pt.Y - (h * d.X) / distance); + var i1 = new Vector(pt.X + (h * d.Y) / distance, pt.Y - (h * d.X) / distance); - var i2 = new Vector( - pt.X - (h * d.Y) / distance, - pt.Y + (h * d.X) / distance); + var i2 = new Vector(pt.X - (h * d.Y) / distance, pt.Y + (h * d.X) / distance); pts = i1 != i2 ? new List { i1, i2 } : new List { i1 }; diff --git a/OpenNest.Core/Geometry/Layer.cs b/OpenNest.Core/Geometry/Layer.cs index b623b34..07fe43f 100644 --- a/OpenNest.Core/Geometry/Layer.cs +++ b/OpenNest.Core/Geometry/Layer.cs @@ -7,7 +7,7 @@ namespace OpenNest.Geometry public static readonly Layer Default = new Layer("0") { Color = Color.White, - IsVisible = true + IsVisible = true, }; public Layer(string name) diff --git a/OpenNest.Core/Geometry/Line.cs b/OpenNest.Core/Geometry/Line.cs index 9b4474b..5477cec 100644 --- a/OpenNest.Core/Geometry/Line.cs +++ b/OpenNest.Core/Geometry/Line.cs @@ -1,6 +1,6 @@ -using OpenNest.Math; -using System; +using System; using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -9,14 +9,10 @@ namespace OpenNest.Geometry internal Vector pt1; internal Vector pt2; - public Line() - { - } + public Line() { } public Line(double x1, double y1, double x2, double y2) - : this(new Vector(x1, y1), new Vector(x2, y2)) - { - } + : this(new Vector(x1, y1), new Vector(x2, y2)) { } public Line(Vector startPoint, Vector endPoint) { @@ -83,9 +79,7 @@ namespace OpenNest.Geometry return EndPoint; else { - return new Vector( - StartPoint.X + param * diff2.X, - StartPoint.Y + param * diff2.Y); + return new Vector(StartPoint.X + param * diff2.X, StartPoint.Y + param * diff2.Y); } } @@ -372,7 +366,7 @@ namespace OpenNest.Geometry /// /// Updates the bounding box. /// - public override sealed void UpdateBounds() + public sealed override void UpdateBounds() { if (StartPoint.X < EndPoint.X) { @@ -429,13 +423,13 @@ namespace OpenNest.Geometry /// A tuple of (first, second) sub-lines. public (Line first, Line second) SplitAt(Vector point) { - var first = point.DistanceTo(StartPoint) < Tolerance.Epsilon - ? null - : new Line(StartPoint, point); + var first = + point.DistanceTo(StartPoint) < Tolerance.Epsilon + ? null + : new Line(StartPoint, point); - var second = point.DistanceTo(EndPoint) < Tolerance.Epsilon - ? null - : new Line(point, EndPoint); + var second = + point.DistanceTo(EndPoint) < Tolerance.Epsilon ? null : new Line(point, EndPoint); return (first, second); } diff --git a/OpenNest.Core/Geometry/NoFitPolygon.cs b/OpenNest.Core/Geometry/NoFitPolygon.cs index 7d88c87..d519f12 100644 --- a/OpenNest.Core/Geometry/NoFitPolygon.cs +++ b/OpenNest.Core/Geometry/NoFitPolygon.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using Clipper2Lib; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Geometry { @@ -99,14 +99,15 @@ namespace OpenNest.Geometry var startB = FindBottomLeft(b); var result = new Polygon(); - - // The starting point of the Minkowski sum A + B is the sum of the - // starting points of A and B. For NFP = A + (-B), this is + + // The starting point of the Minkowski sum A + B is the sum of the + // starting points of A and B. For NFP = A + (-B), this is // startA + startReflectedB. var current = new Vector( a.Vertices[startA].X + b.Vertices[startB].X, - a.Vertices[startA].Y + b.Vertices[startB].Y); - + a.Vertices[startA].Y + b.Vertices[startB].Y + ); + result.Vertices.Add(current); var ia = 0; @@ -132,10 +133,12 @@ namespace OpenNest.Geometry else { var angleA = System.Math.Atan2(orderedA[ia].Y, orderedA[ia].X); - if (angleA < 0) angleA += Angle.TwoPI; + if (angleA < 0) + angleA += Angle.TwoPI; var angleB = System.Math.Atan2(orderedB[ib].Y, orderedB[ib].X); - if (angleB < 0) angleB += Angle.TwoPI; + if (angleB < 0) + angleB += Angle.TwoPI; if (angleA < angleB) { @@ -149,7 +152,8 @@ namespace OpenNest.Geometry { edge = new Vector( orderedA[ia].X + orderedB[ib].X, - orderedA[ia].Y + orderedB[ib].Y); + orderedA[ia].Y + orderedB[ib].Y + ); ia++; ib++; } @@ -203,8 +207,10 @@ namespace OpenNest.Geometry for (var i = 1; i < n; i++) { - if (verts[i].Y < verts[best].Y || - (verts[i].Y == verts[best].Y && verts[i].X < verts[best].X)) + if ( + verts[i].Y < verts[best].Y + || (verts[i].Y == verts[best].Y && verts[i].X < verts[best].X) + ) best = i; } diff --git a/OpenNest.Core/Geometry/PolyLabel.cs b/OpenNest.Core/Geometry/PolyLabel.cs index e6b2c9d..e8de568 100644 --- a/OpenNest.Core/Geometry/PolyLabel.cs +++ b/OpenNest.Core/Geometry/PolyLabel.cs @@ -4,12 +4,14 @@ namespace OpenNest.Geometry { public static class PolyLabel { - public static Vector Find(Polygon outer, IList holes = null, double precision = 0.5) + public static Vector Find( + Polygon outer, + IList holes = null, + double precision = 0.5 + ) { if (outer.Vertices.Count < 3) - return outer.Vertices.Count > 0 - ? outer.Vertices[0] - : new Vector(); + return outer.Vertices.Count > 0 ? outer.Vertices[0] : new Vector(); var minX = double.MaxValue; var minY = double.MaxValue; @@ -19,10 +21,14 @@ namespace OpenNest.Geometry for (var i = 0; i < outer.Vertices.Count; i++) { var v = outer.Vertices[i]; - if (v.X < minX) minX = v.X; - if (v.Y < minY) minY = v.Y; - if (v.X > maxX) maxX = v.X; - if (v.Y > maxY) maxY = v.Y; + if (v.X < minX) + minX = v.X; + if (v.Y < minY) + minY = v.Y; + if (v.X > maxX) + maxX = v.X; + if (v.Y > maxY) + maxY = v.Y; } var width = maxX - minX; @@ -37,8 +43,8 @@ namespace OpenNest.Geometry var queue = new List(); for (var x = minX; x < maxX; x += cellSize) - for (var y = minY; y < maxY; y += cellSize) - queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes)); + for (var y = minY; y < maxY; y += cellSize) + queue.Add(new Cell(x + halfCell, y + halfCell, halfCell, outer, holes)); queue.Sort((a, b) => b.MaxDist.CompareTo(a.MaxDist)); @@ -194,7 +200,12 @@ namespace OpenNest.Geometry } } - private static double PointToAllEdgesDist(double x, double y, Polygon outer, IList holes) + private static double PointToAllEdgesDist( + double x, + double y, + Polygon outer, + IList holes + ) { var minDist = PointToPolygonDist(x, y, outer); diff --git a/OpenNest.Core/Geometry/Polygon.cs b/OpenNest.Core/Geometry/Polygon.cs index 9ae1cab..dec134e 100644 --- a/OpenNest.Core/Geometry/Polygon.cs +++ b/OpenNest.Core/Geometry/Polygon.cs @@ -1,7 +1,7 @@ -using OpenNest.Math; -using System; +using System; using System.Collections.Generic; using System.Linq; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -107,7 +107,9 @@ namespace OpenNest.Geometry public RotationType RotationDirection() { if (Vertices.Count < 3) - throw new Exception("Not enough points to determine direction. Must have at least 3 points."); + throw new Exception( + "Not enough points to determine direction. Must have at least 3 points." + ); return CalculateArea() > 0 ? RotationType.CCW : RotationType.CW; } @@ -309,11 +311,15 @@ namespace OpenNest.Geometry { var vertex = Vertices[i]; - if (vertex.X < minX) minX = vertex.X; - else if (vertex.X > maxX) maxX = vertex.X; + if (vertex.X < minX) + minX = vertex.X; + else if (vertex.X > maxX) + maxX = vertex.X; - if (vertex.Y < minY) minY = vertex.Y; - else if (vertex.Y > maxY) maxY = vertex.Y; + if (vertex.Y < minY) + minY = vertex.Y; + else if (vertex.Y > maxY) + maxY = vertex.Y; } boundingBox.X = minX; @@ -354,10 +360,19 @@ namespace OpenNest.Geometry { var prev = (i - 1 + count) % count; - var a1 = new Vector(Vertices[prev].X + normals[prev].X, Vertices[prev].Y + normals[prev].Y); - var a2 = new Vector(Vertices[i].X + normals[prev].X, Vertices[i].Y + normals[prev].Y); + var a1 = new Vector( + Vertices[prev].X + normals[prev].X, + Vertices[prev].Y + normals[prev].Y + ); + var a2 = new Vector( + Vertices[i].X + normals[prev].X, + Vertices[i].Y + normals[prev].Y + ); var b1 = new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y); - var b2 = new Vector(Vertices[(i + 1) % count].X + normals[i].X, Vertices[(i + 1) % count].Y + normals[i].Y); + var b2 = new Vector( + Vertices[(i + 1) % count].X + normals[i].X, + Vertices[(i + 1) % count].Y + normals[i].Y + ); var edgeA = new Line(a1, a2); var edgeB = new Line(b1, b2); @@ -365,7 +380,9 @@ namespace OpenNest.Geometry if (edgeA.Intersects(edgeB, out var pt) && pt.IsValid()) result.Vertices.Add(pt); else - result.Vertices.Add(new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y)); + result.Vertices.Add( + new Vector(Vertices[i].X + normals[i].X, Vertices[i].Y + normals[i].Y) + ); } result.Close(); @@ -379,8 +396,10 @@ namespace OpenNest.Geometry var left = OffsetEntity(distance, OffsetSide.Left); var right = OffsetEntity(distance, OffsetSide.Right); - if (left == null) return right; - if (right == null) return left; + if (left == null) + return right; + if (right == null) + return left; var distLeft = left.ClosestPointTo(pt).DistanceTo(pt); var distRight = right.ClosestPointTo(pt).DistanceTo(pt); @@ -581,13 +600,25 @@ namespace OpenNest.Geometry var bj = edgeBounds[j]; // Prune with bounding box check. - if (bi.maxX < bj.minX || bj.maxX < bi.minX || - bi.maxY < bj.minY || bj.maxY < bi.minY) + if ( + bi.maxX < bj.minX + || bj.maxX < bi.minX + || bi.maxY < bj.minY + || bj.maxY < bi.minY + ) { continue; } - if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt)) + if ( + SegmentsIntersect( + Vertices[i], + Vertices[i + 1], + Vertices[j], + Vertices[j + 1], + out pt + ) + ) { edgeI = i; edgeJ = j; @@ -620,7 +651,13 @@ namespace OpenNest.Geometry return areaA >= areaB ? loopA : loopB; } - private static bool SegmentsIntersect(Vector a1, Vector a2, Vector b1, Vector b2, out Vector pt) + private static bool SegmentsIntersect( + Vector a1, + Vector a2, + Vector b1, + Vector b2, + out Vector pt + ) { var da = a2 - a1; var db = b2 - b1; @@ -636,8 +673,12 @@ namespace OpenNest.Geometry var t = (dc.X * db.Y - dc.Y * db.X) / cross; var u = (dc.X * da.Y - dc.Y * da.X) / cross; - if (t > Tolerance.Epsilon && t < 1.0 - Tolerance.Epsilon && - u > Tolerance.Epsilon && u < 1.0 - Tolerance.Epsilon) + if ( + t > Tolerance.Epsilon + && t < 1.0 - Tolerance.Epsilon + && u > Tolerance.Epsilon + && u < 1.0 - Tolerance.Epsilon + ) { pt = new Vector(a1.X + t * da.X, a1.Y + t * da.Y); return true; @@ -701,8 +742,10 @@ namespace OpenNest.Geometry var vi = Vertices[i]; var vj = Vertices[j]; - if ((vi.Y > pt.Y) != (vj.Y > pt.Y) && - pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X) + if ( + (vi.Y > pt.Y) != (vj.Y > pt.Y) + && pt.X < (vj.X - vi.X) * (pt.Y - vi.Y) / (vj.Y - vi.Y) + vi.X + ) { inside = !inside; } diff --git a/OpenNest.Core/Geometry/RotatingCalipers.cs b/OpenNest.Core/Geometry/RotatingCalipers.cs index cc7ff08..8e2989d 100644 --- a/OpenNest.Core/Geometry/RotatingCalipers.cs +++ b/OpenNest.Core/Geometry/RotatingCalipers.cs @@ -1,5 +1,5 @@ -using OpenNest.Math; using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -71,18 +71,24 @@ namespace OpenNest.Geometry var vy = ux; // Project all hull vertices onto edge direction (u) and perpendicular (v) - double minU = double.MaxValue, maxU = double.MinValue; - double minV = double.MaxValue, maxV = double.MinValue; + double minU = double.MaxValue, + maxU = double.MinValue; + double minV = double.MaxValue, + maxV = double.MinValue; for (int j = 0; j < n; j++) { var projU = vertices[j].X * ux + vertices[j].Y * uy; var projV = vertices[j].X * vx + vertices[j].Y * vy; - if (projU < minU) minU = projU; - if (projU > maxU) maxU = projU; - if (projV < minV) minV = projV; - if (projV > maxV) maxV = projV; + if (projU < minU) + minU = projU; + if (projU > maxU) + maxU = projU; + if (projV < minV) + minV = projV; + if (projV > maxV) + maxV = projV; } var width = maxU - minU; @@ -99,7 +105,11 @@ namespace OpenNest.Geometry return best ?? new BoundingRectangleResult(0, 0, 0); } - public static BoundingRectangleResult MinimumBoundingRectangle(Polygon hull, double startAngle, double endAngle) + public static BoundingRectangleResult MinimumBoundingRectangle( + Polygon hull, + double startAngle, + double endAngle + ) { var vertices = hull.Vertices; int n = hull.IsClosed() ? vertices.Count - 1 : vertices.Count; @@ -153,23 +163,33 @@ namespace OpenNest.Geometry return best ?? new BoundingRectangleResult(startAngle, 0, 0); } - private static BoundingRectangleResult EvaluateAtAngle(IList vertices, int n, double angle) + private static BoundingRectangleResult EvaluateAtAngle( + IList vertices, + int n, + double angle + ) { var cos = System.Math.Cos(angle); var sin = System.Math.Sin(angle); - double minU = double.MaxValue, maxU = double.MinValue; - double minV = double.MaxValue, maxV = double.MinValue; + double minU = double.MaxValue, + maxU = double.MinValue; + double minV = double.MaxValue, + maxV = double.MinValue; for (int j = 0; j < n; j++) { var projU = vertices[j].X * cos + vertices[j].Y * sin; var projV = -vertices[j].X * sin + vertices[j].Y * cos; - if (projU < minU) minU = projU; - if (projU > maxU) maxU = projU; - if (projV < minV) minV = projV; - if (projV > maxV) maxV = projV; + if (projU < minU) + minU = projU; + if (projU > maxU) + maxU = projU; + if (projV < minV) + minV = projV; + if (projV > maxV) + maxV = projV; } var width = maxU - minU; diff --git a/OpenNest.Core/Geometry/Shape.cs b/OpenNest.Core/Geometry/Shape.cs index 6cd9358..a2d9792 100644 --- a/OpenNest.Core/Geometry/Shape.cs +++ b/OpenNest.Core/Geometry/Shape.cs @@ -282,11 +282,7 @@ namespace OpenNest.Geometry case EntityType.Line: var line = (Line)entity; - polygon.Vertices.AddRange(new[] - { - line.StartPoint, - line.EndPoint - }); + polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint }); break; case EntityType.Circle: @@ -320,21 +316,21 @@ namespace OpenNest.Geometry { case EntityType.Arc: var arc = (Arc)entity; - polygon.Vertices.AddRange(arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe)); + polygon.Vertices.AddRange( + arc.ToPoints(arc.SegmentsForTolerance(tolerance), circumscribe) + ); break; case EntityType.Line: var line = (Line)entity; - polygon.Vertices.AddRange(new[] - { - line.StartPoint, - line.EndPoint - }); + polygon.Vertices.AddRange(new[] { line.StartPoint, line.EndPoint }); break; case EntityType.Circle: var circle = (Circle)entity; - polygon.Vertices.AddRange(circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe)); + polygon.Vertices.AddRange( + circle.ToPoints(circle.SegmentsForTolerance(tolerance), circumscribe) + ); break; default: @@ -462,9 +458,7 @@ namespace OpenNest.Geometry /// public override void UpdateBounds() { - boundingBox = Entities.Select(geo => geo.BoundingBox) - .ToList() - .GetBoundingBox(); + boundingBox = Entities.Select(geo => geo.BoundingBox).ToList().GetBoundingBox(); } public override Entity OffsetEntity(double distance, OffsetSide side) @@ -493,22 +487,27 @@ namespace OpenNest.Geometry switch (entity.Type) { case EntityType.Line: + { + var line = (Line)entity; + var offsetLine = (Line)offsetEntity; + + if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line) { - var line = (Line)entity; - var offsetLine = (Line)offsetEntity; - - if (lastOffsetEntity != null && lastOffsetEntity.Type == EntityType.Line) - { - JoinOffsetLines( - (Line)lastEntity, (Line)lastOffsetEntity, - line, offsetLine, - distance, side, offsetShape); - } - - offsetShape.Entities.Add(offsetLine); - break; + JoinOffsetLines( + (Line)lastEntity, + (Line)lastOffsetEntity, + line, + offsetLine, + distance, + side, + offsetShape + ); } + offsetShape.Entities.Add(offsetLine); + break; + } + default: offsetShape.Entities.Add(offsetEntity); break; @@ -519,27 +518,42 @@ namespace OpenNest.Geometry } // Close the shape: join last offset entity back to first - if (lastOffsetEntity != null && firstOffsetEntity != null + if ( + lastOffsetEntity != null + && firstOffsetEntity != null && lastOffsetEntity != firstOffsetEntity && lastOffsetEntity.Type == EntityType.Line - && firstOffsetEntity.Type == EntityType.Line) + && firstOffsetEntity.Type == EntityType.Line + ) { JoinOffsetLines( - (Line)lastEntity, (Line)lastOffsetEntity, - (Line)firstEntity, (Line)firstOffsetEntity, - distance, side, offsetShape); + (Line)lastEntity, + (Line)lastOffsetEntity, + (Line)firstEntity, + (Line)firstOffsetEntity, + distance, + side, + offsetShape + ); } foreach (var cutout in definedShape.Cutouts) - offsetShape.Entities.AddRange(((Shape)cutout.OffsetEntity(distance, side)).Entities); + offsetShape.Entities.AddRange( + ((Shape)cutout.OffsetEntity(distance, side)).Entities + ); return offsetShape; } private static void JoinOffsetLines( - Line lastLine, Line lastOffsetLine, - Line line, Line offsetLine, - double distance, OffsetSide side, Shape offsetShape) + Line lastLine, + Line lastOffsetLine, + Line line, + Line offsetLine, + double distance, + OffsetSide side, + Shape offsetShape + ) { // Determine if this is a convex corner using the cross product of // the original line directions. Convex corners need an arc; concave @@ -548,8 +562,9 @@ namespace OpenNest.Geometry var d2 = line.EndPoint - line.StartPoint; var cross = d1.X * d2.Y - d1.Y * d2.X; - var isConvex = (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon) || - (side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon); + var isConvex = + (side == OffsetSide.Left && cross < -OpenNest.Math.Tolerance.Epsilon) + || (side == OffsetSide.Right && cross > OpenNest.Math.Tolerance.Epsilon); if (isConvex) { @@ -559,11 +574,13 @@ namespace OpenNest.Geometry line.StartPoint.AngleTo(lastOffsetLine.EndPoint), line.StartPoint.AngleTo(offsetLine.StartPoint), side == OffsetSide.Left - ); + ); offsetShape.Entities.Add(arc); } - else if (Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection)) + else if ( + Intersect.IntersectsUnbounded(offsetLine, lastOffsetLine, out var intersection) + ) { offsetLine.StartPoint = intersection; lastOffsetLine.EndPoint = intersection; @@ -576,7 +593,7 @@ namespace OpenNest.Geometry line.StartPoint.AngleTo(lastOffsetLine.EndPoint), line.StartPoint.AngleTo(offsetLine.StartPoint), side == OffsetSide.Left - ); + ); offsetShape.Entities.Add(arc); } @@ -596,8 +613,11 @@ namespace OpenNest.Geometry { var poly = ToPolygon(); - if (poly == null || poly.Vertices.Count < 3 - || poly.RotationDirection() == RotationType.CW) + if ( + poly == null + || poly.Vertices.Count < 3 + || poly.RotationDirection() == RotationType.CW + ) return OffsetEntity(distance, OffsetSide.Left) as Shape; // Shape is CCW — reverse to CW so Left offset goes outward. @@ -611,10 +631,21 @@ namespace OpenNest.Geometry copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer }); break; case Arc a: - copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer }); + copy.Entities.Add( + new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) + { + Layer = a.Layer, + } + ); break; case Circle c: - copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CW }); + copy.Entities.Add( + new Circle(c.Center, c.Radius) + { + Layer = c.Layer, + Rotation = RotationType.CW, + } + ); break; } } @@ -631,8 +662,11 @@ namespace OpenNest.Geometry { var poly = ToPolygon(); - if (poly == null || poly.Vertices.Count < 3 - || poly.RotationDirection() == RotationType.CCW) + if ( + poly == null + || poly.Vertices.Count < 3 + || poly.RotationDirection() == RotationType.CCW + ) return OffsetEntity(distance, OffsetSide.Left) as Shape; // Create a reversed copy to avoid mutating shared entity objects. @@ -646,10 +680,21 @@ namespace OpenNest.Geometry copy.Entities.Add(new Line(l.EndPoint, l.StartPoint) { Layer = l.Layer }); break; case Arc a: - copy.Entities.Add(new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) { Layer = a.Layer }); + copy.Entities.Add( + new Arc(a.Center, a.Radius, a.EndAngle, a.StartAngle, !a.IsReversed) + { + Layer = a.Layer, + } + ); break; case Circle c: - copy.Entities.Add(new Circle(c.Center, c.Radius) { Layer = c.Layer, Rotation = RotationType.CCW }); + copy.Entities.Add( + new Circle(c.Center, c.Radius) + { + Layer = c.Layer, + Rotation = RotationType.CCW, + } + ); break; } } diff --git a/OpenNest.Core/Geometry/ShapeBuilder.cs b/OpenNest.Core/Geometry/ShapeBuilder.cs index 2a83b7b..672352f 100644 --- a/OpenNest.Core/Geometry/ShapeBuilder.cs +++ b/OpenNest.Core/Geometry/ShapeBuilder.cs @@ -1,13 +1,16 @@ -using OpenNest.Math; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using OpenNest.Math; namespace OpenNest.Geometry { public static class ShapeBuilder { - public static List GetShapes(IEnumerable entities, double? weldTolerance = null) + public static List GetShapes( + IEnumerable entities, + double? weldTolerance = null + ) { var lines = new List(); var arcs = new List(); @@ -141,7 +144,11 @@ namespace OpenNest.Geometry private static void AddToGroup( List> groups, - Entity entity, bool isStart, Vector point, double tolerance) + Entity entity, + bool isStart, + Vector point, + double tolerance + ) { foreach (var group in groups) { diff --git a/OpenNest.Core/Geometry/ShapeProfile.cs b/OpenNest.Core/Geometry/ShapeProfile.cs index e73632d..370e149 100644 --- a/OpenNest.Core/Geometry/ShapeProfile.cs +++ b/OpenNest.Core/Geometry/ShapeProfile.cs @@ -84,8 +84,7 @@ namespace OpenNest.Geometry { var poly = shape.ToPolygon(); - if (poly != null && poly.Vertices.Count >= 3 - && poly.RotationDirection() != desired) + if (poly != null && poly.Vertices.Count >= 3 && poly.RotationDirection() != desired) { shape.Reverse(); } diff --git a/OpenNest.Core/Geometry/Size.cs b/OpenNest.Core/Geometry/Size.cs index ca225e7..96c963b 100644 --- a/OpenNest.Core/Geometry/Size.cs +++ b/OpenNest.Core/Geometry/Size.cs @@ -44,6 +44,7 @@ namespace OpenNest.Geometry public override string ToString() => $"{Width} x {Length}"; - public string ToString(int decimalPlaces) => $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}"; + public string ToString(int decimalPlaces) => + $"{System.Math.Round(Width, decimalPlaces)} x {System.Math.Round(Length, decimalPlaces)}"; } } diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs index c7bac71..dbb412f 100644 --- a/OpenNest.Core/Geometry/SpatialQuery.cs +++ b/OpenNest.Core/Geometry/SpatialQuery.cs @@ -1,6 +1,6 @@ -using OpenNest.Math; using System.Collections.Generic; using System.Linq; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -13,57 +13,72 @@ namespace OpenNest.Geometry private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction) { return RayEdgeDistance( - vertex.X, vertex.Y, - edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y, - direction); + vertex.X, + vertex.Y, + edge.pt1.X, + edge.pt1.Y, + edge.pt2.X, + edge.pt2.Y, + direction + ); } [System.Runtime.CompilerServices.MethodImpl( - System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining + )] private static double RayEdgeDistance( - double vx, double vy, - double p1x, double p1y, double p2x, double p2y, - PushDirection direction) + double vx, + double vy, + double p1x, + double p1y, + double p2x, + double p2y, + PushDirection direction + ) { switch (direction) { case PushDirection.Left: case PushDirection.Right: - { - var dy = p2y - p1y; - if (System.Math.Abs(dy) < Tolerance.Epsilon) - return double.MaxValue; - - var t = (vy - p1y) / dy; - if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) - return double.MaxValue; - - var ix = p1x + t * (p2x - p1x); - var dist = direction == PushDirection.Left ? vx - ix : ix - vx; - - if (dist > Tolerance.Epsilon) return dist; - if (dist >= -Tolerance.Epsilon) return 0; + { + var dy = p2y - p1y; + if (System.Math.Abs(dy) < Tolerance.Epsilon) return double.MaxValue; - } + + var t = (vy - p1y) / dy; + if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) + return double.MaxValue; + + var ix = p1x + t * (p2x - p1x); + var dist = direction == PushDirection.Left ? vx - ix : ix - vx; + + if (dist > Tolerance.Epsilon) + return dist; + if (dist >= -Tolerance.Epsilon) + return 0; + return double.MaxValue; + } case PushDirection.Down: case PushDirection.Up: - { - var dx = p2x - p1x; - if (System.Math.Abs(dx) < Tolerance.Epsilon) - return double.MaxValue; - - var t = (vx - p1x) / dx; - if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) - return double.MaxValue; - - var iy = p1y + t * (p2y - p1y); - var dist = direction == PushDirection.Down ? vy - iy : iy - vy; - - if (dist > Tolerance.Epsilon) return dist; - if (dist >= -Tolerance.Epsilon) return 0; + { + var dx = p2x - p1x; + if (System.Math.Abs(dx) < Tolerance.Epsilon) return double.MaxValue; - } + + var t = (vx - p1x) / dx; + if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) + return double.MaxValue; + + var iy = p1y + t * (p2y - p1y); + var dist = direction == PushDirection.Down ? vy - iy : iy - vy; + + if (dist > Tolerance.Epsilon) + return dist; + if (dist >= -Tolerance.Epsilon) + return 0; + return double.MaxValue; + } default: return double.MaxValue; @@ -75,11 +90,18 @@ namespace OpenNest.Geometry /// Returns double.MaxValue if the ray does not hit the segment. /// [System.Runtime.CompilerServices.MethodImpl( - System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining + )] public static double RayEdgeDistance( - double vx, double vy, - double p1x, double p1y, double p2x, double p2y, - double dirX, double dirY) + double vx, + double vy, + double p1x, + double p1y, + double p2x, + double p2y, + double dirX, + double dirY + ) { var ex = p2x - p1x; var ey = p2y - p1y; @@ -99,8 +121,10 @@ namespace OpenNest.Geometry if (s < -Tolerance.Epsilon || s > 1.0 + Tolerance.Epsilon) return double.MaxValue; - if (t > Tolerance.Epsilon) return t; - if (t >= -Tolerance.Epsilon) return 0; + if (t > Tolerance.Epsilon) + return t; + if (t >= -Tolerance.Epsilon) + return 0; return double.MaxValue; } @@ -109,12 +133,19 @@ namespace OpenNest.Geometry /// Returns false if no real intersection exists. /// [System.Runtime.CompilerServices.MethodImpl( - System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining + )] private static bool SolveRayCircle( - double vx, double vy, - double cx, double cy, double r, - double dirX, double dirY, - out double t1, out double t2) + double vx, + double vy, + double cx, + double cy, + double r, + double dirX, + double dirY, + out double t1, + out double t2 + ) { var ox = vx - cx; var oy = vy - cy; @@ -143,12 +174,20 @@ namespace OpenNest.Geometry /// angular span. Returns double.MaxValue if no hit. /// [System.Runtime.CompilerServices.MethodImpl( - System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining + )] public static double RayArcDistance( - double vx, double vy, - double cx, double cy, double r, - double startAngle, double endAngle, bool reversed, - double dirX, double dirY) + double vx, + double vy, + double cx, + double cy, + double r, + double startAngle, + double endAngle, + bool reversed, + double dirX, + double dirY + ) { if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2)) return double.MaxValue; @@ -157,16 +196,18 @@ namespace OpenNest.Geometry if (t1 > -Tolerance.Epsilon) { - var hitAngle = Angle.NormalizeRad(System.Math.Atan2( - vy + t1 * dirY - cy, vx + t1 * dirX - cx)); + var hitAngle = Angle.NormalizeRad( + System.Math.Atan2(vy + t1 * dirY - cy, vx + t1 * dirX - cx) + ); if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) best = t1 > Tolerance.Epsilon ? t1 : 0; } if (t2 > -Tolerance.Epsilon && t2 < best) { - var hitAngle = Angle.NormalizeRad(System.Math.Atan2( - vy + t2 * dirY - cy, vx + t2 * dirX - cx)); + var hitAngle = Angle.NormalizeRad( + System.Math.Atan2(vy + t2 * dirY - cy, vx + t2 * dirX - cx) + ); if (Angle.IsBetweenRad(hitAngle, startAngle, endAngle, reversed)) best = t2 > Tolerance.Epsilon ? t2 : 0; } @@ -179,19 +220,29 @@ namespace OpenNest.Geometry /// Returns double.MaxValue if no hit. /// [System.Runtime.CompilerServices.MethodImpl( - System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining + )] public static double RayCircleDistance( - double vx, double vy, - double cx, double cy, double r, - double dirX, double dirY) + double vx, + double vy, + double cx, + double cy, + double r, + double dirX, + double dirY + ) { if (!SolveRayCircle(vx, vy, cx, cy, r, dirX, dirY, out var t1, out var t2)) return double.MaxValue; - if (t1 > Tolerance.Epsilon) return t1; - if (t1 >= -Tolerance.Epsilon) return 0; - if (t2 > Tolerance.Epsilon) return t2; - if (t2 >= -Tolerance.Epsilon) return 0; + if (t1 > Tolerance.Epsilon) + return t1; + if (t1 >= -Tolerance.Epsilon) + return 0; + if (t2 > Tolerance.Epsilon) + return t2; + if (t2 >= -Tolerance.Epsilon) + return 0; return double.MaxValue; } @@ -201,7 +252,11 @@ namespace OpenNest.Geometry /// any edge of movingLines contacts any edge of stationaryLines. /// Returns double.MaxValue if no collision path exists. /// - public static double DirectionalDistance(List movingLines, List stationaryLines, PushDirection direction) + public static double DirectionalDistance( + List movingLines, + List stationaryLines, + PushDirection direction + ) { return DirectionalDistance(movingLines, 0, 0, stationaryLines, direction); } @@ -211,8 +266,12 @@ namespace OpenNest.Geometry /// by (movingDx, movingDy) without creating new Line objects. /// public static double DirectionalDistance( - List movingLines, double movingDx, double movingDy, - List stationaryLines, PushDirection direction) + List movingLines, + double movingDx, + double movingDy, + List stationaryLines, + PushDirection direction + ) { var minDist = double.MaxValue; var movingOffset = new Vector(movingDx, movingDy); @@ -226,7 +285,8 @@ namespace OpenNest.Geometry foreach (var mv in movingVertices) { var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } // Case 2: Each stationary vertex -> each moving edge (opposite direction) @@ -239,7 +299,8 @@ namespace OpenNest.Geometry foreach (var sv in stationaryVertices) { var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } return minDist; @@ -267,9 +328,12 @@ namespace OpenNest.Geometry /// to avoid all intermediate object allocations. /// public static double DirectionalDistance( - (Vector start, Vector end)[] movingEdges, Vector movingOffset, - (Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset, - PushDirection direction) + (Vector start, Vector end)[] movingEdges, + Vector movingOffset, + (Vector start, Vector end)[] stationaryEdges, + Vector stationaryOffset, + PushDirection direction + ) { var minDist = double.MaxValue; @@ -281,7 +345,8 @@ namespace OpenNest.Geometry foreach (var mv in movingVertices) { var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } // Case 2: Each stationary vertex -> each moving edge (opposite direction) @@ -293,15 +358,19 @@ namespace OpenNest.Geometry foreach (var sv in stationaryVertices) { var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } return minDist; } public static double OneWayDistance( - Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset, - PushDirection direction) + Vector vertex, + (Vector start, Vector end)[] edges, + Vector edgeOffset, + PushDirection direction + ) { var minDist = double.MaxValue; var vx = vertex.X; @@ -315,7 +384,9 @@ namespace OpenNest.Geometry var e1 = edges[i].start + edgeOffset; var e2 = edges[i].end + edgeOffset; - double perpValue, edgeMin, edgeMax; + double perpValue, + edgeMin, + edgeMax; if (horizontal) { perpValue = vy; @@ -337,7 +408,8 @@ namespace OpenNest.Geometry continue; var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } return minDist; @@ -347,11 +419,16 @@ namespace OpenNest.Geometry { switch (direction) { - case PushDirection.Left: return PushDirection.Right; - case PushDirection.Right: return PushDirection.Left; - case PushDirection.Up: return PushDirection.Down; - case PushDirection.Down: return PushDirection.Up; - default: return direction; + case PushDirection.Left: + return PushDirection.Right; + case PushDirection.Right: + return PushDirection.Left; + case PushDirection.Up: + return PushDirection.Down; + case PushDirection.Down: + return PushDirection.Up; + default: + return direction; } } @@ -364,11 +441,16 @@ namespace OpenNest.Geometry { switch (direction) { - case PushDirection.Left: return box.Left - boundary.Left; - case PushDirection.Right: return boundary.Right - box.Right; - case PushDirection.Up: return boundary.Top - box.Top; - case PushDirection.Down: return box.Bottom - boundary.Bottom; - default: return double.MaxValue; + case PushDirection.Left: + return box.Left - boundary.Left; + case PushDirection.Right: + return boundary.Right - box.Right; + case PushDirection.Up: + return boundary.Top - box.Top; + case PushDirection.Down: + return box.Bottom - boundary.Bottom; + default: + return double.MaxValue; } } @@ -376,11 +458,16 @@ namespace OpenNest.Geometry { switch (direction) { - case PushDirection.Left: return new Vector(-distance, 0); - case PushDirection.Right: return new Vector(distance, 0); - case PushDirection.Up: return new Vector(0, distance); - case PushDirection.Down: return new Vector(0, -distance); - default: return new Vector(); + case PushDirection.Left: + return new Vector(-distance, 0); + case PushDirection.Right: + return new Vector(distance, 0); + case PushDirection.Up: + return new Vector(0, distance); + case PushDirection.Down: + return new Vector(0, -distance); + default: + return new Vector(); } } @@ -388,11 +475,16 @@ namespace OpenNest.Geometry { switch (direction) { - case PushDirection.Left: return from.Left - to.Right; - case PushDirection.Right: return to.Left - from.Right; - case PushDirection.Up: return to.Bottom - from.Top; - case PushDirection.Down: return from.Bottom - to.Top; - default: return double.MaxValue; + case PushDirection.Left: + return from.Left - to.Right; + case PushDirection.Right: + return to.Left - from.Right; + case PushDirection.Up: + return to.Bottom - from.Top; + case PushDirection.Down: + return from.Bottom - to.Top; + default: + return double.MaxValue; } } @@ -409,23 +501,27 @@ namespace OpenNest.Geometry if (direction.X < -Tolerance.Epsilon) { var d = (box.Left - boundary.Left) / -direction.X; - if (d < dist) dist = d; + if (d < dist) + dist = d; } else if (direction.X > Tolerance.Epsilon) { var d = (boundary.Right - box.Right) / direction.X; - if (d < dist) dist = d; + if (d < dist) + dist = d; } if (direction.Y < -Tolerance.Epsilon) { var d = (box.Bottom - boundary.Bottom) / -direction.Y; - if (d < dist) dist = d; + if (d < dist) + dist = d; } else if (direction.Y > Tolerance.Epsilon) { var d = (boundary.Top - box.Top) / direction.Y; - if (d < dist) dist = d; + if (d < dist) + dist = d; } return dist < 0 ? 0 : dist; @@ -463,7 +559,11 @@ namespace OpenNest.Geometry /// Computes the minimum translation distance along an arbitrary unit direction /// before any edge of movingLines contacts any edge of stationaryLines. /// - public static double DirectionalDistance(List movingLines, List stationaryLines, Vector direction) + public static double DirectionalDistance( + List movingLines, + List stationaryLines, + Vector direction + ) { var minDist = double.MaxValue; var dirX = direction.X; @@ -476,8 +576,18 @@ namespace OpenNest.Geometry for (var i = 0; i < stationaryLines.Count; i++) { var e = stationaryLines[i]; - var d = RayEdgeDistance(mv.X, mv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, dirX, dirY); - if (d < minDist) minDist = d; + var d = RayEdgeDistance( + mv.X, + mv.Y, + e.pt1.X, + e.pt1.Y, + e.pt2.X, + e.pt2.Y, + dirX, + dirY + ); + if (d < minDist) + minDist = d; } } @@ -491,8 +601,18 @@ namespace OpenNest.Geometry for (var i = 0; i < movingLines.Count; i++) { var e = movingLines[i]; - var d = RayEdgeDistance(sv.X, sv.Y, e.pt1.X, e.pt1.Y, e.pt2.X, e.pt2.Y, oppX, oppY); - if (d < minDist) minDist = d; + var d = RayEdgeDistance( + sv.X, + sv.Y, + e.pt1.X, + e.pt1.Y, + e.pt2.X, + e.pt2.Y, + oppX, + oppY + ); + if (d < minDist) + minDist = d; } } @@ -505,9 +625,16 @@ namespace OpenNest.Geometry /// stationaryEntities. Delegates to the Vector-based overload. /// public static double DirectionalDistance( - List movingEntities, List stationaryEntities, PushDirection direction) + List movingEntities, + List stationaryEntities, + PushDirection direction + ) { - return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0)); + return DirectionalDistance( + movingEntities, + stationaryEntities, + DirectionToOffset(direction, 1.0) + ); } /// @@ -517,7 +644,10 @@ namespace OpenNest.Geometry /// without tessellation. /// public static double DirectionalDistance( - List movingEntities, List stationaryEntities, Vector direction) + List movingEntities, + List stationaryEntities, + Vector direction + ) { var minDist = double.MaxValue; var dirX = direction.X; @@ -536,7 +666,8 @@ namespace OpenNest.Geometry if (d < minDist) { minDist = d; - if (d <= 0) return 0; + if (d <= 0) + return 0; } } } @@ -557,7 +688,8 @@ namespace OpenNest.Geometry if (d < minDist) { minDist = d; - if (d <= 0) return 0; + if (d <= 0) + return 0; } } } @@ -566,10 +698,24 @@ namespace OpenNest.Geometry // Phases 1-2 sample arc endpoints and cardinal extremes, but the actual // closest point on a small corner arc to a straight edge may lie between // those samples. Use ClosestPointTo to find it and fire a ray from there. - minDist = ArcToLineClosestDistance(movingEntities, stationaryEntities, dirX, dirY, minDist); - if (minDist <= 0) return 0; - minDist = ArcToLineClosestDistance(stationaryEntities, movingEntities, oppX, oppY, minDist); - if (minDist <= 0) return 0; + minDist = ArcToLineClosestDistance( + movingEntities, + stationaryEntities, + dirX, + dirY, + minDist + ); + if (minDist <= 0) + return 0; + minDist = ArcToLineClosestDistance( + stationaryEntities, + movingEntities, + oppX, + oppY, + minDist + ); + if (minDist <= 0) + return 0; // Phase 4: Curve-to-curve direct distance. // The vertex-to-entity approach misses the closest contact between two @@ -605,20 +751,35 @@ namespace OpenNest.Geometry if (me is Arc mArc) { var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx)); - if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed)) + if ( + !Angle.IsBetweenRad( + angle, + mArc.StartAngle, + mArc.EndAngle, + mArc.IsReversed + ) + ) continue; } if (se is Arc sArc) { var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx)); - if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed)) + if ( + !Angle.IsBetweenRad( + angle, + sArc.StartAngle, + sArc.EndAngle, + sArc.IsReversed + ) + ) continue; } } minDist = d; - if (d <= 0) return 0; + if (d <= 0) + return 0; } } @@ -626,8 +787,12 @@ namespace OpenNest.Geometry } private static double ArcToLineClosestDistance( - List arcEntities, List lineEntities, - double dirX, double dirY, double minDist) + List arcEntities, + List lineEntities, + double dirX, + double dirY, + double minDist + ) { for (var i = 0; i < arcEntities.Count; i++) { @@ -662,15 +827,30 @@ namespace OpenNest.Geometry { var theta = k == 0 ? theta1 : theta2; - if (!Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed)) + if ( + !Angle.IsBetweenRad(theta, arc.StartAngle, arc.EndAngle, arc.IsReversed) + ) continue; var qx = cx + r * System.Math.Cos(theta); var qy = cy + r * System.Math.Sin(theta); - var d = RayEdgeDistance(qx, qy, p1x, p1y, line.pt2.X, line.pt2.Y, - dirX, dirY); - if (d < minDist) { minDist = d; if (d <= 0) return 0; } + var d = RayEdgeDistance( + qx, + qy, + p1x, + p1y, + line.pt2.X, + line.pt2.Y, + dirX, + dirY + ); + if (d < minDist) + { + minDist = d; + if (d <= 0) + return 0; + } } } } @@ -678,28 +858,54 @@ namespace OpenNest.Geometry } private static double RayEntityDistance( - double vx, double vy, Entity entity, double dirX, double dirY) + double vx, + double vy, + Entity entity, + double dirX, + double dirY + ) { if (entity is Line line) { - return RayEdgeDistance(vx, vy, - line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y, - dirX, dirY); + return RayEdgeDistance( + vx, + vy, + line.pt1.X, + line.pt1.Y, + line.pt2.X, + line.pt2.Y, + dirX, + dirY + ); } if (entity is Arc arc) { - return RayArcDistance(vx, vy, - arc.Center.X, arc.Center.Y, arc.Radius, - arc.StartAngle, arc.EndAngle, arc.IsReversed, - dirX, dirY); + return RayArcDistance( + vx, + vy, + arc.Center.X, + arc.Center.Y, + arc.Radius, + arc.StartAngle, + arc.EndAngle, + arc.IsReversed, + dirX, + dirY + ); } if (entity is Circle circle) { - return RayCircleDistance(vx, vy, - circle.Center.X, circle.Center.Y, circle.Radius, - dirX, dirY); + return RayCircleDistance( + vx, + vy, + circle.Center.X, + circle.Center.Y, + circle.Radius, + dirX, + dirY + ); } return double.MaxValue; @@ -759,7 +965,10 @@ namespace OpenNest.Geometry return CollectVertices(ToEdgeArray(lines), offset); } - private static HashSet CollectVertices((Vector start, Vector end)[] edges, Vector offset) + private static HashSet CollectVertices( + (Vector start, Vector end)[] edges, + Vector offset + ) { var vertices = new HashSet(); for (var i = 0; i < edges.Length; i++) @@ -778,26 +987,48 @@ namespace OpenNest.Geometry return edges; } - private static void SortEdgesForPruning((Vector start, Vector end)[] edges, PushDirection direction) + private static void SortEdgesForPruning( + (Vector start, Vector end)[] edges, + PushDirection direction + ) { if (direction == PushDirection.Left || direction == PushDirection.Right) - System.Array.Sort(edges, (a, b) => - System.Math.Min(a.start.Y, a.end.Y).CompareTo(System.Math.Min(b.start.Y, b.end.Y))); + System.Array.Sort( + edges, + (a, b) => + System + .Math.Min(a.start.Y, a.end.Y) + .CompareTo(System.Math.Min(b.start.Y, b.end.Y)) + ); else - System.Array.Sort(edges, (a, b) => - System.Math.Min(a.start.X, a.end.X).CompareTo(System.Math.Min(b.start.X, b.end.X))); + System.Array.Sort( + edges, + (a, b) => + System + .Math.Min(a.start.X, a.end.X) + .CompareTo(System.Math.Min(b.start.X, b.end.X)) + ); } - private static bool TryGetCurveParams(Entity entity, out double cx, out double cy, out double r) + private static bool TryGetCurveParams( + Entity entity, + out double cx, + out double cy, + out double r + ) { if (entity is Circle circle) { - cx = circle.Center.X; cy = circle.Center.Y; r = circle.Radius; + cx = circle.Center.X; + cy = circle.Center.Y; + r = circle.Radius; return true; } if (entity is Arc arc) { - cx = arc.Center.X; cy = arc.Center.Y; r = arc.Radius; + cx = arc.Center.X; + cy = arc.Center.Y; + r = arc.Radius; return true; } cx = cy = r = 0; @@ -850,7 +1081,13 @@ namespace OpenNest.Geometry return new Box(lft, btm, rgt - lft, top - btm); } - private static bool FindVerticalLimits(Vector pt, Box bounds, List boxes, out double top, out double btm) + private static bool FindVerticalLimits( + Vector pt, + Box bounds, + List boxes, + out double top, + out double btm + ) { top = double.MaxValue; btm = double.MinValue; @@ -868,20 +1105,30 @@ namespace OpenNest.Geometry if (top == double.MaxValue) { - if (bounds.Top > pt.Y) top = bounds.Top; - else return false; + if (bounds.Top > pt.Y) + top = bounds.Top; + else + return false; } if (btm == double.MinValue) { - if (bounds.Bottom < pt.Y) btm = bounds.Bottom; - else return false; + if (bounds.Bottom < pt.Y) + btm = bounds.Bottom; + else + return false; } return true; } - private static bool FindHorizontalLimits(Vector pt, Box bounds, List boxes, out double lft, out double rgt) + private static bool FindHorizontalLimits( + Vector pt, + Box bounds, + List boxes, + out double lft, + out double rgt + ) { lft = double.MinValue; rgt = double.MaxValue; @@ -899,14 +1146,18 @@ namespace OpenNest.Geometry if (rgt == double.MaxValue) { - if (bounds.Right > pt.X) rgt = bounds.Right; - else return false; + if (bounds.Right > pt.X) + rgt = bounds.Right; + else + return false; } if (lft == double.MinValue) { - if (bounds.Left < pt.X) lft = bounds.Left; - else return false; + if (bounds.Left < pt.X) + lft = bounds.Left; + else + return false; } return true; diff --git a/OpenNest.Core/Geometry/SplineConverter.cs b/OpenNest.Core/Geometry/SplineConverter.cs index f7e7c6e..6ab4eff 100644 --- a/OpenNest.Core/Geometry/SplineConverter.cs +++ b/OpenNest.Core/Geometry/SplineConverter.cs @@ -1,6 +1,6 @@ -using OpenNest.Math; using System; using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -8,7 +8,11 @@ namespace OpenNest.Geometry { private const int MinPointsForArc = 3; - public static List Convert(List points, bool isClosed, double tolerance = 0.001) + public static List Convert( + List points, + bool isClosed, + double tolerance = 0.001 + ) { if (points == null || points.Count < 2) return new List(); @@ -37,8 +41,12 @@ namespace OpenNest.Geometry return entities; } - private static ArcFitResult TryFitArc(List points, int start, - Vector chainedTangent, double tolerance) + private static ArcFitResult TryFitArc( + List points, + int start, + Vector chainedTangent, + double tolerance + ) { var minEnd = start + MinPointsForArc - 1; if (minEnd >= points.Count) @@ -83,7 +91,8 @@ namespace OpenNest.Geometry } private static (Vector center, double radius, double deviation) FitCircumscribed( - List points) + List points + ) { if (points.Count < 3) return (Vector.Invalid, 0, double.MaxValue); @@ -131,11 +140,16 @@ namespace OpenNest.Geometry } private static (Vector center, double radius, double deviation) FitWithStartTangent( - List points, Vector tangent) => - ArcFit.FitWithStartTangent(points, tangent); + List points, + Vector tangent + ) => ArcFit.FitWithStartTangent(points, tangent); - private static double MaxRadialDeviation(List points, double cx, double cy, double radius) => - ArcFit.MaxRadialDeviation(points, cx, cy, radius); + private static double MaxRadialDeviation( + List points, + double cx, + double cy, + double radius + ) => ArcFit.MaxRadialDeviation(points, cx, cy, radius); private static double SumSignedAngles(Vector center, List points) { @@ -145,8 +159,10 @@ namespace OpenNest.Geometry var a1 = System.Math.Atan2(points[i].Y - center.Y, points[i].X - center.X); var a2 = System.Math.Atan2(points[i + 1].Y - center.Y, points[i + 1].X - center.X); var da = a2 - a1; - while (da > System.Math.PI) da -= Angle.TwoPI; - while (da < -System.Math.PI) da += Angle.TwoPI; + while (da > System.Math.PI) + da -= Angle.TwoPI; + while (da < -System.Math.PI) + da += Angle.TwoPI; total += da; } return total; @@ -160,9 +176,7 @@ namespace OpenNest.Geometry var rx = lastPt.X - center.X; var ry = lastPt.Y - center.Y; - return totalAngle >= 0 - ? new Vector(-ry, rx) - : new Vector(ry, -rx); + return totalAngle >= 0 ? new Vector(-ry, rx) : new Vector(ry, -rx); } private static Arc CreateArc(Vector center, double radius, List points) @@ -174,8 +188,10 @@ namespace OpenNest.Geometry var endAngle = System.Math.Atan2(lastPoint.Y - center.Y, lastPoint.X - center.X); var isReversed = SumSignedAngles(center, points) < 0; - if (startAngle < 0) startAngle += Angle.TwoPI; - if (endAngle < 0) endAngle += Angle.TwoPI; + if (startAngle < 0) + startAngle += Angle.TwoPI; + if (endAngle < 0) + endAngle += Angle.TwoPI; return new Arc(center, radius, startAngle, endAngle, isReversed); } diff --git a/OpenNest.Core/Geometry/Vector.cs b/OpenNest.Core/Geometry/Vector.cs index ebae254..285a7fc 100644 --- a/OpenNest.Core/Geometry/Vector.cs +++ b/OpenNest.Core/Geometry/Vector.cs @@ -1,5 +1,5 @@ -using OpenNest.Math; -using System; +using System; +using OpenNest.Math; namespace OpenNest.Geometry { @@ -31,7 +31,7 @@ namespace OpenNest.Geometry { unchecked { - // Use a simple but effective hash combine. + // Use a simple but effective hash combine. // We use a small epsilon-safe rounding if needed, but for uniqueness in HashSet // during a single operation, raw bits or slightly rounded is usually fine. // However, IsEqualTo uses Tolerance.Epsilon, so we should probably round to some precision. diff --git a/OpenNest.Core/Material.cs b/OpenNest.Core/Material.cs index c033d2b..15b2c5b 100644 --- a/OpenNest.Core/Material.cs +++ b/OpenNest.Core/Material.cs @@ -2,9 +2,7 @@ { public class Material { - public Material() - { - } + public Material() { } public Material(string name) { diff --git a/OpenNest.Core/Math/Angle.cs b/OpenNest.Core/Math/Angle.cs index 7b2aaea..82708f2 100644 --- a/OpenNest.Core/Math/Angle.cs +++ b/OpenNest.Core/Math/Angle.cs @@ -90,8 +90,7 @@ a1 = Angle.NormalizeRad(angle - a1); a2 = Angle.NormalizeRad(a2 - angle); - return diff >= a1 - Tolerance.Epsilon || - diff >= a2 - Tolerance.Epsilon; + return diff >= a1 - Tolerance.Epsilon || diff >= a2 - Tolerance.Epsilon; } /// @@ -116,8 +115,7 @@ a1 = Angle.NormalizeRad(angle - a1); a2 = Angle.NormalizeRad(a2 - angle); - return diff >= a1 - Tolerance.Epsilon || - diff >= a2 - Tolerance.Epsilon; + return diff >= a1 - Tolerance.Epsilon || diff >= a2 - Tolerance.Epsilon; } } } diff --git a/OpenNest.Core/Math/ExpressionEvaluator.cs b/OpenNest.Core/Math/ExpressionEvaluator.cs index 591aa28..738f27d 100644 --- a/OpenNest.Core/Math/ExpressionEvaluator.cs +++ b/OpenNest.Core/Math/ExpressionEvaluator.cs @@ -10,13 +10,18 @@ namespace OpenNest.Math /// public static class ExpressionEvaluator { - public static double Evaluate(string expression, IReadOnlyDictionary variables) + public static double Evaluate( + string expression, + IReadOnlyDictionary variables + ) { var parser = new Parser(expression, variables); var result = parser.ParseExpression(); parser.SkipWhitespace(); if (!parser.IsEnd) - throw new FormatException($"Unexpected character at position {parser.Position}: '{parser.Current}'"); + throw new FormatException( + $"Unexpected character at position {parser.Position}: '{parser.Current}'" + ); return result; } @@ -52,10 +57,12 @@ namespace OpenNest.Math while (true) { SkipWhitespace(); - if (IsEnd) break; + if (IsEnd) + break; var op = Current; - if (op != '+' && op != '-') break; + if (op != '+' && op != '-') + break; _pos++; SkipWhitespace(); @@ -75,10 +82,12 @@ namespace OpenNest.Math while (true) { SkipWhitespace(); - if (IsEnd) break; + if (IsEnd) + break; var op = Current; - if (op != '*' && op != '/') break; + if (op != '*' && op != '/') + break; _pos++; SkipWhitespace(); @@ -129,7 +138,10 @@ namespace OpenNest.Math { _pos++; // consume '$' var start = _pos; - while (_pos < _input.Length && (char.IsLetterOrDigit(_input[_pos]) || _input[_pos] == '_')) + while ( + _pos < _input.Length + && (char.IsLetterOrDigit(_input[_pos]) || _input[_pos] == '_') + ) _pos++; if (_pos == start) throw new FormatException("Expected variable name after '$'."); @@ -145,10 +157,19 @@ namespace OpenNest.Math _pos++; if (_pos == numStart) - throw new FormatException($"Unexpected character '{Current}' at position {_pos}."); + throw new FormatException( + $"Unexpected character '{Current}' at position {_pos}." + ); var numSpan = _input.Slice(numStart, _pos - numStart).ToString(); - if (!double.TryParse(numSpan, NumberStyles.Float, CultureInfo.InvariantCulture, out var number)) + if ( + !double.TryParse( + numSpan, + NumberStyles.Float, + CultureInfo.InvariantCulture, + out var number + ) + ) throw new FormatException($"Invalid number: '{numSpan}'"); return number; diff --git a/OpenNest.Core/Math/Fraction.cs b/OpenNest.Core/Math/Fraction.cs index a369a15..be192f9 100644 --- a/OpenNest.Core/Math/Fraction.cs +++ b/OpenNest.Core/Math/Fraction.cs @@ -7,8 +7,9 @@ namespace OpenNest.Math { public static class Fraction { - public static readonly Regex FractionRegex = - new Regex(@"((?\d+)(\ |-))?(?\d+\/\d+)"); + public static readonly Regex FractionRegex = new Regex( + @"((?\d+)(\ |-))?(?\d+\/\d+)" + ); public static bool IsValid(string s) { @@ -59,7 +60,8 @@ namespace OpenNest.Math { var sb = new StringBuilder(input); - var fractionMatches = FractionRegex.Matches(sb.ToString()) + var fractionMatches = FractionRegex + .Matches(sb.ToString()) .Cast() .OrderByDescending(m => m.Index); diff --git a/OpenNest.Core/Nest.cs b/OpenNest.Core/Nest.cs index a933f53..cee1483 100644 --- a/OpenNest.Core/Nest.cs +++ b/OpenNest.Core/Nest.cs @@ -1,7 +1,7 @@ -using OpenNest.Collections; -using OpenNest.Geometry; -using System; +using System; using System.Collections.Generic; +using OpenNest.Collections; +using OpenNest.Geometry; namespace OpenNest { @@ -11,9 +11,7 @@ namespace OpenNest public DrawingCollection Drawings; public Nest() - : this(string.Empty) - { - } + : this(string.Empty) { } public Nest(string name) { @@ -128,7 +126,7 @@ namespace OpenNest EdgeSpacing = EdgeSpacing, PartSpacing = PartSpacing, Quadrant = Quadrant, - Quantity = 1 + Quantity = 1, }; } } diff --git a/OpenNest.Core/OffsetSide.cs b/OpenNest.Core/OffsetSide.cs index fb86c1a..28a03e7 100644 --- a/OpenNest.Core/OffsetSide.cs +++ b/OpenNest.Core/OffsetSide.cs @@ -1,9 +1,8 @@ - -namespace OpenNest +namespace OpenNest { public enum OffsetSide { Left, - Right + Right, } } diff --git a/OpenNest.Core/Part.cs b/OpenNest.Core/Part.cs index 19024b4..312c83c 100644 --- a/OpenNest.Core/Part.cs +++ b/OpenNest.Core/Part.cs @@ -1,9 +1,9 @@ -using OpenNest.CNC; +using System.Collections.Generic; +using System.Linq; +using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; -using System.Linq; namespace OpenNest { @@ -27,9 +27,7 @@ namespace OpenNest public readonly Drawing BaseDrawing; public Part(Drawing baseDrawing) - : this(baseDrawing, new Vector()) - { - } + : this(baseDrawing, new Vector()) { } public Part(Drawing baseDrawing, Vector location) { @@ -61,15 +59,25 @@ namespace OpenNest public CNC.CuttingStrategy.CuttingParameters CuttingParameters { get; set; } - public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint) + public void ApplyLeadIns( + CNC.CuttingStrategy.CuttingParameters parameters, + Vector approachPoint + ) { ApplyLeadIns(parameters, approachPoint, Geometry.Vector.Invalid); } - public void ApplyLeadIns(CNC.CuttingStrategy.CuttingParameters parameters, Vector approachPoint, Vector nextPartStart) + public void ApplyLeadIns( + CNC.CuttingStrategy.CuttingParameters parameters, + Vector approachPoint, + Vector nextPartStart + ) { preLeadInRotation = Rotation; - var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters }; + var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy + { + Parameters = parameters, + }; var result = strategy.Apply(Program, approachPoint, nextPartStart); Program = result.Program; CuttingParameters = parameters; @@ -77,11 +85,18 @@ namespace OpenNest UpdateBounds(); } - public void ApplySingleLeadIn(CNC.CuttingStrategy.CuttingParameters parameters, - Geometry.Vector point, Geometry.Entity entity, CNC.CuttingStrategy.ContourType contourType) + public void ApplySingleLeadIn( + CNC.CuttingStrategy.CuttingParameters parameters, + Geometry.Vector point, + Geometry.Entity entity, + CNC.CuttingStrategy.ContourType contourType + ) { preLeadInRotation = Rotation; - var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy { Parameters = parameters }; + var strategy = new CNC.CuttingStrategy.ContourCuttingStrategy + { + Parameters = parameters, + }; var result = strategy.ApplySingle(Program, point, entity, contourType); Program = result.Program; CuttingParameters = parameters; @@ -214,10 +229,12 @@ namespace OpenNest { pts = new List(); - var entities1 = ConvertProgram.ToGeometry(Program) + var entities1 = ConvertProgram + .ToGeometry(Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); - var entities2 = ConvertProgram.ToGeometry(part.Program) + var entities2 = ConvertProgram + .ToGeometry(part.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); @@ -286,10 +303,17 @@ namespace OpenNest { // Share the Program instance — offset-only copies don't modify the program codes. // This is a major performance win for tiling large patterns. - var part = new Part(BaseDrawing, Program, + var part = new Part( + BaseDrawing, + Program, location + offset, - new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y, - BoundingBox.Length, BoundingBox.Width)); + new Box( + BoundingBox.X + offset.X, + BoundingBox.Y + offset.Y, + BoundingBox.Length, + BoundingBox.Width + ) + ); return part; } diff --git a/OpenNest.Core/PartGeometry.cs b/OpenNest.Core/PartGeometry.cs index 573d641..a0b0a69 100644 --- a/OpenNest.Core/PartGeometry.cs +++ b/OpenNest.Core/PartGeometry.cs @@ -1,7 +1,7 @@ -using OpenNest.Converters; -using OpenNest.Geometry; using System.Collections.Generic; using System.Linq; +using OpenNest.Converters; +using OpenNest.Geometry; namespace OpenNest { @@ -10,7 +10,9 @@ namespace OpenNest public static List GetPartLines(Part part, double chordTolerance = 0.001) { var entities = ConvertProgram.ToGeometry(part.Program); - var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var shapes = ShapeBuilder.GetShapes( + entities.Where(e => e.Layer != SpecialLayers.Rapid) + ); var lines = new List(); foreach (var shape in shapes) @@ -23,10 +25,16 @@ namespace OpenNest return lines; } - public static List GetPartLines(Part part, PushDirection facingDirection, double chordTolerance = 0.001) + public static List GetPartLines( + Part part, + PushDirection facingDirection, + double chordTolerance = 0.001 + ) { var entities = ConvertProgram.ToGeometry(part.Program); - var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var shapes = ShapeBuilder.GetShapes( + entities.Where(e => e.Layer != SpecialLayers.Rapid) + ); var lines = new List(); foreach (var shape in shapes) @@ -47,7 +55,8 @@ namespace OpenNest { var geoEntities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( - geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); var offsetShape = profile.Perimeter.OffsetOutward(spacing); if (offsetShape == null) @@ -69,7 +78,8 @@ namespace OpenNest { var geoEntities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( - geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); var entities = new List(); var perimeter = profile.Perimeter.OffsetOutward(spacing); @@ -83,7 +93,8 @@ namespace OpenNest foreach (var cutout in profile.Cutouts) { var inset = cutout.OffsetInward(spacing); - if (inset == null) continue; + if (inset == null) + continue; foreach (var entity in inset.Entities) entity.Offset(part.Location); entities.AddRange(inset.Entities); @@ -100,7 +111,8 @@ namespace OpenNest { var geoEntities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( - geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location); } @@ -113,7 +125,8 @@ namespace OpenNest { var geoEntities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( - geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location); foreach (var cutout in profile.Cutouts) @@ -136,50 +149,85 @@ namespace OpenNest return result; } - public static List GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001, - bool perimeterOnly = false) + public static List GetOffsetPartLines( + Part part, + double spacing, + double chordTolerance = 0.001, + bool perimeterOnly = false + ) { var entities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( - entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); var lines = new List(); var totalSpacing = spacing; - AddOffsetLines(lines, profile.Perimeter.OffsetOutward(totalSpacing), - chordTolerance, part.Location); + AddOffsetLines( + lines, + profile.Perimeter.OffsetOutward(totalSpacing), + chordTolerance, + part.Location + ); if (!perimeterOnly) { foreach (var cutout in profile.Cutouts) - AddOffsetLines(lines, cutout.OffsetInward(totalSpacing), - chordTolerance, part.Location); + AddOffsetLines( + lines, + cutout.OffsetInward(totalSpacing), + chordTolerance, + part.Location + ); } return lines; } - public static List GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001) + public static List GetOffsetPartLines( + Part part, + double spacing, + PushDirection facingDirection, + double chordTolerance = 0.001 + ) { var entities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( - entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); var lines = new List(); var totalSpacing = spacing; - AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing), - chordTolerance, part.Location, facingDirection); + AddOffsetDirectionalLines( + lines, + profile.Perimeter.OffsetOutward(totalSpacing), + chordTolerance, + part.Location, + facingDirection + ); foreach (var cutout in profile.Cutouts) - AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing), - chordTolerance, part.Location, facingDirection); + AddOffsetDirectionalLines( + lines, + cutout.OffsetInward(totalSpacing), + chordTolerance, + part.Location, + facingDirection + ); return lines; } - public static List GetPartLines(Part part, Vector facingDirection, double chordTolerance = 0.001) + public static List GetPartLines( + Part part, + Vector facingDirection, + double chordTolerance = 0.001 + ) { var entities = ConvertProgram.ToGeometry(part.Program); - var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid)); + var shapes = ShapeBuilder.GetShapes( + entities.Where(e => e.Layer != SpecialLayers.Rapid) + ); var lines = new List(); foreach (var shape in shapes) @@ -192,20 +240,36 @@ namespace OpenNest return lines; } - public static List GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001) + public static List GetOffsetPartLines( + Part part, + double spacing, + Vector facingDirection, + double chordTolerance = 0.001 + ) { var entities = ConvertProgram.ToGeometry(part.Program); var profile = new ShapeProfile( - entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); var lines = new List(); var totalSpacing = spacing; - AddOffsetDirectionalLines(lines, profile.Perimeter.OffsetOutward(totalSpacing), - chordTolerance, part.Location, facingDirection); + AddOffsetDirectionalLines( + lines, + profile.Perimeter.OffsetOutward(totalSpacing), + chordTolerance, + part.Location, + facingDirection + ); foreach (var cutout in profile.Cutouts) - AddOffsetDirectionalLines(lines, cutout.OffsetInward(totalSpacing), - chordTolerance, part.Location, facingDirection); + AddOffsetDirectionalLines( + lines, + cutout.OffsetInward(totalSpacing), + chordTolerance, + part.Location, + facingDirection + ); return lines; } @@ -242,7 +306,10 @@ namespace OpenNest /// /// Returns only polygon edges whose outward normal faces the specified direction. /// - private static List GetDirectionalLines(Polygon polygon, PushDirection facingDirection) + private static List GetDirectionalLines( + Polygon polygon, + PushDirection facingDirection + ) { if (polygon.Vertices.Count < 3) return polygon.ToLines(); @@ -261,11 +328,21 @@ namespace OpenNest switch (facingDirection) { - case PushDirection.Left: keep = -sign * dy > 0; break; - case PushDirection.Right: keep = sign * dy > 0; break; - case PushDirection.Up: keep = -sign * dx > 0; break; - case PushDirection.Down: keep = sign * dx > 0; break; - default: keep = true; break; + case PushDirection.Left: + keep = -sign * dy > 0; + break; + case PushDirection.Right: + keep = sign * dy > 0; + break; + case PushDirection.Up: + keep = -sign * dx > 0; + break; + case PushDirection.Down: + keep = sign * dx > 0; + break; + default: + keep = true; + break; } if (keep) @@ -277,8 +354,12 @@ namespace OpenNest return lines; } - private static void AddOffsetLines(List lines, Shape offsetEntity, - double chordTolerance, Vector location) + private static void AddOffsetLines( + List lines, + Shape offsetEntity, + double chordTolerance, + Vector location + ) { if (offsetEntity == null) return; @@ -289,8 +370,13 @@ namespace OpenNest lines.AddRange(polygon.ToLines()); } - private static void AddOffsetDirectionalLines(List lines, Shape offsetEntity, - double chordTolerance, Vector location, PushDirection facingDirection) + private static void AddOffsetDirectionalLines( + List lines, + Shape offsetEntity, + double chordTolerance, + Vector location, + PushDirection facingDirection + ) { if (offsetEntity == null) return; @@ -301,8 +387,13 @@ namespace OpenNest lines.AddRange(GetDirectionalLines(polygon, facingDirection)); } - private static void AddOffsetDirectionalLines(List lines, Shape offsetEntity, - double chordTolerance, Vector location, Vector facingDirection) + private static void AddOffsetDirectionalLines( + List lines, + Shape offsetEntity, + double chordTolerance, + Vector location, + Vector facingDirection + ) { if (offsetEntity == null) return; diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index e5a3714..1b6c7f4 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -1,10 +1,10 @@ -using OpenNest.Collections; +using System; +using System.Collections.Generic; +using System.Linq; +using OpenNest.Collections; using OpenNest.Geometry; using OpenNest.Math; using OpenNest.Shapes; -using System; -using System.Collections.Generic; -using System.Linq; namespace OpenNest { @@ -31,14 +31,10 @@ namespace OpenNest } public Plate() - : this(60, 120) - { - } + : this(60, 120) { } public Plate(double width, double length) - : this(new Size(width, length)) - { - } + : this(new Size(width, length)) { } public Plate(Size size) { @@ -140,7 +136,8 @@ namespace OpenNest Geometry.Entity perimeter = null; try { - var entities = Converters.ConvertProgram.ToGeometry(part.Program) + var entities = Converters + .ConvertProgram.ToGeometry(part.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); @@ -413,7 +410,10 @@ namespace OpenNest } foreach (var cutoff in CutOffs) - cutoff.Position = new Vector(cutoff.Position.X + voffset.X, cutoff.Position.Y + voffset.Y); + cutoff.Position = new Vector( + cutoff.Position.X + voffset.X, + cutoff.Position.Y + voffset.Y + ); } /// @@ -461,21 +461,19 @@ namespace OpenNest var boundingBox = new Box(); var partsBox = Parts.GetBoundingBox(); - boundingBox.X = partsBox.Left < plateBox.Left - ? partsBox.Left - : plateBox.Left; + boundingBox.X = partsBox.Left < plateBox.Left ? partsBox.Left : plateBox.Left; - boundingBox.Y = partsBox.Bottom < plateBox.Bottom - ? partsBox.Bottom - : plateBox.Bottom; + boundingBox.Y = partsBox.Bottom < plateBox.Bottom ? partsBox.Bottom : plateBox.Bottom; - boundingBox.Length = partsBox.Right > plateBox.Right - ? partsBox.Right - boundingBox.X - : plateBox.Right - boundingBox.X; + boundingBox.Length = + partsBox.Right > plateBox.Right + ? partsBox.Right - boundingBox.X + : plateBox.Right - boundingBox.X; - boundingBox.Width = partsBox.Top > plateBox.Top - ? partsBox.Top - boundingBox.Y - : plateBox.Top - boundingBox.Y; + boundingBox.Width = + partsBox.Top > plateBox.Top + ? partsBox.Top - boundingBox.Y + : plateBox.Top - boundingBox.Y; return boundingBox; } @@ -546,7 +544,8 @@ namespace OpenNest Size = new Size( Rounding.RoundUpToNearest(yExtent, roundingFactor), - Rounding.RoundUpToNearest(xExtent, roundingFactor)); + Rounding.RoundUpToNearest(xExtent, roundingFactor) + ); } /// @@ -601,9 +600,9 @@ namespace OpenNest // Plate convention: Length = X axis, Width = Y axis. if (xExtent >= yExtent) - Size = new Size(result.Width, result.Length); // X is the long axis + Size = new Size(result.Width, result.Length); // X is the long axis else - Size = new Size(result.Length, result.Width); // Y is the long axis + Size = new Size(result.Length, result.Width); // Y is the long axis return result; } @@ -639,7 +638,8 @@ namespace OpenNest /// Returns a number between 0.0 and 1.0 public double Utilization() { - return Parts.Where(p => !p.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area) / Area(); + return Parts.Where(p => !p.BaseDrawing.IsCutOff).Sum(part => part.BaseDrawing.Area) + / Area(); } public bool HasOverlappingParts(out List pts) @@ -661,10 +661,10 @@ namespace OpenNest // Floating-point rounding can produce sub-epsilon overlaps for // parts that are merely edge-touching, so require the overlap // region to exceed Epsilon in both dimensions. - var overlapX = System.Math.Min(b1.Right, b2.Right) - - System.Math.Max(b1.Left, b2.Left); - var overlapY = System.Math.Min(b1.Top, b2.Top) - - System.Math.Max(b1.Bottom, b2.Bottom); + var overlapX = + System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left); + var overlapY = + System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom); if (overlapX <= Math.Tolerance.Epsilon || overlapY <= Math.Tolerance.Epsilon) continue; @@ -676,6 +676,5 @@ namespace OpenNest return pts.Count > 0; } - } } diff --git a/OpenNest.Core/PlateManager.cs b/OpenNest.Core/PlateManager.cs index b5e1701..01d4cd8 100644 --- a/OpenNest.Core/PlateManager.cs +++ b/OpenNest.Core/PlateManager.cs @@ -1,5 +1,5 @@ -using OpenNest.Collections; using System; +using OpenNest.Collections; namespace OpenNest { @@ -44,7 +44,8 @@ namespace OpenNest public bool IsLast => CurrentIndex + 1 >= Count; - public bool CanRemoveCurrent => Count > 1 && CurrentPlate != null && CurrentPlate.Parts.Count > 0; + public bool CanRemoveCurrent => + Count > 1 && CurrentPlate != null && CurrentPlate.Parts.Count > 0; public void LoadFirst() { @@ -101,9 +102,11 @@ namespace OpenNest if (Count == 0 || nest.Plates[^1].Parts.Count > 0) nest.CreatePlate(); - while (Count > 1 + while ( + Count > 1 && nest.Plates[^1].Parts.Count == 0 - && nest.Plates[^2].Parts.Count == 0) + && nest.Plates[^2].Parts.Count == 0 + ) { nest.Plates.RemoveAt(Count - 1); } @@ -226,7 +229,10 @@ namespace OpenNest private void FireCurrentPlateChanged() { - CurrentPlateChanged?.Invoke(this, new PlateChangedEventArgs(CurrentPlate, CurrentIndex)); + CurrentPlateChanged?.Invoke( + this, + new PlateChangedEventArgs(CurrentPlate, CurrentIndex) + ); } public void Dispose() diff --git a/OpenNest.Core/PushDirection.cs b/OpenNest.Core/PushDirection.cs index 82cd27f..8b78243 100644 --- a/OpenNest.Core/PushDirection.cs +++ b/OpenNest.Core/PushDirection.cs @@ -1,4 +1,3 @@ - namespace OpenNest { public enum PushDirection @@ -6,6 +5,6 @@ namespace OpenNest Up, Down, Left, - Right + Right, } } diff --git a/OpenNest.Core/RelativePosition.cs b/OpenNest.Core/RelativePosition.cs index 7982cda..2702dc5 100644 --- a/OpenNest.Core/RelativePosition.cs +++ b/OpenNest.Core/RelativePosition.cs @@ -1,5 +1,4 @@ - -namespace OpenNest +namespace OpenNest { public enum RelativePosition { @@ -8,6 +7,6 @@ namespace OpenNest Right, Top, Bottom, - None + None, } } diff --git a/OpenNest.Core/RotationType.cs b/OpenNest.Core/RotationType.cs index 03c106f..b3647be 100644 --- a/OpenNest.Core/RotationType.cs +++ b/OpenNest.Core/RotationType.cs @@ -1,5 +1,4 @@ - -namespace OpenNest +namespace OpenNest { public enum RotationType { @@ -11,6 +10,6 @@ namespace OpenNest /// /// Counter-Clockwise /// - CCW + CCW, } } diff --git a/OpenNest.Core/Sequence.cs b/OpenNest.Core/Sequence.cs index f45e5d6..2b1af8e 100644 --- a/OpenNest.Core/Sequence.cs +++ b/OpenNest.Core/Sequence.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest { @@ -28,7 +28,14 @@ namespace OpenNest seqList.Add(lastPart); dupList.Remove(lastPart); - for (int i = 0; i < parts.Count - 1 /*STOP BEFORE LAST PART*/; i++) + for ( + int i = 0; + i + < parts.Count + - 1 /*STOP BEFORE LAST PART*/ + ; + i++ + ) { var nextPart = GetClosestPart(lastPart.Location, dupList); diff --git a/OpenNest.Core/Shapes/CircleShape.cs b/OpenNest.Core/Shapes/CircleShape.cs index 89fe138..4fe286a 100644 --- a/OpenNest.Core/Shapes/CircleShape.cs +++ b/OpenNest.Core/Shapes/CircleShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -16,10 +16,7 @@ namespace OpenNest.Shapes public override Drawing GetDrawing() { - var entities = new List - { - new Circle(0, 0, Diameter / 2.0) - }; + var entities = new List { new Circle(0, 0, Diameter / 2.0) }; return CreateDrawing(entities); } diff --git a/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs b/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs index 5771830..b317479 100644 --- a/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs +++ b/OpenNest.Core/Shapes/IsoscelesTriangleShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -24,7 +24,7 @@ namespace OpenNest.Shapes { new Line(0, 0, Base, 0), new Line(Base, 0, midX, Height), - new Line(midX, Height, 0, 0) + new Line(midX, Height, 0, 0), }; return CreateDrawing(entities); diff --git a/OpenNest.Core/Shapes/LShape.cs b/OpenNest.Core/Shapes/LShape.cs index 9e54ffb..e35c8d7 100644 --- a/OpenNest.Core/Shapes/LShape.cs +++ b/OpenNest.Core/Shapes/LShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -32,7 +32,7 @@ namespace OpenNest.Shapes new Line(Width, lh, lw, lh), new Line(lw, lh, lw, Height), new Line(lw, Height, 0, Height), - new Line(0, Height, 0, 0) + new Line(0, Height, 0, 0), }; return CreateDrawing(entities); diff --git a/OpenNest.Core/Shapes/NgonShape.cs b/OpenNest.Core/Shapes/NgonShape.cs index 55238c1..96653c2 100644 --- a/OpenNest.Core/Shapes/NgonShape.cs +++ b/OpenNest.Core/Shapes/NgonShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -30,7 +30,8 @@ namespace OpenNest.Shapes var angle = start + i * step; vertices[i] = new Vector( center + circumRadius * System.Math.Cos(angle), - center + circumRadius * System.Math.Sin(angle)); + center + circumRadius * System.Math.Sin(angle) + ); } var entities = new List(); diff --git a/OpenNest.Core/Shapes/PipeFlangeShape.cs b/OpenNest.Core/Shapes/PipeFlangeShape.cs index 980fd79..1db16bc 100644 --- a/OpenNest.Core/Shapes/PipeFlangeShape.cs +++ b/OpenNest.Core/Shapes/PipeFlangeShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -50,7 +50,11 @@ namespace OpenNest.Shapes entities.Add(new Circle(cx, cy, holeRadius)); } - if (!Blind && !string.IsNullOrEmpty(PipeSize) && PipeSizes.TryGetOD(PipeSize, out var pipeOD)) + if ( + !Blind + && !string.IsNullOrEmpty(PipeSize) + && PipeSizes.TryGetOD(PipeSize, out var pipeOD) + ) { var boreDiameter = pipeOD + PipeClearance; entities.Add(new Circle(0, 0, boreDiameter / 2.0)); diff --git a/OpenNest.Core/Shapes/PipeSizes.cs b/OpenNest.Core/Shapes/PipeSizes.cs index b8ac4c5..23e9785 100644 --- a/OpenNest.Core/Shapes/PipeSizes.cs +++ b/OpenNest.Core/Shapes/PipeSizes.cs @@ -6,44 +6,45 @@ namespace OpenNest.Shapes { public readonly record struct Entry(string Label, double OuterDiameter); - public static IReadOnlyList All { get; } = new[] - { - new Entry("1/8", 0.405), - new Entry("1/4", 0.540), - new Entry("3/8", 0.675), - new Entry("1/2", 0.840), - new Entry("3/4", 1.050), - new Entry("1", 1.315), - new Entry("1 1/4", 1.660), - new Entry("1 1/2", 1.900), - new Entry("2", 2.375), - new Entry("2 1/2", 2.875), - new Entry("3", 3.500), - new Entry("3 1/2", 4.000), - new Entry("4", 4.500), - new Entry("4 1/2", 5.000), - new Entry("5", 5.563), - new Entry("6", 6.625), - new Entry("7", 7.625), - new Entry("8", 8.625), - new Entry("9", 9.625), - new Entry("10", 10.750), - new Entry("11", 11.750), - new Entry("12", 12.750), - new Entry("14", 14.000), - new Entry("16", 16.000), - new Entry("18", 18.000), - new Entry("20", 20.000), - new Entry("24", 24.000), - new Entry("26", 26.000), - new Entry("28", 28.000), - new Entry("30", 30.000), - new Entry("32", 32.000), - new Entry("34", 34.000), - new Entry("36", 36.000), - new Entry("42", 42.000), - new Entry("48", 48.000), - }; + public static IReadOnlyList All { get; } = + new[] + { + new Entry("1/8", 0.405), + new Entry("1/4", 0.540), + new Entry("3/8", 0.675), + new Entry("1/2", 0.840), + new Entry("3/4", 1.050), + new Entry("1", 1.315), + new Entry("1 1/4", 1.660), + new Entry("1 1/2", 1.900), + new Entry("2", 2.375), + new Entry("2 1/2", 2.875), + new Entry("3", 3.500), + new Entry("3 1/2", 4.000), + new Entry("4", 4.500), + new Entry("4 1/2", 5.000), + new Entry("5", 5.563), + new Entry("6", 6.625), + new Entry("7", 7.625), + new Entry("8", 8.625), + new Entry("9", 9.625), + new Entry("10", 10.750), + new Entry("11", 11.750), + new Entry("12", 12.750), + new Entry("14", 14.000), + new Entry("16", 16.000), + new Entry("18", 18.000), + new Entry("20", 20.000), + new Entry("24", 24.000), + new Entry("26", 26.000), + new Entry("28", 28.000), + new Entry("30", 30.000), + new Entry("32", 32.000), + new Entry("34", 34.000), + new Entry("36", 36.000), + new Entry("42", 42.000), + new Entry("48", 48.000), + }; public static bool TryGetOD(string label, out double outerDiameter) { diff --git a/OpenNest.Core/Shapes/PlateSizes.cs b/OpenNest.Core/Shapes/PlateSizes.cs index 08af25d..d8e1cbb 100644 --- a/OpenNest.Core/Shapes/PlateSizes.cs +++ b/OpenNest.Core/Shapes/PlateSizes.cs @@ -29,17 +29,18 @@ namespace OpenNest.Shapes /// Standard mill sheet sizes (inches), sorted by area ascending. /// Canonical orientation: Width <= Length. /// - public static IReadOnlyList All { get; } = new[] - { - new Entry("48x96", 48, 96), // 4608 - new Entry("48x120", 48, 120), // 5760 - new Entry("48x144", 48, 144), // 6912 - new Entry("60x120", 60, 120), // 7200 - new Entry("60x144", 60, 144), // 8640 - new Entry("72x120", 72, 120), // 8640 - new Entry("72x144", 72, 144), // 10368 - new Entry("96x240", 96, 240), // 23040 - }; + public static IReadOnlyList All { get; } = + new[] + { + new Entry("48x96", 48, 96), // 4608 + new Entry("48x120", 48, 120), // 5760 + new Entry("48x144", 48, 144), // 6912 + new Entry("60x120", 60, 120), // 7200 + new Entry("60x144", 60, 144), // 8640 + new Entry("72x120", 72, 120), // 8640 + new Entry("72x144", 72, 144), // 10368 + new Entry("96x240", 96, 240), // 23040 + }; /// /// Looks up a standard size by label. Case-insensitive. @@ -77,7 +78,10 @@ namespace OpenNest.Shapes /// /// Recommends a plate size for the envelope of the given boxes. /// - public static PlateSizeResult Recommend(IEnumerable boxes, PlateSizeOptions options = null) + public static PlateSizeResult Recommend( + IEnumerable boxes, + PlateSizeOptions options = null + ) { if (boxes == null) throw new ArgumentNullException(nameof(boxes)); @@ -91,10 +95,14 @@ namespace OpenNest.Shapes foreach (var box in boxes) { hasAny = true; - if (box.Left < minX) minX = box.Left; - if (box.Bottom < minY) minY = box.Bottom; - if (box.Right > maxX) maxX = box.Right; - if (box.Top > maxY) maxY = box.Top; + if (box.Left < minX) + minX = box.Left; + if (box.Bottom < minY) + minY = box.Bottom; + if (box.Right > maxX) + maxX = box.Right; + if (box.Top > maxY) + maxY = box.Top; } if (!hasAny) @@ -109,7 +117,11 @@ namespace OpenNest.Shapes /// Recommends a plate size for a (width, length) pair. /// Inputs are treated as orientation-independent. /// - public static PlateSizeResult Recommend(double width, double length, PlateSizeOptions options = null) + public static PlateSizeResult Recommend( + double width, + double length, + PlateSizeOptions options = null + ) { options ??= new PlateSizeOptions(); @@ -174,10 +186,23 @@ namespace OpenNest.Shapes if (!string.IsNullOrWhiteSpace(label)) { var parts = label.Split(new[] { 'x', 'X' }, 2); - if (parts.Length == 2 - && double.TryParse(parts[0].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var a) - && double.TryParse(parts[1].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var b) - && a > 0 && b > 0) + if ( + parts.Length == 2 + && double.TryParse( + parts[0].Trim(), + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out var a + ) + && double.TryParse( + parts[1].Trim(), + System.Globalization.NumberStyles.Float, + System.Globalization.CultureInfo.InvariantCulture, + out var b + ) + && a > 0 + && b > 0 + ) { var width = System.Math.Min(a, b); var length = System.Math.Max(a, b); @@ -190,13 +215,20 @@ namespace OpenNest.Shapes return false; } - private static Entry? PickBest(IReadOnlyList catalog, double width, double length, PlateSizeSelection selection) + private static Entry? PickBest( + IReadOnlyList catalog, + double width, + double length, + PlateSizeSelection selection + ) { var fitting = catalog.Where(e => e.Fits(width, length)); fitting = selection switch { - PlateSizeSelection.NarrowestFirst => fitting.OrderBy(e => e.Width).ThenBy(e => e.Area), + PlateSizeSelection.NarrowestFirst => fitting + .OrderBy(e => e.Width) + .ThenBy(e => e.Area), _ => fitting.OrderBy(e => e.Area).ThenBy(e => e.Width), }; @@ -249,6 +281,7 @@ namespace OpenNest.Shapes { /// Pick the cheapest sheet that contains the bbox (smallest area). SmallestArea, + /// Prefer narrower-width sheets (e.g. 48-wide before 60-wide). NarrowestFirst, } diff --git a/OpenNest.Core/Shapes/RectangleShape.cs b/OpenNest.Core/Shapes/RectangleShape.cs index 5914dc8..3459a93 100644 --- a/OpenNest.Core/Shapes/RectangleShape.cs +++ b/OpenNest.Core/Shapes/RectangleShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -23,7 +23,7 @@ namespace OpenNest.Shapes new Line(0, 0, Length, 0), new Line(Length, 0, Length, Width), new Line(Length, Width, 0, Width), - new Line(0, Width, 0, 0) + new Line(0, Width, 0, 0), }; return CreateDrawing(entities); diff --git a/OpenNest.Core/Shapes/RightTriangleShape.cs b/OpenNest.Core/Shapes/RightTriangleShape.cs index 7aec564..06e2ed3 100644 --- a/OpenNest.Core/Shapes/RightTriangleShape.cs +++ b/OpenNest.Core/Shapes/RightTriangleShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -22,7 +22,7 @@ namespace OpenNest.Shapes { new Line(0, 0, Width, 0), new Line(Width, 0, 0, Height), - new Line(0, Height, 0, 0) + new Line(0, Height, 0, 0), }; return CreateDrawing(entities); diff --git a/OpenNest.Core/Shapes/RingShape.cs b/OpenNest.Core/Shapes/RingShape.cs index ba60ce8..8c168cc 100644 --- a/OpenNest.Core/Shapes/RingShape.cs +++ b/OpenNest.Core/Shapes/RingShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -21,7 +21,7 @@ namespace OpenNest.Shapes var entities = new List { new Circle(0, 0, OuterDiameter / 2.0), - new Circle(0, 0, InnerDiameter / 2.0) + new Circle(0, 0, InnerDiameter / 2.0), }; return CreateDrawing(entities); diff --git a/OpenNest.Core/Shapes/RoundedRectangleShape.cs b/OpenNest.Core/Shapes/RoundedRectangleShape.cs index 0a61887..e0ce89e 100644 --- a/OpenNest.Core/Shapes/RoundedRectangleShape.cs +++ b/OpenNest.Core/Shapes/RoundedRectangleShape.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Shapes { @@ -10,7 +10,8 @@ namespace OpenNest.Shapes public double Width { get; set; } public double Radius { get; set; } - public override string GenerateName() => $"Rounded Rectangle {Dim(Length)}x{Dim(Width)} R{Dim(Radius)}"; + public override string GenerateName() => + $"Rounded Rectangle {Dim(Length)}x{Dim(Width)} R{Dim(Radius)}"; public override void SetPreviewDefaults() { @@ -37,29 +38,27 @@ namespace OpenNest.Shapes entities.Add(new Line(r, 0, Length - r, 0)); // Bottom-right corner arc: center at (Length-r, r), from 270deg to 360deg - entities.Add(new Arc(Length - r, r, r, - Angle.ToRadians(270), Angle.ToRadians(360))); + entities.Add(new Arc(Length - r, r, r, Angle.ToRadians(270), Angle.ToRadians(360))); // Right edge entities.Add(new Line(Length, r, Length, Width - r)); // Top-right corner arc: center at (Length-r, Width-r), from 0deg to 90deg - entities.Add(new Arc(Length - r, Width - r, r, - Angle.ToRadians(0), Angle.ToRadians(90))); + entities.Add( + new Arc(Length - r, Width - r, r, Angle.ToRadians(0), Angle.ToRadians(90)) + ); // Top edge (right to left) entities.Add(new Line(Length - r, Width, r, Width)); // Top-left corner arc: center at (r, Width-r), from 90deg to 180deg - entities.Add(new Arc(r, Width - r, r, - Angle.ToRadians(90), Angle.ToRadians(180))); + entities.Add(new Arc(r, Width - r, r, Angle.ToRadians(90), Angle.ToRadians(180))); // Left edge entities.Add(new Line(0, Width - r, 0, r)); // Bottom-left corner arc: center at (r, r), from 180deg to 270deg - entities.Add(new Arc(r, r, r, - Angle.ToRadians(180), Angle.ToRadians(270))); + entities.Add(new Arc(r, r, r, Angle.ToRadians(180), Angle.ToRadians(270))); } return CreateDrawing(entities); diff --git a/OpenNest.Core/Shapes/ShapeDefinition.cs b/OpenNest.Core/Shapes/ShapeDefinition.cs index 3aef511..449162c 100644 --- a/OpenNest.Core/Shapes/ShapeDefinition.cs +++ b/OpenNest.Core/Shapes/ShapeDefinition.cs @@ -1,9 +1,9 @@ -using OpenNest.Converters; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.IO; using System.Text.Json; +using OpenNest.Converters; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -11,7 +11,7 @@ namespace OpenNest.Shapes { private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, }; public string Name { get; set; } @@ -36,7 +36,8 @@ namespace OpenNest.Shapes public virtual void SetPreviewDefaults() { } - public static List LoadFromJson(string path) where T : ShapeDefinition + public static List LoadFromJson(string path) + where T : ShapeDefinition { var json = File.ReadAllText(path); return JsonSerializer.Deserialize>(json, JsonOptions); @@ -50,7 +51,8 @@ namespace OpenNest.Shapes if (pgm == null) throw new InvalidOperationException( - $"Failed to create program for shape '{Name}'. Check that parameters produce valid geometry."); + $"Failed to create program for shape '{Name}'. Check that parameters produce valid geometry." + ); return new Drawing(Name, pgm); } diff --git a/OpenNest.Core/Shapes/TShape.cs b/OpenNest.Core/Shapes/TShape.cs index 8a8bfee..1ddabcc 100644 --- a/OpenNest.Core/Shapes/TShape.cs +++ b/OpenNest.Core/Shapes/TShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -37,7 +37,7 @@ namespace OpenNest.Shapes new Line(Width, Height, 0, Height), new Line(0, Height, 0, stemTop), new Line(0, stemTop, stemLeft, stemTop), - new Line(stemLeft, stemTop, stemLeft, 0) + new Line(stemLeft, stemTop, stemLeft, 0), }; return CreateDrawing(entities); diff --git a/OpenNest.Core/Shapes/TrapezoidShape.cs b/OpenNest.Core/Shapes/TrapezoidShape.cs index 660c9be..5792354 100644 --- a/OpenNest.Core/Shapes/TrapezoidShape.cs +++ b/OpenNest.Core/Shapes/TrapezoidShape.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Shapes { @@ -9,7 +9,8 @@ namespace OpenNest.Shapes public double BottomWidth { get; set; } public double Height { get; set; } - public override string GenerateName() => $"Trapezoid {Dim(TopWidth)}x{Dim(BottomWidth)}x{Dim(Height)}"; + public override string GenerateName() => + $"Trapezoid {Dim(TopWidth)}x{Dim(BottomWidth)}x{Dim(Height)}"; public override void SetPreviewDefaults() { @@ -27,7 +28,7 @@ namespace OpenNest.Shapes new Line(0, 0, BottomWidth, 0), new Line(BottomWidth, 0, offset + TopWidth, Height), new Line(offset + TopWidth, Height, offset, Height), - new Line(offset, Height, 0, 0) + new Line(offset, Height, 0, 0), }; return CreateDrawing(entities); diff --git a/OpenNest.Core/Splitting/AutoSplitCalculator.cs b/OpenNest.Core/Splitting/AutoSplitCalculator.cs index 31420ff..ee82f60 100644 --- a/OpenNest.Core/Splitting/AutoSplitCalculator.cs +++ b/OpenNest.Core/Splitting/AutoSplitCalculator.cs @@ -5,19 +5,28 @@ namespace OpenNest; public static class AutoSplitCalculator { - public static List FitToPlate(Box partBounds, double plateWidth, double plateHeight, - double edgeSpacing, double featureOverhang) + public static List FitToPlate( + Box partBounds, + double plateWidth, + double plateHeight, + double edgeSpacing, + double featureOverhang + ) { var usableWidth = plateWidth - 2 * edgeSpacing - featureOverhang; var usableHeight = plateHeight - 2 * edgeSpacing - featureOverhang; var lines = new List(); - var verticalSplits = usableWidth > 0 ? (int)System.Math.Ceiling(partBounds.Length / usableWidth) - 1 : 0; - var horizontalSplits = usableHeight > 0 ? (int)System.Math.Ceiling(partBounds.Width / usableHeight) - 1 : 0; + var verticalSplits = + usableWidth > 0 ? (int)System.Math.Ceiling(partBounds.Length / usableWidth) - 1 : 0; + var horizontalSplits = + usableHeight > 0 ? (int)System.Math.Ceiling(partBounds.Width / usableHeight) - 1 : 0; - if (verticalSplits < 0) verticalSplits = 0; - if (horizontalSplits < 0) horizontalSplits = 0; + if (verticalSplits < 0) + verticalSplits = 0; + if (horizontalSplits < 0) + horizontalSplits = 0; for (var i = 1; i <= verticalSplits; i++) lines.Add(new SplitLine(partBounds.X + usableWidth * i, CutOffAxis.Vertical)); @@ -28,7 +37,11 @@ public static class AutoSplitCalculator return lines; } - public static List SplitByCount(Box partBounds, int horizontalPieces, int verticalPieces) + public static List SplitByCount( + Box partBounds, + int horizontalPieces, + int verticalPieces + ) { var lines = new List(); diff --git a/OpenNest.Core/Splitting/DrawingSplitter.cs b/OpenNest.Core/Splitting/DrawingSplitter.cs index 0bbd577..5901b21 100644 --- a/OpenNest.Core/Splitting/DrawingSplitter.cs +++ b/OpenNest.Core/Splitting/DrawingSplitter.cs @@ -10,7 +10,11 @@ namespace OpenNest; /// public static class DrawingSplitter { - public static List Split(Drawing drawing, List splitLines, SplitParameters parameters) + public static List Split( + Drawing drawing, + List splitLines, + SplitParameters parameters + ) { if (splitLines.Count == 0) return new List { drawing }; @@ -35,8 +39,8 @@ public static class DrawingSplitter // Polygonize cutouts once. Used for trimming feature edges (so cut lines // don't travel through a cutout interior) and for hole/containment tests // in the final component-assembly pass. - var cutoutPolygons = profile.Cutouts - .Select(c => c.ToPolygon()) + var cutoutPolygons = profile + .Cutouts.Select(c => c.ToPolygon()) .Where(p => p != null) .ToList(); @@ -45,7 +49,14 @@ public static class DrawingSplitter foreach (var region in regions) { - var pieceEntities = ClipPerimeterToRegion(perimeter, region, sortedLines, feature, parameters, cutoutPolygons); + var pieceEntities = ClipPerimeterToRegion( + perimeter, + region, + sortedLines, + feature, + parameters, + cutoutPolygons + ); if (pieceEntities.Count == 0) continue; @@ -72,13 +83,18 @@ public static class DrawingSplitter private static ShapeProfile BuildProfile(Drawing drawing) { - var entities = ConvertProgram.ToGeometry(drawing.Program) + var entities = ConvertProgram + .ToGeometry(drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); return new ShapeProfile(entities); } - private static List CollectCutouts(List cutouts, Box region, List splitLines) + private static List CollectCutouts( + List cutouts, + Box region, + List splitLines + ) { var entities = new List(); foreach (var cutout in cutouts) @@ -95,7 +111,12 @@ public static class DrawingSplitter return entities; } - private static Drawing BuildPieceDrawing(Drawing source, List entities, int pieceIndex, Box region) + private static Drawing BuildPieceDrawing( + Drawing source, + List entities, + int pieceIndex, + Box region + ) { var pieceBounds = entities.Select(e => e.BoundingBox).ToList().GetBoundingBox(); var offsetX = -pieceBounds.X; @@ -123,15 +144,23 @@ public static class DrawingSplitter if (clipped == null) continue; - piece.Bends.Add(new Bending.Bend - { - StartPoint = new Vector(clipped.Value.Start.X + offsetX, clipped.Value.Start.Y + offsetY), - EndPoint = new Vector(clipped.Value.End.X + offsetX, clipped.Value.End.Y + offsetY), - Direction = bend.Direction, - Angle = bend.Angle, - Radius = bend.Radius, - NoteText = bend.NoteText, - }); + piece.Bends.Add( + new Bending.Bend + { + StartPoint = new Vector( + clipped.Value.Start.X + offsetX, + clipped.Value.Start.Y + offsetY + ), + EndPoint = new Vector( + clipped.Value.End.X + offsetX, + clipped.Value.End.Y + offsetY + ), + Direction = bend.Direction, + Angle = bend.Angle, + Radius = bend.Radius, + NoteText = bend.NoteText, + } + ); } } @@ -146,10 +175,17 @@ public static class DrawingSplitter { var dx = end.X - start.X; var dy = end.Y - start.Y; - double t0 = 0, t1 = 1; + double t0 = 0, + t1 = 1; double[] p = { -dx, dx, -dy, dy }; - double[] q = { start.X - box.Left, box.Right - start.X, start.Y - box.Bottom, box.Top - start.Y }; + double[] q = + { + start.X - box.Left, + box.Right - start.X, + start.Y - box.Bottom, + box.Top - start.Y, + }; for (var i = 0; i < 4; i++) { @@ -190,7 +226,12 @@ public static class DrawingSplitter if (shape.Entities[i] is Circle circle) { var arc1 = new Arc(circle.Center, circle.Radius, 0, System.Math.PI); - var arc2 = new Arc(circle.Center, circle.Radius, System.Math.PI, System.Math.PI * 2); + var arc2 = new Arc( + circle.Center, + circle.Radius, + System.Math.PI, + System.Math.PI * 2 + ); shape.Entities.RemoveAt(i); shape.Entities.Insert(i, arc2); shape.Entities.Insert(i, arc1); @@ -202,15 +243,21 @@ public static class DrawingSplitter { return line.Axis == CutOffAxis.Vertical ? line.Position > bounds.Left + OpenNest.Math.Tolerance.Epsilon - && line.Position < bounds.Right - OpenNest.Math.Tolerance.Epsilon + && line.Position < bounds.Right - OpenNest.Math.Tolerance.Epsilon : line.Position > bounds.Bottom + OpenNest.Math.Tolerance.Epsilon - && line.Position < bounds.Top - OpenNest.Math.Tolerance.Epsilon; + && line.Position < bounds.Top - OpenNest.Math.Tolerance.Epsilon; } private static List BuildClipRegions(List sortedLines, Box bounds) { - var verticals = sortedLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList(); - var horizontals = sortedLines.Where(l => l.Axis == CutOffAxis.Horizontal).OrderBy(l => l.Position).ToList(); + var verticals = sortedLines + .Where(l => l.Axis == CutOffAxis.Vertical) + .OrderBy(l => l.Position) + .ToList(); + var horizontals = sortedLines + .Where(l => l.Axis == CutOffAxis.Horizontal) + .OrderBy(l => l.Position) + .ToList(); var xEdges = new List { bounds.Left }; xEdges.AddRange(verticals.Select(v => v.Position)); @@ -222,8 +269,15 @@ public static class DrawingSplitter var regions = new List(); for (var yi = 0; yi < yEdges.Count - 1; yi++) - for (var xi = 0; xi < xEdges.Count - 1; xi++) - regions.Add(new Box(xEdges[xi], yEdges[yi], xEdges[xi + 1] - xEdges[xi], yEdges[yi + 1] - yEdges[yi])); + for (var xi = 0; xi < xEdges.Count - 1; xi++) + regions.Add( + new Box( + xEdges[xi], + yEdges[yi], + xEdges[xi + 1] - xEdges[xi], + yEdges[yi + 1] - yEdges[yi] + ) + ); return regions; } @@ -232,9 +286,14 @@ public static class DrawingSplitter /// Clip perimeter to a region by walking entities, splitting at split line crossings, /// and stitching in feature edges. No polygon clipping library needed. /// - private static List ClipPerimeterToRegion(Shape perimeter, Box region, - List splitLines, ISplitFeature feature, SplitParameters parameters, - List cutoutPolygons) + private static List ClipPerimeterToRegion( + Shape perimeter, + Box region, + List splitLines, + ISplitFeature feature, + SplitParameters parameters, + List cutoutPolygons + ) { var boundarySplitLines = GetBoundarySplitLines(region, splitLines); var entities = new List(); @@ -245,7 +304,14 @@ public static class DrawingSplitter if (entities.Count == 0) return new List(); - InsertFeatureEdges(entities, region, boundarySplitLines, feature, parameters, cutoutPolygons); + InsertFeatureEdges( + entities, + region, + boundarySplitLines, + feature, + parameters, + cutoutPolygons + ); // Winding is handled later in AssemblePieces, once connected components // are known. At this stage the piece may still be multiple disjoint loops. return entities; @@ -256,8 +322,10 @@ public static class DrawingSplitter if (entity is Line line) { var clipped = ClipLineToBox(line.StartPoint, line.EndPoint, region); - if (clipped == null) return; - if (clipped.Value.Start.DistanceTo(clipped.Value.End) < Math.Tolerance.Epsilon) return; + if (clipped == null) + return; + if (clipped.Value.Start.DistanceTo(clipped.Value.End) < Math.Tolerance.Epsilon) + return; entities.Add(new Line(clipped.Value.Start, clipped.Value.End)); return; } @@ -279,10 +347,13 @@ public static class DrawingSplitter { var edges = new[] { - new Line(new Vector(region.Left, region.Bottom), new Vector(region.Right, region.Bottom)), + new Line( + new Vector(region.Left, region.Bottom), + new Vector(region.Right, region.Bottom) + ), new Line(new Vector(region.Right, region.Bottom), new Vector(region.Right, region.Top)), new Line(new Vector(region.Right, region.Top), new Vector(region.Left, region.Top)), - new Line(new Vector(region.Left, region.Top), new Vector(region.Left, region.Bottom)) + new Line(new Vector(region.Left, region.Top), new Vector(region.Left, region.Bottom)), }; var arcs = new List { arc }; @@ -308,7 +379,11 @@ public static class DrawingSplitter foreach (var w in working) { var onArc = OpenNest.Math.Angle.IsBetweenRad( - w.Center.AngleTo(pt), w.StartAngle, w.EndAngle, w.IsReversed); + w.Center.AngleTo(pt), + w.StartAngle, + w.EndAngle, + w.IsReversed + ); if (!onArc) { replaced.Add(w); @@ -316,8 +391,10 @@ public static class DrawingSplitter } var (first, second) = w.SplitAt(pt); - if (first != null && first.SweepAngle() > Math.Tolerance.Epsilon) replaced.Add(first); - if (second != null && second.SweepAngle() > Math.Tolerance.Epsilon) replaced.Add(second); + if (first != null && first.SweepAngle() > Math.Tolerance.Epsilon) + replaced.Add(first); + if (second != null && second.SweepAngle() > Math.Tolerance.Epsilon) + replaced.Add(second); } working = replaced; } @@ -345,14 +422,18 @@ public static class DrawingSplitter { if (sl.Axis == CutOffAxis.Vertical) { - if (System.Math.Abs(sl.Position - region.Left) < OpenNest.Math.Tolerance.Epsilon - || System.Math.Abs(sl.Position - region.Right) < OpenNest.Math.Tolerance.Epsilon) + if ( + System.Math.Abs(sl.Position - region.Left) < OpenNest.Math.Tolerance.Epsilon + || System.Math.Abs(sl.Position - region.Right) < OpenNest.Math.Tolerance.Epsilon + ) result.Add(sl); } else { - if (System.Math.Abs(sl.Position - region.Bottom) < OpenNest.Math.Tolerance.Epsilon - || System.Math.Abs(sl.Position - region.Top) < OpenNest.Math.Tolerance.Epsilon) + if ( + System.Math.Abs(sl.Position - region.Bottom) < OpenNest.Math.Tolerance.Epsilon + || System.Math.Abs(sl.Position - region.Top) < OpenNest.Math.Tolerance.Epsilon + ) result.Add(sl); } } @@ -381,7 +462,8 @@ public static class DrawingSplitter var midAngle = (arc.StartAngle + arc.EndAngle) / 2; return new Vector( arc.Center.X + arc.Radius * System.Math.Cos(midAngle), - arc.Center.Y + arc.Radius * System.Math.Sin(midAngle)); + arc.Center.Y + arc.Radius * System.Math.Sin(midAngle) + ); } return new Vector(0, 0); @@ -395,10 +477,14 @@ public static class DrawingSplitter /// crossing), spanning cutouts (two holes puncturing the line), and /// normal mid-part splits uniformly. /// - private static void InsertFeatureEdges(List entities, - Box region, List boundarySplitLines, - ISplitFeature feature, SplitParameters parameters, - List cutoutPolygons) + private static void InsertFeatureEdges( + List entities, + Box region, + List boundarySplitLines, + ISplitFeature feature, + SplitParameters parameters, + List cutoutPolygons + ) { foreach (var sl in boundarySplitLines) { @@ -411,7 +497,9 @@ public static class DrawingSplitter var featureResult = feature.GenerateFeatures(sl, extentStart, extentEnd, parameters); var isNegativeSide = RegionSideOf(region, sl) < 0; - var featureEdge = isNegativeSide ? featureResult.NegativeSideEdge : featureResult.PositiveSideEdge; + var featureEdge = isNegativeSide + ? featureResult.NegativeSideEdge + : featureResult.PositiveSideEdge; // Trim any line segments that cross a cutout — cut lines must never // travel through a hole. @@ -427,7 +515,10 @@ public static class DrawingSplitter /// passed through unchanged; a tighter fix for arcs in feature edges (weld-gap /// tabs, spike-groove) can be added later if a test demands it. /// - private static List TrimFeatureEdgeAgainstCutouts(List featureEdge, List cutoutPolygons) + private static List TrimFeatureEdgeAgainstCutouts( + List featureEdge, + List cutoutPolygons + ) { if (cutoutPolygons.Count == 0 || featureEdge.Count == 0) return featureEdge; @@ -456,7 +547,15 @@ public static class DrawingSplitter var polyLines = poly.ToLines(); foreach (var edge in polyLines) { - if (TryIntersectSegments(line.StartPoint, line.EndPoint, edge.StartPoint, edge.EndPoint, out var t)) + if ( + TryIntersectSegments( + line.StartPoint, + line.EndPoint, + edge.StartPoint, + edge.EndPoint, + out var t + ) + ) { if (t > Math.Tolerance.Epsilon && t < 1.0 - Math.Tolerance.Epsilon) ts.Add(t); @@ -471,12 +570,14 @@ public static class DrawingSplitter { var t0 = ts[i]; var t1 = ts[i + 1]; - if (t1 - t0 < Math.Tolerance.Epsilon) continue; + if (t1 - t0 < Math.Tolerance.Epsilon) + continue; var tMid = (t0 + t1) * 0.5; var mid = new Vector( line.StartPoint.X + (line.EndPoint.X - line.StartPoint.X) * tMid, - line.StartPoint.Y + (line.EndPoint.Y - line.StartPoint.Y) * tMid); + line.StartPoint.Y + (line.EndPoint.Y - line.StartPoint.Y) * tMid + ); var insideCutout = false; foreach (var poly in cutoutPolygons) @@ -487,14 +588,17 @@ public static class DrawingSplitter break; } } - if (insideCutout) continue; + if (insideCutout) + continue; var p0 = new Vector( line.StartPoint.X + (line.EndPoint.X - line.StartPoint.X) * t0, - line.StartPoint.Y + (line.EndPoint.Y - line.StartPoint.Y) * t0); + line.StartPoint.Y + (line.EndPoint.Y - line.StartPoint.Y) * t0 + ); var p1 = new Vector( line.StartPoint.X + (line.EndPoint.X - line.StartPoint.X) * t1, - line.StartPoint.Y + (line.EndPoint.Y - line.StartPoint.Y) * t1); + line.StartPoint.Y + (line.EndPoint.Y - line.StartPoint.Y) * t1 + ); segments.Add(new Line(p0, p1)); } @@ -506,7 +610,13 @@ public static class DrawingSplitter /// Segment-segment intersection. On hit, returns the parameter t along segment AB /// (0 = a0, 1 = a1) via . /// - private static bool TryIntersectSegments(Vector a0, Vector a1, Vector b0, Vector b1, out double tOnA) + private static bool TryIntersectSegments( + Vector a0, + Vector a1, + Vector b0, + Vector b1, + out double tOnA + ) { tOnA = 0; var rx = a1.X - a0.X; @@ -523,8 +633,10 @@ public static class DrawingSplitter var t = (dx * sy - dy * sx) / denom; var u = (dx * ry - dy * rx) / denom; - if (t < -Math.Tolerance.Epsilon || t > 1 + Math.Tolerance.Epsilon) return false; - if (u < -Math.Tolerance.Epsilon || u > 1 + Math.Tolerance.Epsilon) return false; + if (t < -Math.Tolerance.Epsilon || t > 1 + Math.Tolerance.Epsilon) + return false; + if (u < -Math.Tolerance.Epsilon || u > 1 + Math.Tolerance.Epsilon) + return false; tOnA = t; return true; @@ -532,7 +644,8 @@ public static class DrawingSplitter private static bool IsCutoutInRegion(Shape cutout, Box region) { - if (cutout.Entities.Count == 0) return false; + if (cutout.Entities.Count == 0) + return false; var bb = cutout.BoundingBox; // Fully contained iff the cutout's bounding box fits inside the region. return bb.Left >= region.Left - Math.Tolerance.Epsilon @@ -566,7 +679,11 @@ public static class DrawingSplitter /// using endpoint connectivity, which produces the correct closed loops — one /// loop per physically-connected strip of material. /// - private static List ClipCutoutToRegion(Shape cutout, Box region, List splitLines) + private static List ClipCutoutToRegion( + Shape cutout, + Box region, + List splitLines + ) { var entities = new List(); foreach (var entity in cutout.Entities) @@ -583,10 +700,12 @@ public static class DrawingSplitter private static List> AssemblePieces(List entities) { var pieces = new List>(); - if (entities.Count == 0) return pieces; + if (entities.Count == 0) + return pieces; var shapes = ShapeBuilder.GetShapes(entities); - if (shapes.Count == 0) return pieces; + if (shapes.Count == 0) + return pieces; // Polygonize every shape once so we can run containment tests. var polygons = new List(shapes.Count); @@ -606,13 +725,18 @@ public static class DrawingSplitter for (var j = 0; j < shapes.Count; j++) { - if (i == j) continue; - if (polygons[j] == null) continue; - if (polygons[j].Vertices.Count < 3) continue; + if (i == j) + continue; + if (polygons[j] == null) + continue; + if (polygons[j].Vertices.Count < 3) + continue; var bbB = shapes[j].BoundingBox; - if (!BoxContainsBox(bbB, bbA)) continue; - if (!polygons[j].ContainsPoint(repA)) continue; + if (!BoxContainsBox(bbB, bbA)) + continue; + if (!polygons[j].ContainsPoint(repA)) + continue; isHole[i] = true; break; @@ -622,14 +746,18 @@ public static class DrawingSplitter // For each outer, attach the holes that fall inside it. for (var i = 0; i < shapes.Count; i++) { - if (isHole[i]) continue; + if (isHole[i]) + continue; var outer = shapes[i]; var outerPoly = polygons[i]; // Enforce perimeter winding = CW. - if (outerPoly != null && outerPoly.Vertices.Count >= 3 - && outerPoly.RotationDirection() != RotationType.CW) + if ( + outerPoly != null + && outerPoly.Vertices.Count >= 3 + && outerPoly.RotationDirection() != RotationType.CW + ) outer.Reverse(); var piece = new List(); @@ -637,19 +765,26 @@ public static class DrawingSplitter for (var j = 0; j < shapes.Count; j++) { - if (!isHole[j]) continue; - if (polygons[i] == null || polygons[i].Vertices.Count < 3) continue; + if (!isHole[j]) + continue; + if (polygons[i] == null || polygons[i].Vertices.Count < 3) + continue; var bbJ = shapes[j].BoundingBox; - if (!BoxContainsBox(shapes[i].BoundingBox, bbJ)) continue; + if (!BoxContainsBox(shapes[i].BoundingBox, bbJ)) + continue; var rep = FirstVertexOf(shapes[j]); - if (!polygons[i].ContainsPoint(rep)) continue; + if (!polygons[i].ContainsPoint(rep)) + continue; var hole = shapes[j]; var holePoly = polygons[j]; - if (holePoly != null && holePoly.Vertices.Count >= 3 - && holePoly.RotationDirection() != RotationType.CCW) + if ( + holePoly != null + && holePoly.Vertices.Count >= 3 + && holePoly.RotationDirection() != RotationType.CCW + ) hole.Reverse(); piece.AddRange(hole.Entities); @@ -692,7 +827,7 @@ public static class DrawingSplitter { Line l => l.StartPoint, Arc a => a.StartPoint(), - _ => new Vector(0, 0) + _ => new Vector(0, 0), }; } @@ -702,7 +837,7 @@ public static class DrawingSplitter { Line l => l.EndPoint, Arc a => a.EndPoint(), - _ => new Vector(0, 0) + _ => new Vector(0, 0), }; } @@ -713,7 +848,7 @@ public static class DrawingSplitter SplitType.Straight => new StraightSplit(), SplitType.WeldGapTabs => new WeldGapTabSplit(), SplitType.SpikeGroove => new SpikeGrooveSplit(), - _ => new StraightSplit() + _ => new StraightSplit(), }; } } diff --git a/OpenNest.Core/Splitting/ISplitFeature.cs b/OpenNest.Core/Splitting/ISplitFeature.cs index 055d73d..6361ab3 100644 --- a/OpenNest.Core/Splitting/ISplitFeature.cs +++ b/OpenNest.Core/Splitting/ISplitFeature.cs @@ -18,5 +18,10 @@ public class SplitFeatureResult public interface ISplitFeature { string Name { get; } - SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters); + SplitFeatureResult GenerateFeatures( + SplitLine line, + double extentStart, + double extentEnd, + SplitParameters parameters + ); } diff --git a/OpenNest.Core/Splitting/SpikeGrooveSplit.cs b/OpenNest.Core/Splitting/SpikeGrooveSplit.cs index ba173fe..1089581 100644 --- a/OpenNest.Core/Splitting/SpikeGrooveSplit.cs +++ b/OpenNest.Core/Splitting/SpikeGrooveSplit.cs @@ -13,7 +13,12 @@ public class SpikeGrooveSplit : ISplitFeature { public string Name => "Spike / V-Groove"; - public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters) + public SplitFeatureResult GenerateFeatures( + SplitLine line, + double extentStart, + double extentEnd, + SplitParameters parameters + ) { var extent = extentEnd - extentStart; var pairCount = parameters.SpikePairCount; @@ -44,14 +49,37 @@ public class SpikeGrooveSplit : ISplitFeature pairPositions.Add(extentStart + margin + usable * i / (pairCount - 1)); } - var negEntities = BuildGrooveSide(pairPositions, grooveHalfWidth, grooveDepth, extentStart, extentEnd, pos, isVertical); - var posEntities = BuildSpikeSide(pairPositions, spikeHalfWidth, spikeDepth, extentStart, extentEnd, pos, isVertical); + var negEntities = BuildGrooveSide( + pairPositions, + grooveHalfWidth, + grooveDepth, + extentStart, + extentEnd, + pos, + isVertical + ); + var posEntities = BuildSpikeSide( + pairPositions, + spikeHalfWidth, + spikeDepth, + extentStart, + extentEnd, + pos, + isVertical + ); return new SplitFeatureResult(negEntities, posEntities); } - private static List BuildGrooveSide(List pairPositions, double halfWidth, double depth, - double extentStart, double extentEnd, double pos, bool isVertical) + private static List BuildGrooveSide( + List pairPositions, + double halfWidth, + double depth, + double extentStart, + double extentEnd, + double pos, + bool isVertical + ) { var entities = new List(); var cursor = extentStart; @@ -76,8 +104,15 @@ public class SpikeGrooveSplit : ISplitFeature return entities; } - private static List BuildSpikeSide(List pairPositions, double halfWidth, double depth, - double extentStart, double extentEnd, double pos, bool isVertical) + private static List BuildSpikeSide( + List pairPositions, + double halfWidth, + double depth, + double extentStart, + double extentEnd, + double pos, + bool isVertical + ) { var entities = new List(); var cursor = extentEnd; @@ -103,7 +138,13 @@ public class SpikeGrooveSplit : ISplitFeature return entities; } - private static Line MakeLine(double splitAxis1, double along1, double splitAxis2, double along2, bool isVertical) + private static Line MakeLine( + double splitAxis1, + double along1, + double splitAxis2, + double along2, + bool isVertical + ) { return isVertical ? new Line(new Vector(splitAxis1, along1), new Vector(splitAxis2, along2)) diff --git a/OpenNest.Core/Splitting/SplitLine.cs b/OpenNest.Core/Splitting/SplitLine.cs index 54435aa..8729788 100644 --- a/OpenNest.Core/Splitting/SplitLine.cs +++ b/OpenNest.Core/Splitting/SplitLine.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest; diff --git a/OpenNest.Core/Splitting/SplitLineIntersect.cs b/OpenNest.Core/Splitting/SplitLineIntersect.cs index 222febd..0ac4a7e 100644 --- a/OpenNest.Core/Splitting/SplitLineIntersect.cs +++ b/OpenNest.Core/Splitting/SplitLineIntersect.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest; diff --git a/OpenNest.Core/Splitting/SplitParameters.cs b/OpenNest.Core/Splitting/SplitParameters.cs index cf4a62f..823f6fc 100644 --- a/OpenNest.Core/Splitting/SplitParameters.cs +++ b/OpenNest.Core/Splitting/SplitParameters.cs @@ -4,7 +4,7 @@ public enum SplitType { Straight, WeldGapTabs, - SpikeGroove + SpikeGroove, } public class SplitParameters @@ -26,10 +26,11 @@ public class SplitParameters /// /// Max protrusion from the split edge (for auto-fit plate size calculation). /// - public double FeatureOverhang => Type switch - { - SplitType.WeldGapTabs => TabHeight, - SplitType.SpikeGroove => System.Math.Max(SpikeDepth, GrooveDepth), - _ => 0 - }; + public double FeatureOverhang => + Type switch + { + SplitType.WeldGapTabs => TabHeight, + SplitType.SpikeGroove => System.Math.Max(SpikeDepth, GrooveDepth), + _ => 0, + }; } diff --git a/OpenNest.Core/Splitting/StraightSplit.cs b/OpenNest.Core/Splitting/StraightSplit.cs index 5ef039d..897f3b9 100644 --- a/OpenNest.Core/Splitting/StraightSplit.cs +++ b/OpenNest.Core/Splitting/StraightSplit.cs @@ -7,16 +7,36 @@ public class StraightSplit : ISplitFeature { public string Name => "Straight"; - public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters) + public SplitFeatureResult GenerateFeatures( + SplitLine line, + double extentStart, + double extentEnd, + SplitParameters parameters + ) { - var (negEdge, posEdge) = line.Axis == CutOffAxis.Vertical - ? (new Line(new Vector(line.Position, extentStart), new Vector(line.Position, extentEnd)), - new Line(new Vector(line.Position, extentEnd), new Vector(line.Position, extentStart))) - : (new Line(new Vector(extentStart, line.Position), new Vector(extentEnd, line.Position)), - new Line(new Vector(extentEnd, line.Position), new Vector(extentStart, line.Position))); + var (negEdge, posEdge) = + line.Axis == CutOffAxis.Vertical + ? ( + new Line( + new Vector(line.Position, extentStart), + new Vector(line.Position, extentEnd) + ), + new Line( + new Vector(line.Position, extentEnd), + new Vector(line.Position, extentStart) + ) + ) + : ( + new Line( + new Vector(extentStart, line.Position), + new Vector(extentEnd, line.Position) + ), + new Line( + new Vector(extentEnd, line.Position), + new Vector(extentStart, line.Position) + ) + ); - return new SplitFeatureResult( - new List { negEdge }, - new List { posEdge }); + return new SplitFeatureResult(new List { negEdge }, new List { posEdge }); } } diff --git a/OpenNest.Core/Splitting/WeldGapTabSplit.cs b/OpenNest.Core/Splitting/WeldGapTabSplit.cs index f7d9c6c..f718d73 100644 --- a/OpenNest.Core/Splitting/WeldGapTabSplit.cs +++ b/OpenNest.Core/Splitting/WeldGapTabSplit.cs @@ -11,7 +11,12 @@ public class WeldGapTabSplit : ISplitFeature { public string Name => "Weld-Gap Tabs"; - public SplitFeatureResult GenerateFeatures(SplitLine line, double extentStart, double extentEnd, SplitParameters parameters) + public SplitFeatureResult GenerateFeatures( + SplitLine line, + double extentStart, + double extentEnd, + SplitParameters parameters + ) { var extent = extentEnd - extentStart; var tabCount = parameters.TabCount; @@ -51,18 +56,42 @@ public class WeldGapTabSplit : ISplitFeature if (tabStart > cursor + OpenNest.Math.Tolerance.Epsilon) negEntities.Add(new Line(new Vector(pos, cursor), new Vector(pos, tabStart))); - negEntities.Add(new Line(new Vector(pos, tabStart), new Vector(pos + tabDir * tabHeight, tabStart))); - negEntities.Add(new Line(new Vector(pos + tabDir * tabHeight, tabStart), new Vector(pos + tabDir * tabHeight, tabEnd))); - negEntities.Add(new Line(new Vector(pos + tabDir * tabHeight, tabEnd), new Vector(pos, tabEnd))); + negEntities.Add( + new Line( + new Vector(pos, tabStart), + new Vector(pos + tabDir * tabHeight, tabStart) + ) + ); + negEntities.Add( + new Line( + new Vector(pos + tabDir * tabHeight, tabStart), + new Vector(pos + tabDir * tabHeight, tabEnd) + ) + ); + negEntities.Add( + new Line(new Vector(pos + tabDir * tabHeight, tabEnd), new Vector(pos, tabEnd)) + ); } else { if (tabStart > cursor + OpenNest.Math.Tolerance.Epsilon) negEntities.Add(new Line(new Vector(cursor, pos), new Vector(tabStart, pos))); - negEntities.Add(new Line(new Vector(tabStart, pos), new Vector(tabStart, pos + tabDir * tabHeight))); - negEntities.Add(new Line(new Vector(tabStart, pos + tabDir * tabHeight), new Vector(tabEnd, pos + tabDir * tabHeight))); - negEntities.Add(new Line(new Vector(tabEnd, pos + tabDir * tabHeight), new Vector(tabEnd, pos))); + negEntities.Add( + new Line( + new Vector(tabStart, pos), + new Vector(tabStart, pos + tabDir * tabHeight) + ) + ); + negEntities.Add( + new Line( + new Vector(tabStart, pos + tabDir * tabHeight), + new Vector(tabEnd, pos + tabDir * tabHeight) + ) + ); + negEntities.Add( + new Line(new Vector(tabEnd, pos + tabDir * tabHeight), new Vector(tabEnd, pos)) + ); } cursor = tabEnd; diff --git a/OpenNest.Core/Timing.cs b/OpenNest.Core/Timing.cs index ee8479a..0d6e628 100644 --- a/OpenNest.Core/Timing.cs +++ b/OpenNest.Core/Timing.cs @@ -1,9 +1,9 @@ -using OpenNest.Api; +using System; +using System.Linq; +using OpenNest.Api; using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Geometry; -using System; -using System.Linq; namespace OpenNest { @@ -12,7 +12,9 @@ namespace OpenNest public static TimingInfo GetTimingInfo(Program pgm) { var entities = ConvertProgram.ToGeometry(pgm); - var shapes = ShapeBuilder.GetShapes(entities.Where(entity => entity.Layer != SpecialLayers.Rapid)); + var shapes = ShapeBuilder.GetShapes( + entities.Where(entity => entity.Layer != SpecialLayers.Rapid) + ); var info = new TimingInfo { PierceCount = shapes.Count }; var last = entities[0]; @@ -58,10 +60,12 @@ namespace OpenNest { info.CutDistance += entity.Length; - if (entity.Type == EntityType.Line && - lastEntity != null && - lastEntity.Type == EntityType.Line && - lastEntity.Layer == SpecialLayers.Cut) + if ( + entity.Type == EntityType.Line + && lastEntity != null + && lastEntity.Type == EntityType.Line + && lastEntity.Layer == SpecialLayers.Cut + ) info.IntersectionCount++; } else if (entity.Layer == SpecialLayers.Rapid) diff --git a/OpenNest.Core/TimingInfo.cs b/OpenNest.Core/TimingInfo.cs index e0f5854..b2bdf81 100644 --- a/OpenNest.Core/TimingInfo.cs +++ b/OpenNest.Core/TimingInfo.cs @@ -17,7 +17,7 @@ namespace OpenNest CutDistance = info1.CutDistance + info2.CutDistance, IntersectionCount = info1.IntersectionCount + info2.IntersectionCount, TravelDistance = info1.TravelDistance + info2.TravelDistance, - PierceCount = info1.PierceCount + info2.PierceCount + PierceCount = info1.PierceCount + info2.PierceCount, }; } @@ -28,7 +28,7 @@ namespace OpenNest CutDistance = info1.CutDistance - info2.CutDistance, IntersectionCount = info1.IntersectionCount - info2.IntersectionCount, TravelDistance = info1.TravelDistance - info2.TravelDistance, - PierceCount = info1.PierceCount - info2.PierceCount + PierceCount = info1.PierceCount - info2.PierceCount, }; } @@ -39,7 +39,7 @@ namespace OpenNest CutDistance = info1.CutDistance * info2.CutDistance, IntersectionCount = info1.IntersectionCount * info2.IntersectionCount, TravelDistance = info1.TravelDistance * info2.TravelDistance, - PierceCount = info1.PierceCount * info2.PierceCount + PierceCount = info1.PierceCount * info2.PierceCount, }; } @@ -50,7 +50,7 @@ namespace OpenNest CutDistance = info1.CutDistance * factor, IntersectionCount = info1.IntersectionCount * factor, TravelDistance = info1.TravelDistance * factor, - PierceCount = info1.PierceCount * factor + PierceCount = info1.PierceCount * factor, }; } } diff --git a/OpenNest.Core/Units.cs b/OpenNest.Core/Units.cs index c695143..890c398 100644 --- a/OpenNest.Core/Units.cs +++ b/OpenNest.Core/Units.cs @@ -1,10 +1,9 @@ - -namespace OpenNest +namespace OpenNest { public enum Units { Inches, - Millimeters + Millimeters, } public static class UnitsHelper diff --git a/OpenNest.Data/LocalJsonProvider.cs b/OpenNest.Data/LocalJsonProvider.cs index 2f2432a..fe13ac0 100644 --- a/OpenNest.Data/LocalJsonProvider.cs +++ b/OpenNest.Data/LocalJsonProvider.cs @@ -11,7 +11,7 @@ public class LocalJsonProvider : IDataProvider { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, }; public LocalJsonProvider(string directory) @@ -77,19 +77,23 @@ public class LocalJsonProvider : IDataProvider return; var assembly = typeof(LocalJsonProvider).Assembly; - var resourceName = assembly.GetManifestResourceNames() + var resourceName = assembly + .GetManifestResourceNames() .FirstOrDefault(n => n.EndsWith("CL-980.json")); - if (resourceName is null) return; + if (resourceName is null) + return; using var stream = assembly.GetManifestResourceStream(resourceName); - if (stream is null) return; + if (stream is null) + return; using var reader = new StreamReader(stream); var json = reader.ReadToEnd(); var config = JsonSerializer.Deserialize(json, JsonOptions); - if (config is null) return; + if (config is null) + return; SaveMachine(config); } diff --git a/OpenNest.Data/MachineConfig.cs b/OpenNest.Data/MachineConfig.cs index 77abe8a..83ef0e4 100644 --- a/OpenNest.Data/MachineConfig.cs +++ b/OpenNest.Data/MachineConfig.cs @@ -14,13 +14,15 @@ public class MachineConfig public ThicknessConfig? GetParameters(string material, double thickness) { var mat = GetMaterial(material); - if (mat is null) return null; + if (mat is null) + return null; return mat.Thicknesses.FirstOrDefault(t => t.Value.IsEqualTo(thickness)); } public MaterialConfig? GetMaterial(string name) { return Materials.FirstOrDefault(m => - string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase)); + string.Equals(m.Name, name, StringComparison.OrdinalIgnoreCase) + ); } } diff --git a/OpenNest.Data/MachineType.cs b/OpenNest.Data/MachineType.cs index c59a8c5..5226f37 100644 --- a/OpenNest.Data/MachineType.cs +++ b/OpenNest.Data/MachineType.cs @@ -4,5 +4,5 @@ public enum MachineType { Laser, Plasma, - Waterjet + Waterjet, } diff --git a/OpenNest.Data/UnitSystem.cs b/OpenNest.Data/UnitSystem.cs index c4b500f..9182e48 100644 --- a/OpenNest.Data/UnitSystem.cs +++ b/OpenNest.Data/UnitSystem.cs @@ -3,5 +3,5 @@ namespace OpenNest.Data; public enum UnitSystem { Inches, - Millimeters + Millimeters, } diff --git a/OpenNest.Engine.Tests/Fill/SortStripsTests.cs b/OpenNest.Engine.Tests/Fill/SortStripsTests.cs index 40bce6e..f10f677 100644 --- a/OpenNest.Engine.Tests/Fill/SortStripsTests.cs +++ b/OpenNest.Engine.Tests/Fill/SortStripsTests.cs @@ -25,7 +25,7 @@ public class SortStripsTests // shortest, then medium). The tallest column's original position leaves a // 5-unit gap to its neighbor; that single sampled gap must not get replayed // as the spacing for the whole staircase once it's no longer the leading pair. - var tall = MakeRectPart(0, 0, 10, 30); // Left 0-10, gap of 5 to next + var tall = MakeRectPart(0, 0, 10, 30); // Left 0-10, gap of 5 to next var shortCol = MakeRectPart(15, 0, 5, 5); // Left 15-20, gap of 1 to next var medium = MakeRectPart(21, 0, 20, 15); // Left 21-41 @@ -40,7 +40,9 @@ public class SortStripsTests var newLeft = parts.Min(p => p.BoundingBox.Left); var newSpan = newRight - newLeft; - Assert.True(newSpan <= originalSpan + 1e-9, - $"Resequenced columns must not exceed the original footprint: original span {originalSpan}, new span {newSpan}"); + Assert.True( + newSpan <= originalSpan + 1e-9, + $"Resequenced columns must not exceed the original footprint: original span {originalSpan}, new span {newSpan}" + ); } } diff --git a/OpenNest.Engine.Tests/Jobs/FiniteStockJobTests.cs b/OpenNest.Engine.Tests/Jobs/FiniteStockJobTests.cs index 4cc65d4..959b569 100644 --- a/OpenNest.Engine.Tests/Jobs/FiniteStockJobTests.cs +++ b/OpenNest.Engine.Tests/Jobs/FiniteStockJobTests.cs @@ -4,36 +4,66 @@ namespace OpenNest.Engine.Tests.Jobs; public class FiniteStockJobTests { - internal static NestJob Job(int? stock = 3, NestJobOptions? options = null) => new( - new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), 3) }, - new[] { new NestPlateStock("s", new Size(100, 200), stock) }, options); + internal static NestJob Job(int? stock = 3, NestJobOptions? options = null) => + new( + new[] + { + new NestJobPart( + "p", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), + 3 + ), + }, + new[] { new NestPlateStock("s", new Size(100, 200), stock) }, + options + ); internal sealed class Nester(Func place) : IPlateNester { public int Calls { get; private set; } - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) { Calls++; return place(request); } + + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) + { + Calls++; + return place(request); + } } - internal static PlateCandidate One(PlatePlacementRequest request) => new(new[] - { new NestJobPlacement(request.Parts[0].Id, 99, 0, 0, 0) }); + internal static PlateCandidate One(PlatePlacementRequest request) => + new(new[] { new NestJobPlacement(request.Parts[0].Id, 99, 0, 0, 0) }); [Theory] [InlineData(3, 3, 0, NestJobStatus.Complete, NestJobStopReason.Completed)] [InlineData(2, 2, 1, NestJobStatus.Incomplete, NestJobStopReason.StockExhausted)] [InlineData(0, 0, 3, NestJobStatus.Incomplete, NestJobStopReason.StockExhausted)] - public void DemandAndPhysicalStockAreAccountedFromPlacements(int stock, int placed, int left, - NestJobStatus status, NestJobStopReason reason) + public void DemandAndPhysicalStockAreAccountedFromPlacements( + int stock, + int placed, + int left, + NestJobStatus status, + NestJobStopReason reason + ) { var requests = new List(); - var nester = new Nester(r => { requests.Add(r.Parts[0].Quantity); return One(r); }); + var nester = new Nester(r => + { + requests.Add(r.Parts[0].Quantity); + return One(r); + }); var job = Job(stock); var result = new NestJobRunner(_ => nester).Solve(job); Assert.Equal(status, result.Status); Assert.Equal(reason, result.StopReason); Assert.Equal(placed, result.Plates.Count); Assert.All(result.Plates, p => Assert.Single(p.Placements)); - Assert.Equal(Enumerable.Range(0, placed), result.Plates.SelectMany(p => p.Placements).Select(p => p.InstanceIndex)); + Assert.Equal( + Enumerable.Range(0, placed), + result.Plates.SelectMany(p => p.Placements).Select(p => p.InstanceIndex) + ); Assert.Equal(Enumerable.Range(0, placed).Select(i => 3 - i), requests); Assert.Equal(new PartFulfillment("p", 3, placed, left), Assert.Single(result.Fulfillment)); Assert.Equal(new StockUsage("s", placed, stock - placed), Assert.Single(result.StockUsage)); @@ -58,7 +88,9 @@ public class FiniteStockJobTests [Fact] public void PlateLimitStopsUnlimitedStock() { - var result = new NestJobRunner(_ => new Nester(One)).Solve(Job(null, new NestJobOptions(maxPlates: 2))); + var result = new NestJobRunner(_ => new Nester(One)).Solve( + Job(null, new NestJobOptions(maxPlates: 2)) + ); Assert.Equal(2, result.Plates.Count); Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason); Assert.Equal(new StockUsage("s", 2, null), Assert.Single(result.StockUsage)); @@ -69,9 +101,14 @@ public class FiniteStockJobTests { using var cts = new CancellationTokenSource(); var commits = new List(); - var nester = new Nester(r => { cts.Cancel(); return One(r); }); - Assert.Throws(() => new NestJobRunner(_ => nester) - .Solve(Job(), new InlineProgress(commits.Add), cts.Token)); + var nester = new Nester(r => + { + cts.Cancel(); + return One(r); + }); + Assert.Throws(() => + new NestJobRunner(_ => nester).Solve(Job(), new InlineProgress(commits.Add), cts.Token) + ); Assert.DoesNotContain(commits, p => p.Stage == NestJobStage.PlateCommitted); } @@ -80,8 +117,9 @@ public class FiniteStockJobTests { using var cts = new CancellationTokenSource(); cts.Cancel(); - Assert.Throws(() => new NestJobRunner(_ => throw new Exception("called")) - .Solve(Job(), token: cts.Token)); + Assert.Throws(() => + new NestJobRunner(_ => throw new Exception("called")).Solve(Job(), token: cts.Token) + ); } [Theory] @@ -92,23 +130,31 @@ public class FiniteStockJobTests [InlineData("p", 0, 0, 0, 4)] public void InvalidCandidateThrows(string id, double x, double y, double rotation, int count) { - var nester = new Nester(_ => new PlateCandidate(Enumerable.Range(0, count) - .Select(i => new NestJobPlacement(id, i, x, y, rotation)))); + var nester = new Nester(_ => new PlateCandidate( + Enumerable.Range(0, count).Select(i => new NestJobPlacement(id, i, x, y, rotation)) + )); Assert.Throws(() => new NestJobRunner(_ => nester).Solve(Job())); } [Fact] public void NullCandidateAndUnknownStrategyAreExplicitErrors() { - Assert.Throws(() => new NestJobRunner(_ => new Nester(_ => null!)).Solve(Job())); - Assert.Throws(() => new NestJobRunner(_ => null!).Solve(Job(options: new NestJobOptions("missing")))); + Assert.Throws(() => + new NestJobRunner(_ => new Nester(_ => null!)).Solve(Job()) + ); + Assert.Throws(() => + new NestJobRunner(_ => null!).Solve(Job(options: new NestJobOptions("missing"))) + ); } [Fact] public void MixedStockCanBeEvaluated() { var job = Job(); - var mixed = new NestJob(job.Parts, job.Plates.Concat(new[] { new NestPlateStock("other", new Size(20, 10), 2) })); + var mixed = new NestJob( + job.Parts, + job.Plates.Concat(new[] { new NestPlateStock("other", new Size(20, 10), 2) }) + ); var result = new NestJobRunner(_ => new Nester(One)).Solve(mixed); Assert.Equal(NestJobStatus.Complete, result.Status); } @@ -119,9 +165,20 @@ public class FiniteStockJobTests [InlineData(10, 20, -1, 1)] [InlineData(10, 20, double.PositiveInfinity, 1)] [InlineData(10, 20, 0, 5)] - public void InvalidStockSettingsRejected(double width, double length, double spacing, int quadrant) + public void InvalidStockSettingsRejected( + double width, + double length, + double spacing, + int quadrant + ) { - var job = new NestJob(Job().Parts, new[] { new NestPlateStock("s", new Size(width, length), 1, spacing, quadrant: quadrant) }); + var job = new NestJob( + Job().Parts, + new[] + { + new NestPlateStock("s", new Size(width, length), 1, spacing, quadrant: quadrant), + } + ); Assert.Throws(() => new NestJobRunner(_ => new Nester(One)).Solve(job)); } @@ -129,25 +186,56 @@ public class FiniteStockJobTests public void InvalidEdgesAndGeometryRejected() { var runner = new NestJobRunner(_ => new Nester(One)); - foreach (var edges in new[] { new Spacing(-1, 0, 0, 0), new Spacing(0, double.NaN, 0, 0), new Spacing(1000, 1000, 1000, 1000) }) - Assert.Throws(() => runner.Solve(new NestJob(Job().Parts, - new[] { new NestPlateStock("s", new Size(100, 200), edgeSpacing: edges) }))); + foreach ( + var edges in new[] + { + new Spacing(-1, 0, 0, 0), + new Spacing(0, double.NaN, 0, 0), + new Spacing(1000, 1000, 1000, 1000), + } + ) + Assert.Throws(() => + runner.Solve( + new NestJob( + Job().Parts, + new[] { new NestPlateStock("s", new Size(100, 200), edgeSpacing: edges) } + ) + ) + ); var program = TestDrawingFactory.Rectangle(); program.LineTo(double.NaN, 0); - Assert.Throws(() => runner.Solve(new NestJob(new[] - { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) }, Job().Plates))); + Assert.Throws(() => + runner.Solve( + new NestJob( + new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) }, + Job().Plates + ) + ) + ); } [Fact] public void InvalidContractInputsAreRejected() { var job = Job(); - Assert.Throws(() => new NestJobRunner(_ => new Nester(One)).Solve(null!)); - Assert.Throws(() => new NestJob(new NestJobPart[] { null! }, job.Plates)); - Assert.Throws(() => new NestJob(job.Parts.Concat(job.Parts), job.Plates)); - Assert.Throws(() => new NestJob(job.Parts, job.Plates.Concat(job.Plates))); - Assert.Throws(() => new NestJobPart("p", job.Parts[0].Geometry, 0)); - Assert.Throws(() => new NestPlateStock("s", new Size(1, 1), -1)); + Assert.Throws(() => + new NestJobRunner(_ => new Nester(One)).Solve(null!) + ); + Assert.Throws(() => + new NestJob(new NestJobPart[] { null! }, job.Plates) + ); + Assert.Throws(() => + new NestJob(job.Parts.Concat(job.Parts), job.Plates) + ); + Assert.Throws(() => + new NestJob(job.Parts, job.Plates.Concat(job.Plates)) + ); + Assert.Throws(() => + new NestJobPart("p", job.Parts[0].Geometry, 0) + ); + Assert.Throws(() => + new NestPlateStock("s", new Size(1, 1), -1) + ); Assert.Throws(() => new NestJobOptions(maxPlates: 0)); } @@ -155,11 +243,28 @@ public class FiniteStockJobTests public void RunnerFactoriesAreInstanceScopedAndReceiveExactStrategyKeys() { var keys = new List(); - var first = new NestJobRunner(key => { keys.Add(key); return new Nester(One); }); - var second = new NestJobRunner(key => { keys.Add(key); return new Nester(_ => new PlateCandidate(Array.Empty())); }); - Assert.Equal(NestJobStatus.Complete, first.Solve(Job(options: new NestJobOptions("custom-A"))).Status); - Assert.Equal(NestJobStopReason.NoPlacementFound, second.Solve(Job(options: new NestJobOptions("custom-B"))).StopReason); - Assert.Equal(NestJobStatus.Complete, first.Solve(Job(options: new NestJobOptions("custom-A"))).Status); + var first = new NestJobRunner(key => + { + keys.Add(key); + return new Nester(One); + }); + var second = new NestJobRunner(key => + { + keys.Add(key); + return new Nester(_ => new PlateCandidate(Array.Empty())); + }); + Assert.Equal( + NestJobStatus.Complete, + first.Solve(Job(options: new NestJobOptions("custom-A"))).Status + ); + Assert.Equal( + NestJobStopReason.NoPlacementFound, + second.Solve(Job(options: new NestJobOptions("custom-B"))).StopReason + ); + Assert.Equal( + NestJobStatus.Complete, + first.Solve(Job(options: new NestJobOptions("custom-A"))).Status + ); Assert.Equal(new[] { "custom-A", "custom-B", "custom-A" }, keys); } @@ -169,10 +274,19 @@ public class FiniteStockJobTests using var cts = new CancellationTokenSource(); var calls = 0; var commits = new List(); - var nester = new Nester(r => { if (++calls == 2) cts.Cancel(); return One(r); }); - Assert.Throws(() => new NestJobRunner(_ => nester) - .Solve(Job(), new InlineProgress(commits.Add), cts.Token)); - Assert.Equal(1, Assert.Single(commits.Where(p => p.Stage == NestJobStage.PlateCommitted)).CommittedParts); + var nester = new Nester(r => + { + if (++calls == 2) + cts.Cancel(); + return One(r); + }); + Assert.Throws(() => + new NestJobRunner(_ => nester).Solve(Job(), new InlineProgress(commits.Add), cts.Token) + ); + Assert.Equal( + 1, + Assert.Single(commits.Where(p => p.Stage == NestJobStage.PlateCommitted)).CommittedParts + ); Assert.Equal(2, calls); } diff --git a/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs index 8297c01..4083a44 100644 --- a/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs +++ b/OpenNest.Engine.Tests/Jobs/FixedStrategyNestingEngineTests.cs @@ -22,10 +22,24 @@ public class FixedStrategyNestingEngineTests public void PreservesJobMaxPlates() { var engine = new FixedStrategyNestingEngine("Default"); - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(100, 100)), 6); - var stock = new NestPlateStock("sheet", new Size(220, 220), quantity: null, partSpacing: 2.0, - edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1); - var job = new NestJob(new[] { part }, new[] { stock }, new NestJobOptions("Default", maxPlates: 1)); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(100, 100)), + 6 + ); + var stock = new NestPlateStock( + "sheet", + new Size(220, 220), + quantity: null, + partSpacing: 2.0, + edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), + quadrant: 1 + ); + var job = new NestJob( + new[] { part }, + new[] { stock }, + new NestJobOptions("Default", maxPlates: 1) + ); var result = engine.Solve(job); diff --git a/OpenNest.Engine.Tests/Jobs/JobAdapterTests.cs b/OpenNest.Engine.Tests/Jobs/JobAdapterTests.cs index 87c1f12..3dc3ed6 100644 --- a/OpenNest.Engine.Tests/Jobs/JobAdapterTests.cs +++ b/OpenNest.Engine.Tests/Jobs/JobAdapterTests.cs @@ -10,33 +10,54 @@ public class JobAdapterTests { var drawing = new Drawing("same name", TestDrawingFactory.Rectangle()); drawing.Quantity.Required = 9; - var item = new NestItem { Drawing = drawing, Quantity = 3, Priority = 7, StepAngle = 0 }; - var sourcePlate = new Plate(100, 200) { Quantity = 3, PartSpacing = 2 }; - var job = new NestJob(new[] { DrawingJobMapper.FromItem("requirement", item) }, - new[] { DrawingJobMapper.FromPlate("stock", sourcePlate, 3) }); - var quantities = new List(); - var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(p, items => + var item = new NestItem { - var privateItem = Assert.Single(items); - quantities.Add(privateItem.Quantity); - Assert.NotSame(drawing, privateItem.Drawing); - Assert.Equal(0, privateItem.StepAngle); - Assert.Equal(7, privateItem.Priority); - var part = new Part(privateItem.Drawing); - privateItem.Quantity = 0; - privateItem.Drawing.Quantity.Required = 0; - return new List { part }; - })); + Drawing = drawing, + Quantity = 3, + Priority = 7, + StepAngle = 0, + }; + var sourcePlate = new Plate(100, 200) { Quantity = 3, PartSpacing = 2 }; + var job = new NestJob( + new[] { DrawingJobMapper.FromItem("requirement", item) }, + new[] { DrawingJobMapper.FromPlate("stock", sourcePlate, 3) } + ); + var quantities = new List(); + var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine( + p, + items => + { + var privateItem = Assert.Single(items); + quantities.Add(privateItem.Quantity); + Assert.NotSame(drawing, privateItem.Drawing); + Assert.Equal(0, privateItem.StepAngle); + Assert.Equal(7, privateItem.Priority); + var part = new Part(privateItem.Drawing); + privateItem.Quantity = 0; + privateItem.Drawing.Quantity.Required = 0; + return new List { part }; + } + )); var result = new NestJobRunner(_ => adapter).Solve(job); var materialized = NestResultMaterializer.Materialize(job, result); Assert.Equal(new[] { 3, 2, 1 }, quantities); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(3, materialized.Nest.Plates.Count); - Assert.All(materialized.Nest.Plates, p => { Assert.Equal(1, p.Quantity); Assert.Single(p.Parts); }); + Assert.All( + materialized.Nest.Plates, + p => + { + Assert.Equal(1, p.Quantity); + Assert.Single(p.Parts); + } + ); var outputDrawing = materialized.DrawingsByPartId["requirement"]; Assert.Equal(3, outputDrawing.Quantity.Required); Assert.Equal(3, outputDrawing.Quantity.Nested); - Assert.All(materialized.Nest.Plates, p => Assert.Same(outputDrawing, p.Parts[0].BaseDrawing)); + Assert.All( + materialized.Nest.Plates, + p => Assert.Same(outputDrawing, p.Parts[0].BaseDrawing) + ); Assert.NotSame(drawing, outputDrawing); Assert.Equal(9, drawing.Quantity.Required); Assert.Equal(0, drawing.Quantity.Nested); @@ -44,26 +65,38 @@ public class JobAdapterTests Assert.Equal(3, sourcePlate.Quantity); Assert.Empty(sourcePlate.Parts); Assert.Equal(2, sourcePlate.PartSpacing); - Assert.Equal(PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()).Motions, - PartGeometrySnapshot.FromProgram(drawing.Program).Motions); + Assert.Equal( + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()).Motions, + PartGeometrySnapshot.FromProgram(drawing.Program).Motions + ); } [Fact] public void ReferenceIdentityNotNamesControlsLegacyPlacements() { var drawing = new Drawing("duplicate", TestDrawingFactory.Rectangle()); - var job = new NestJob(new[] { DrawingJobMapper.FromDrawing("a", drawing, 1), - DrawingJobMapper.FromDrawing("b", drawing, 1) }, FiniteStockJobTests.Job(1).Plates); - var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(p, items => - { - Assert.NotSame(items[0].Drawing, items[1].Drawing); - foreach (var item in items) item.Drawing.Name = "identical"; - return new List + var job = new NestJob( + new[] { - new Part(items[0].Drawing, new Vector(0, 0)), - new Part(items[1].Drawing, new Vector(10, 0)) - }; - })); + DrawingJobMapper.FromDrawing("a", drawing, 1), + DrawingJobMapper.FromDrawing("b", drawing, 1), + }, + FiniteStockJobTests.Job(1).Plates + ); + var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine( + p, + items => + { + Assert.NotSame(items[0].Drawing, items[1].Drawing); + foreach (var item in items) + item.Drawing.Name = "identical"; + return new List + { + new Part(items[0].Drawing, new Vector(0, 0)), + new Part(items[1].Drawing, new Vector(10, 0)), + }; + } + )); var result = new NestJobRunner(_ => adapter).Solve(job); Assert.Equal(new[] { "a", "b" }, result.Plates[0].Placements.Select(p => p.PartId)); var output = NestResultMaterializer.Materialize(job, result); @@ -74,10 +107,19 @@ public class JobAdapterTests [Fact] public void UnknownPrivateDrawingIsRejectedEvenWithMatchingName() { - var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine(p, items => new List - { new(new Drawing(items[0].Drawing.Name, TestDrawingFactory.Rectangle())) })); - Assert.Throws(() => new NestJobRunner(_ => adapter).Solve(FiniteStockJobTests.Job())); - Assert.Throws(() => LegacyPlateNesterAdapter.Create("not registered")); + var adapter = new LegacyPlateNesterAdapter(p => new MutatingEngine( + p, + items => new List + { + new(new Drawing(items[0].Drawing.Name, TestDrawingFactory.Rectangle())), + } + )); + Assert.Throws(() => + new NestJobRunner(_ => adapter).Solve(FiniteStockJobTests.Job()) + ); + Assert.Throws(() => + LegacyPlateNesterAdapter.Create("not registered") + ); } [Fact] @@ -103,17 +145,26 @@ public class JobAdapterTests incremental.LineTo(7, 4); var incrementalSnapshot = PartGeometrySnapshot.FromProgram(incremental); Assert.Equal(Mode.Incremental, DrawingJobMapper.ToProgram(incrementalSnapshot).Mode); - Assert.Equal(incrementalSnapshot.Motions, - PartGeometrySnapshot.FromProgram(DrawingJobMapper.ToProgram(incrementalSnapshot)).Motions); + Assert.Equal( + incrementalSnapshot.Motions, + PartGeometrySnapshot + .FromProgram(DrawingJobMapper.ToProgram(incrementalSnapshot)) + .Motions + ); } [Fact] public void RealDefaultEngineRunsFromDrawingThroughMaterialization() { - var drawing = new Drawing("generated asymmetric rectangle", TestDrawingFactory.Rectangle(13, 7)); + var drawing = new Drawing( + "generated asymmetric rectangle", + TestDrawingFactory.Rectangle(13, 7) + ); drawing.Quantity.Required = 1; - var job = new NestJob(new[] { DrawingJobMapper.FromDrawing("rectangle", drawing, 1) }, - new[] { new NestPlateStock("sheet", new Size(40, 60), 1, 1, new Spacing(2, 2, 2, 2)) }); + var job = new NestJob( + new[] { DrawingJobMapper.FromDrawing("rectangle", drawing, 1) }, + new[] { new NestPlateStock("sheet", new Size(40, 60), 1, 1, new Spacing(2, 2, 2, 2)) } + ); var result = new NestJobRunner(LegacyPlateNesterAdapter.Create).Solve(job); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(new StockUsage("sheet", 1, 0), Assert.Single(result.StockUsage)); @@ -140,10 +191,13 @@ public class JobAdapterTests { var program = TestDrawingFactory.Rectangle(); program.Offset(-5, 3); - var job = new NestJob(new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) }, - FiniteStockJobTests.Job(1).Plates); - var result = new NestJobRunner(_ => new FiniteStockJobTests.Nester(_ => new PlateCandidate(new[] - { new NestJobPlacement("p", 0, 23, 31, 0.7) }))).Solve(job); + var job = new NestJob( + new[] { new NestJobPart("p", PartGeometrySnapshot.FromProgram(program), 1) }, + FiniteStockJobTests.Job(1).Plates + ); + var result = new NestJobRunner(_ => new FiniteStockJobTests.Nester(_ => new PlateCandidate( + new[] { new NestJobPlacement("p", 0, 23, 31, 0.7) } + ))).Solve(job); var output = NestResultMaterializer.Materialize(job, result); var part = output.Nest.Plates[0].Parts[0]; var expected = new Vector(-5, 3).Rotate(0.7); @@ -152,11 +206,16 @@ public class JobAdapterTests Assert.Equal(new Vector(23, 31), part.Location); } - private sealed class MutatingEngine(Plate plate, Func, List> nest) : NestEngineBase(plate) + private sealed class MutatingEngine(Plate plate, Func, List> nest) + : NestEngineBase(plate) { public override string Name => "test"; public override string Description => "mutates private demand"; - public override List Nest(List items, IProgress progress, CancellationToken token) - => nest(items); + + public override List Nest( + List items, + IProgress progress, + CancellationToken token + ) => nest(items); } } diff --git a/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs index 649836d..f33462e 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobCancellationTests.cs @@ -10,13 +10,21 @@ public class NestJobCancellationTests { using var cancellation = new CancellationTokenSource(); cancellation.Cancel(); - var nester = new CancellableNester(_ => new PlateCandidate(Array.Empty())); - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1); + var nester = new CancellableNester(_ => new PlateCandidate( + Array.Empty() + )); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), + 1 + ); var sourceGeometry = part.Geometry.Motions.ToArray(); var stock = new NestPlateStock("stock", new Size(20, 30), 1); var job = new NestJob(new[] { part }, new[] { stock }); - Assert.Throws(() => new NestJobRunner(_ => nester).Solve(job, token: cancellation.Token)); + Assert.Throws(() => + new NestJobRunner(_ => nester).Solve(job, token: cancellation.Token) + ); Assert.Equal(0, nester.Calls); Assert.Equal(sourceGeometry, job.Parts[0].Geometry.Motions); @@ -29,19 +37,30 @@ public class NestJobCancellationTests { using var cancellation = new CancellationTokenSource(); var reports = new List(); - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), + 1 + ); var sourceGeometry = part.Geometry.Motions.ToArray(); var stock = new NestPlateStock("stock", new Size(20, 30), 1); var job = new NestJob(new[] { part }, new[] { stock }); - var nester = new CancellableNester((_, token) => - { - cancellation.Cancel(); - token.ThrowIfCancellationRequested(); - return new PlateCandidate(Array.Empty()); - }); + var nester = new CancellableNester( + (_, token) => + { + cancellation.Cancel(); + token.ThrowIfCancellationRequested(); + return new PlateCandidate(Array.Empty()); + } + ); - Assert.Throws(() => new NestJobRunner(_ => nester) - .Solve(job, new InlineProgress(reports.Add), cancellation.Token)); + Assert.Throws(() => + new NestJobRunner(_ => nester).Solve( + job, + new InlineProgress(reports.Add), + cancellation.Token + ) + ); Assert.Equal(1, nester.Calls); Assert.DoesNotContain(reports, report => report.Stage == NestJobStage.PlateCommitted); @@ -54,9 +73,18 @@ public class NestJobCancellationTests public void LegacyProgressIsWrappedWithCurrentCandidateContext() { var reports = new List(); - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1); - var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 30), 1) }); - var runner = new NestJobRunner(_ => new LegacyPlateNesterAdapter(plate => new ReportingEngine(plate))); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), + 1 + ); + var job = new NestJob( + new[] { part }, + new[] { new NestPlateStock("stock", new Size(20, 30), 1) } + ); + var runner = new NestJobRunner(_ => new LegacyPlateNesterAdapter( + plate => new ReportingEngine(plate) + )); var result = runner.Solve(job, new InlineProgress(reports.Add)); @@ -84,15 +112,20 @@ public class NestJobCancellationTests this.place = (request, _) => place(request); } - public CancellableNester(Func place) + public CancellableNester( + Func place + ) { this.place = place; } public int Calls { get; private set; } - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) { Calls++; return place(request, token); @@ -104,8 +137,11 @@ public class NestJobCancellationTests public override string Name => "reporting"; public override string Description => "reports progress"; - public override List Nest(List items, IProgress? progress, - CancellationToken token) + public override List Nest( + List items, + IProgress? progress, + CancellationToken token + ) { progress?.Report(new NestProgress { Description = "legacy detail" }); return new List(); diff --git a/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs index e15fd1d..daa8441 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobEngineSelectionTests.cs @@ -18,8 +18,9 @@ public class NestJobEngineSelectionTests var defaultResult = new NestJobRunner(PlateNesterFactory.Create).Solve(job); Assert.Equal(NestJobStatus.Complete, defaultResult.Status); - var stripResult = new NestJobRunner(PlateNesterFactory.Create) - .Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))); + var stripResult = new NestJobRunner(PlateNesterFactory.Create).Solve( + new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip")) + ); Assert.Equal(NestJobStatus.Complete, stripResult.Status); Assert.Equal(original, NestEngineRegistry.ActiveEngineName); @@ -48,8 +49,10 @@ public class NestJobEngineSelectionTests Assert.Throws(() => PlateNesterFactory.Create("Not A Real Engine")); var job = FiniteStockJobTests.Job(1); Assert.Throws(() => - new NestJobRunner(key => throw new NotSupportedException($"Unknown placement strategy: {key}")) - .Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Bogus")))); + new NestJobRunner(key => + throw new NotSupportedException($"Unknown placement strategy: {key}") + ).Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Bogus"))) + ); } [Fact] @@ -57,7 +60,11 @@ public class NestJobEngineSelectionTests { // A plugin engine registered through the legacy registry must not become selectable // through the job factory; the new boundary is independent of registry state. - NestEngineRegistry.Register("ProbePlugin", "test plugin", plate => new PluginShapeEngine(plate)); + NestEngineRegistry.Register( + "ProbePlugin", + "test plugin", + plate => new PluginShapeEngine(plate) + ); Assert.Contains(NestEngineRegistry.AvailableEngines, e => e.Name == "ProbePlugin"); Assert.Throws(() => PlateNesterFactory.Create("ProbePlugin")); @@ -68,10 +75,13 @@ public class NestJobEngineSelectionTests public void StripEngineEndToEndPlacesAndAccounts() { var drawing = new Drawing("strip part", TestDrawingFactory.Rectangle(30, 30)); - var job = new NestJob(new[] { DrawingJobMapper.FromDrawing("part", drawing, 2) }, - new[] { new NestPlateStock("s", new Size(90, 90), 1) }); - var result = new NestJobRunner(PlateNesterFactory.Create) - .Solve(new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip"))); + var job = new NestJob( + new[] { DrawingJobMapper.FromDrawing("part", drawing, 2) }, + new[] { new NestPlateStock("s", new Size(90, 90), 1) } + ); + var result = new NestJobRunner(PlateNesterFactory.Create).Solve( + new NestJob(job.Parts, job.Plates, new NestJobOptions("Strip")) + ); Assert.True(result.Plates.SelectMany(p => p.Placements).Count() >= 1); foreach (var f in result.Fulfillment) diff --git a/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs index 068429c..078fbb2 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobExampleTests.cs @@ -24,11 +24,24 @@ public class NestJobExampleTests // Mixed inventory: five large sheets and unlimited small sheets. new[] { - new NestPlateStock("large", new Size(600.0, 400.0), quantity: 5, partSpacing: 2.0, - edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1), - new NestPlateStock("small", new Size(300.0, 300.0), quantity: null, partSpacing: 2.0, - edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1), - }); + new NestPlateStock( + "large", + new Size(600.0, 400.0), + quantity: 5, + partSpacing: 2.0, + edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), + quadrant: 1 + ), + new NestPlateStock( + "small", + new Size(300.0, 300.0), + quantity: null, + partSpacing: 2.0, + edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), + quadrant: 1 + ), + } + ); var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job); @@ -36,22 +49,30 @@ public class NestJobExampleTests Console.WriteLine($"Status: {result.Status}, stop reason: {result.StopReason}."); foreach (var plate in result.Plates) { - Console.WriteLine($"Plate {plate.PlateIndex} from stock '{plate.StockId}' " + - $"({plate.Stock.Size.Width} x {plate.Stock.Size.Length}):"); + Console.WriteLine( + $"Plate {plate.PlateIndex} from stock '{plate.StockId}' " + + $"({plate.Stock.Size.Width} x {plate.Stock.Size.Length}):" + ); foreach (var placement in plate.Placements) - Console.WriteLine($" {placement.PartId} #{placement.InstanceIndex} at " + - $"({placement.X:F1}, {placement.Y:F1}) rotated {placement.Rotation:F3} rad."); + Console.WriteLine( + $" {placement.PartId} #{placement.InstanceIndex} at " + + $"({placement.X:F1}, {placement.Y:F1}) rotated {placement.Rotation:F3} rad." + ); } // -- Every requirement reports exact fulfillment, including leftovers. -- foreach (var fulfillment in result.Fulfillment) - Console.WriteLine($"Requirement '{fulfillment.PartId}': requested {fulfillment.Requested}, " + - $"placed {fulfillment.Placed}, unplaced {fulfillment.Unplaced}."); + Console.WriteLine( + $"Requirement '{fulfillment.PartId}': requested {fulfillment.Requested}, " + + $"placed {fulfillment.Placed}, unplaced {fulfillment.Unplaced}." + ); // -- Every stock line reports physical sheets used and remaining availability. -- foreach (var usage in result.StockUsage) - Console.WriteLine($"Stock '{usage.StockId}': used {usage.Used}, " + - $"remaining {(usage.Remaining.HasValue ? usage.Remaining.Value.ToString() : "unlimited")}."); + Console.WriteLine( + $"Stock '{usage.StockId}': used {usage.Used}, " + + $"remaining {(usage.Remaining.HasValue ? usage.Remaining.Value.ToString() : "unlimited")}." + ); // Invariants the enumeration relies on: conservation per requirement and per stock line, no // empty plates, every plate bound to supplied stock, and per-placement instance accounting. @@ -64,28 +85,45 @@ public class NestJobExampleTests { var stock = job.Plates.First(candidate => candidate.Id == usage.StockId); Assert.True(usage.Used >= 0); - Assert.Equal(stock.Quantity is int capacity ? capacity - usage.Used : (int?)null, usage.Remaining); + Assert.Equal( + stock.Quantity is int capacity ? capacity - usage.Used : (int?)null, + usage.Remaining + ); } Assert.All(result.Plates, plate => Assert.NotEmpty(plate.Placements)); - var plateCountByStock = result.Plates.GroupBy(plate => plate.StockId) + var plateCountByStock = result + .Plates.GroupBy(plate => plate.StockId) .ToDictionary(group => group.Key, group => group.Count()); foreach (var usage in result.StockUsage) Assert.Equal(usage.Used, plateCountByStock.GetValueOrDefault(usage.StockId)); - var instanceIndicesByPart = result.Plates - .SelectMany(plate => plate.Placements) + var instanceIndicesByPart = result + .Plates.SelectMany(plate => plate.Placements) .GroupBy(placement => placement.PartId) - .ToDictionary(group => group.Key, group => group.Select(placement => placement.InstanceIndex)); + .ToDictionary( + group => group.Key, + group => group.Select(placement => placement.InstanceIndex) + ); foreach (var fulfillment in result.Fulfillment) - Assert.Equal(Enumerable.Range(0, fulfillment.Placed), - instanceIndicesByPart.GetValueOrDefault(fulfillment.PartId, new List()).OrderBy(index => index)); + Assert.Equal( + Enumerable.Range(0, fulfillment.Placed), + instanceIndicesByPart + .GetValueOrDefault(fulfillment.PartId, new List()) + .OrderBy(index => index) + ); // The default heuristic completes this synthetic job from the mixed inventory. Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(NestJobStopReason.Completed, result.StopReason); - Assert.Equal(5, result.Fulfillment.Single(fulfillment => fulfillment.PartId == "bracket").Placed); - Assert.Equal(8, result.Fulfillment.Single(fulfillment => fulfillment.PartId == "plate-clip").Placed); + Assert.Equal( + 5, + result.Fulfillment.Single(fulfillment => fulfillment.PartId == "bracket").Placed + ); + Assert.Equal( + 8, + result.Fulfillment.Single(fulfillment => fulfillment.PartId == "plate-clip").Placed + ); } [Fact] @@ -94,9 +132,19 @@ public class NestJobExampleTests // Same shape of job, but a plate budget forces an explicit partial result. var job = new NestJob( new[] { Part("part", 100.0, 100.0, 6, priority: 0) }, - new[] { new NestPlateStock("sheet", new Size(220.0, 220.0), quantity: null, partSpacing: 2.0, - edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), quadrant: 1) }, - new NestJobOptions("Default", maxPlates: 1)); + new[] + { + new NestPlateStock( + "sheet", + new Size(220.0, 220.0), + quantity: null, + partSpacing: 2.0, + edgeSpacing: new Spacing(5.0, 5.0, 5.0, 5.0), + quadrant: 1 + ), + }, + new NestJobOptions("Default", maxPlates: 1) + ); var result = new NestJobRunner(PlateNesterFactory.Create).Solve(job); @@ -143,6 +191,17 @@ public class NestJobExampleTests Assert.Same(drawing, placed.BaseDrawing); } - private static NestJobPart Part(string id, double width, double length, int quantity, int priority) => - new(id, PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(width, length)), quantity, priority); + private static NestJobPart Part( + string id, + double width, + double length, + int quantity, + int priority + ) => + new( + id, + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(width, length)), + quantity, + priority + ); } diff --git a/OpenNest.Engine.Tests/Jobs/NestJobGeometryTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobGeometryTests.cs index f10da11..0e38063 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobGeometryTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobGeometryTests.cs @@ -7,12 +7,22 @@ public class NestJobGeometryTests [Fact] public void CandidateOutsideUsableWorkAreaFailsWithoutMutatingInput() { - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 1); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), + 1 + ); var sourceGeometry = part.Geometry.Motions.ToArray(); - var stock = new NestPlateStock("stock", new Size(20, 30), 1, - edgeSpacing: new Spacing(2, 1, 3, 4)); + var stock = new NestPlateStock( + "stock", + new Size(20, 30), + 1, + edgeSpacing: new Spacing(2, 1, 3, 4) + ); var job = new NestJob(new[] { part }, new[] { stock }); - var runner = new NestJobRunner(_ => new CandidateNester(new[] { new NestJobPlacement("part", 0, 26, 1, 0) })); + var runner = new NestJobRunner(_ => new CandidateNester( + new[] { new NestJobPlacement("part", 0, 26, 1, 0) } + )); Assert.Throws(() => runner.Solve(job)); @@ -28,48 +38,88 @@ public class NestJobGeometryTests [InlineData(4, 0, -7)] public void UnequalRectanglesFitAtEachQuadrantsUsableOrigin(int quadrant, double x, double y) { - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 3)), 1); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 3)), + 1 + ); var stock = new NestPlateStock("stock", new Size(7, 11), 1, quadrant: quadrant); var job = new NestJob(new[] { part }, new[] { stock }); var result = Solve(job, new NestJobPlacement("part", 0, x, y, 0)); Assert.Equal(NestJobStatus.Complete, result.Status); - Assert.Equal(new NestJobPlacement("part", 0, x, y, 0), Assert.Single(result.Plates[0].Placements)); + Assert.Equal( + new NestJobPlacement("part", 0, x, y, 0), + Assert.Single(result.Plates[0].Placements) + ); } [Fact] public void FixedAndBoundedRotationPoliciesRejectDisallowedAngles() { - var fixedPart = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)), 1, - rotation: RotationPolicy.Fixed(System.Math.PI / 2)); - var fixedJob = new NestJob(new[] { fixedPart }, new[] { new NestPlateStock("stock", new Size(10, 10), 1) }); - Assert.Throws(() => Solve(fixedJob, new NestJobPlacement("part", 0, 0, 0, 0))); + var fixedPart = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)), + 1, + rotation: RotationPolicy.Fixed(System.Math.PI / 2) + ); + var fixedJob = new NestJob( + new[] { fixedPart }, + new[] { new NestPlateStock("stock", new Size(10, 10), 1) } + ); + Assert.Throws(() => + Solve(fixedJob, new NestJobPlacement("part", 0, 0, 0, 0)) + ); - var fixedResult = Solve(fixedJob, new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 2)); + var fixedResult = Solve( + fixedJob, + new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 2) + ); Assert.Equal(NestJobStatus.Complete, fixedResult.Status); - var boundedPart = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)), 1, - rotation: RotationPolicy.BoundedSweep(0, System.Math.PI / 2, System.Math.PI / 4)); - var boundedJob = new NestJob(new[] { boundedPart }, new[] { new NestPlateStock("stock", new Size(10, 10), 1) }); - Assert.Throws(() => Solve(boundedJob, - new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 3))); + var boundedPart = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 2)), + 1, + rotation: RotationPolicy.BoundedSweep(0, System.Math.PI / 2, System.Math.PI / 4) + ); + var boundedJob = new NestJob( + new[] { boundedPart }, + new[] { new NestPlateStock("stock", new Size(10, 10), 1) } + ); + Assert.Throws(() => + Solve(boundedJob, new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 3)) + ); - var boundedResult = Solve(boundedJob, new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 4)); + var boundedResult = Solve( + boundedJob, + new NestJobPlacement("part", 0, 2, 0, System.Math.PI / 4) + ); Assert.Equal(NestJobStatus.Complete, boundedResult.Status); } [Fact] public void EdgeTouchingIsAllowedAtZeroSpacingAndRejectedAtPositiveSpacing() { - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 2); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), + 2 + ); var touching = new[] { new NestJobPlacement("part", 0, 0, 0, 0), - new NestJobPlacement("part", 1, 2, 0, 0) + new NestJobPlacement("part", 1, 2, 0, 0), }; - var zeroSpacing = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(10, 10), 1) }); - var positiveSpacing = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(10, 10), 1, 0.1) }); + var zeroSpacing = new NestJob( + new[] { part }, + new[] { new NestPlateStock("stock", new Size(10, 10), 1) } + ); + var positiveSpacing = new NestJob( + new[] { part }, + new[] { new NestPlateStock("stock", new Size(10, 10), 1, 0.1) } + ); Assert.Equal(NestJobStatus.Complete, Solve(zeroSpacing, touching).Status); Assert.Throws(() => Solve(positiveSpacing, touching)); @@ -78,20 +128,39 @@ public class NestJobGeometryTests [Fact] public void OverlapAndContainmentAreRejected() { - var outer = new NestJobPart("outer", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 6)), 1); - var inner = new NestJobPart("inner", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1); - var job = new NestJob(new[] { outer, inner }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) }); + var outer = new NestJobPart( + "outer", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 6)), + 1 + ); + var inner = new NestJobPart( + "inner", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), + 1 + ); + var job = new NestJob( + new[] { outer, inner }, + new[] { new NestPlateStock("stock", new Size(20, 20), 1) } + ); - Assert.Throws(() => Solve(job, - new NestJobPlacement("outer", 0, 0, 0, 0), - new NestJobPlacement("inner", 0, 2, 2, 0))); + Assert.Throws(() => + Solve( + job, + new NestJobPlacement("outer", 0, 0, 0, 0), + new NestJobPlacement("inner", 0, 2, 2, 0) + ) + ); } [Fact] public void EmptyStockStopsWithoutCallingCandidateNester() { var nester = new CountingNester(); - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), + 1 + ); var job = new NestJob(new[] { part }, Array.Empty()); var result = new NestJobRunner(_ => nester).Solve(job); @@ -115,25 +184,34 @@ public class NestJobGeometryTests var materialized = NestResultMaterializer.Materialize(job, result); Assert.Equal(NestJobStatus.Complete, result.Status); - Assert.All(result.Fulfillment, fulfillment => Assert.Equal(fulfillment.Requested, - fulfillment.Placed + fulfillment.Unplaced)); + Assert.All( + result.Fulfillment, + fulfillment => + Assert.Equal(fulfillment.Requested, fulfillment.Placed + fulfillment.Unplaced) + ); Assert.Equal(sourceGeometry, job.Parts[0].Geometry.Motions); Assert.Equal(3, job.Parts[0].Quantity); Assert.Equal(1, job.Plates[0].Quantity); - Assert.All(materialized.Nest.Plates, plate => - { - var workArea = plate.WorkArea(); - Assert.All(plate.Parts, placed => + Assert.All( + materialized.Nest.Plates, + plate => { - Assert.True(placed.BoundingBox.Left >= workArea.Left - 1e-6); - Assert.True(placed.BoundingBox.Right <= workArea.Right + 1e-6); - Assert.True(placed.BoundingBox.Bottom >= workArea.Bottom - 1e-6); - Assert.True(placed.BoundingBox.Top <= workArea.Top + 1e-6); - }); - for (var left = 0; left < plate.Parts.Count; left++) + var workArea = plate.WorkArea(); + Assert.All( + plate.Parts, + placed => + { + Assert.True(placed.BoundingBox.Left >= workArea.Left - 1e-6); + Assert.True(placed.BoundingBox.Right <= workArea.Right + 1e-6); + Assert.True(placed.BoundingBox.Bottom >= workArea.Bottom - 1e-6); + Assert.True(placed.BoundingBox.Top <= workArea.Top + 1e-6); + } + ); + for (var left = 0; left < plate.Parts.Count; left++) for (var right = left + 1; right < plate.Parts.Count; right++) Assert.False(plate.Parts[left].Intersects(plate.Parts[right], out _)); - }); + } + ); } private static NestJobResult Solve(NestJob job, params NestJobPlacement[] placements) => @@ -141,16 +219,22 @@ public class NestJobGeometryTests private sealed class CandidateNester(IEnumerable placements) : IPlateNester { - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) => new(placements); + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) => new(placements); } private sealed class CountingNester : IPlateNester { public int Calls { get; private set; } - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) { Calls++; return new PlateCandidate(Array.Empty()); diff --git a/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs index 568508c..d37e7b2 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobIdentityTests.cs @@ -16,17 +16,22 @@ public class NestJobIdentityTests { var a = new Drawing("identical", TestDrawingFactory.Rectangle(40, 40)); var b = new Drawing("identical", TestDrawingFactory.Rectangle(40, 40)); - var job = new NestJob(new[] - { - DrawingJobMapper.FromDrawing("a", a, 2), - DrawingJobMapper.FromDrawing("b", b, 2) - }, new[] { new NestPlateStock("s", new Size(90, 90), 1) }); + var job = new NestJob( + new[] + { + DrawingJobMapper.FromDrawing("a", a, 2), + DrawingJobMapper.FromDrawing("b", b, 2), + }, + new[] { new NestPlateStock("s", new Size(90, 90), 1) } + ); var result = new NestJobRunner(LegacyPlateNesterAdapter.Create).Solve(job); // Every placed part maps to a known requirement ID; no part is invented or cross-counted. Assert.True(result.Plates.SelectMany(p => p.Placements).All(p => p.PartId is "a" or "b")); - var counts = result.Plates.SelectMany(p => p.Placements).GroupBy(p => p.PartId) + var counts = result + .Plates.SelectMany(p => p.Placements) + .GroupBy(p => p.PartId) .ToDictionary(g => g.Key, g => g.Count()); foreach (var (id, placed) in counts) Assert.True(placed <= 2, $"Requirement {id} placed {placed} > requested 2"); @@ -40,11 +45,14 @@ public class NestJobIdentityTests public void TwoRequirementsOnSameSourceDrawingKeepIndependentQuantities() { var source = new Drawing("shared", TestDrawingFactory.Rectangle(30, 30)); - var job = new NestJob(new[] - { - DrawingJobMapper.FromDrawing("first", source, 2), - DrawingJobMapper.FromDrawing("second", source, 2) - }, new[] { new NestPlateStock("s", new Size(90, 90), 1) }); + var job = new NestJob( + new[] + { + DrawingJobMapper.FromDrawing("first", source, 2), + DrawingJobMapper.FromDrawing("second", source, 2), + }, + new[] { new NestPlateStock("s", new Size(90, 90), 1) } + ); var result = new NestJobRunner(LegacyPlateNesterAdapter.Create).Solve(job); @@ -72,7 +80,7 @@ public class NestJobIdentityTests var items = new List { new() { Drawing = a, Quantity = 2 }, - new() { Drawing = b, Quantity = 2 } + new() { Drawing = b, Quantity = 2 }, }; // Place exactly 2 parts from item A and none from item B, then run the base-class // deduction. Deterministic regardless of any fill heuristic. @@ -99,7 +107,7 @@ public class NestJobIdentityTests var items = new List { new() { Drawing = a, Quantity = 1 }, - new() { Drawing = b, Quantity = 1 } + new() { Drawing = b, Quantity = 1 }, }; var placed = new BaseNestEngineProbe(plate).Nest(items, null, default); Assert.Equal(2, placed.Count); @@ -110,15 +118,27 @@ public class NestJobIdentityTests { public override string Name => "probe"; public override string Description => "probe"; - public override List Fill(NestItem item, Box workArea, - IProgress progress, CancellationToken token) - => new DefaultNestEngine(Plate).Fill(item, workArea, progress, token); - public override List Fill(List groupParts, Box workArea, - IProgress progress, CancellationToken token) - => new DefaultNestEngine(Plate).Fill(groupParts, workArea, progress, token); - public override List PackArea(Box box, List items, - IProgress progress, CancellationToken token) - => new DefaultNestEngine(Plate).PackArea(box, items, progress, token); + + public override List Fill( + NestItem item, + Box workArea, + IProgress progress, + CancellationToken token + ) => new DefaultNestEngine(Plate).Fill(item, workArea, progress, token); + + public override List Fill( + List groupParts, + Box workArea, + IProgress progress, + CancellationToken token + ) => new DefaultNestEngine(Plate).Fill(groupParts, workArea, progress, token); + + public override List PackArea( + Box box, + List items, + IProgress progress, + CancellationToken token + ) => new DefaultNestEngine(Plate).PackArea(box, items, progress, token); } /// Places exactly 2 parts from the first multi-quantity item and none from the @@ -128,10 +148,16 @@ public class NestJobIdentityTests private int _first = -1; public override string Name => "starving"; public override string Description => "starves all but the first fill item"; - public override List Fill(NestItem item, Box workArea, - IProgress progress, CancellationToken token) + + public override List Fill( + NestItem item, + Box workArea, + IProgress progress, + CancellationToken token + ) { - if (_first < 0) _first = 1; + if (_first < 0) + _first = 1; if (_first++ != 1) return new List(); var parts = new List(); diff --git a/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs index 5b30faf..44eea9e 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobRunnerTests.cs @@ -10,12 +10,19 @@ public class NestJobRunnerTests { var fake = new FakePlateNester(); var factoryCalls = 0; - var runner = new NestJobRunner(_ => { factoryCalls++; return fake; }); - var job = new NestJob(Array.Empty(), new[] + var runner = new NestJobRunner(_ => { - new NestPlateStock("finite", new Size(100, 200), 2), - new NestPlateStock("unlimited", new Size(100, 200)) + factoryCalls++; + return fake; }); + var job = new NestJob( + Array.Empty(), + new[] + { + new NestPlateStock("finite", new Size(100, 200), 2), + new NestPlateStock("unlimited", new Size(100, 200)), + } + ); var result = runner.Solve(job); @@ -23,9 +30,19 @@ public class NestJobRunnerTests Assert.Equal(NestJobStopReason.Completed, result.StopReason); Assert.Empty(result.Plates); Assert.Empty(result.Fulfillment); - Assert.Collection(result.StockUsage, - usage => { Assert.Equal(0, usage.Used); Assert.Equal(2, usage.Remaining); }, - usage => { Assert.Equal(0, usage.Used); Assert.Null(usage.Remaining); }); + Assert.Collection( + result.StockUsage, + usage => + { + Assert.Equal(0, usage.Used); + Assert.Equal(2, usage.Remaining); + }, + usage => + { + Assert.Equal(0, usage.Used); + Assert.Null(usage.Remaining); + } + ); Assert.Equal(0, factoryCalls); Assert.Equal(0, fake.Calls); } @@ -37,13 +54,19 @@ public class NestJobRunnerTests cancellation.Cancel(); var runner = new NestJobRunner(_ => new FakePlateNester()); var job = new NestJob(Array.Empty(), Array.Empty()); - Assert.Throws(() => runner.Solve(job, token: cancellation.Token)); + Assert.Throws(() => + runner.Solve(job, token: cancellation.Token) + ); } [Fact] public void EmptyStockReturnsIncomplete() { - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), 1); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle()), + 1 + ); var job = new NestJob(new[] { part }, Array.Empty()); var runner = new NestJobRunner(_ => new FakePlateNester()); var result = runner.Solve(job); @@ -64,7 +87,11 @@ public class NestJobRunnerTests var edges = new Spacing(1, 2, 3, 4); var stocks = new List { new("s", size, 0, 2, edges, 3) }; var job = new NestJob(parts, stocks); - parts.Clear(); stocks.Clear(); program.Codes.Clear(); size.Width = 0; edges.Left = 999; + parts.Clear(); + stocks.Clear(); + program.Codes.Clear(); + size.Width = 0; + edges.Left = 999; Assert.Single(job.Parts); Assert.Equal(3, job.Parts[0].Quantity); @@ -92,8 +119,12 @@ public class NestJobRunnerTests private sealed class FakePlateNester : IPlateNester { public int Calls { get; private set; } - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) + + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) { Calls++; return new PlateCandidate(Array.Empty()); diff --git a/OpenNest.Engine.Tests/Jobs/NestJobStockSelectionTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobStockSelectionTests.cs index f81837a..81526d6 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobStockSelectionTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobStockSelectionTests.cs @@ -7,8 +7,11 @@ public class NestJobStockSelectionTests [Fact] public void LaterFittingStockWinsWhenFirstStockCannotPlace() { - var result = Solve(new[] { Part("p", 1) }, new[] { Stock("small", 10, 10, 1), Stock("large", 20, 20, 1) }, - request => request.Stock.Id == "large" ? Candidate(request, "p") : Empty()); + var result = Solve( + new[] { Part("p", 1) }, + new[] { Stock("small", 10, 10, 1), Stock("large", 20, 20, 1) }, + request => request.Stock.Id == "large" ? Candidate(request, "p") : Empty() + ); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal("large", Assert.Single(result.Plates).StockId); @@ -17,58 +20,103 @@ public class NestJobStockSelectionTests [Fact] public void ExhaustedLargeStockIsNotRecreatedWhileSmallerStockServesSmallParts() { - var result = Solve(new[] { Part("large", 1, 0), Part("small", 2, 1) }, - new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 2) }, request => request.Stock.Id switch - { - "large" when request.Parts.Any(part => part.Id == "large") => Candidate(request, "large"), - "small" when request.Parts.Any(part => part.Id == "small") => Candidate(request, "small"), - _ => Empty() - }); + var result = Solve( + new[] { Part("large", 1, 0), Part("small", 2, 1) }, + new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 2) }, + request => + request.Stock.Id switch + { + "large" when request.Parts.Any(part => part.Id == "large") => Candidate( + request, + "large" + ), + "small" when request.Parts.Any(part => part.Id == "small") => Candidate( + request, + "small" + ), + _ => Empty(), + } + ); - Assert.Equal(new[] { "large", "small", "small" }, result.Plates.Select(plate => plate.StockId)); - Assert.Collection(result.StockUsage, + Assert.Equal( + new[] { "large", "small", "small" }, + result.Plates.Select(plate => plate.StockId) + ); + Assert.Collection( + result.StockUsage, usage => Assert.Equal(new StockUsage("large", 1, 0), usage), - usage => Assert.Equal(new StockUsage("small", 2, 0), usage)); + usage => Assert.Equal(new StockUsage("small", 2, 0), usage) + ); } [Fact] public void EqualDimensionsWithDifferentStockIdsRemainIndependent() { - var result = Solve(new[] { Part("p", 2) }, new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) }, - request => Candidate(request, "p")); + var result = Solve( + new[] { Part("p", 2) }, + new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) }, + request => Candidate(request, "p") + ); Assert.Equal(new[] { "first", "second" }, result.Plates.Select(plate => plate.StockId)); - Assert.Equal(new[] { new StockUsage("first", 1, 0), new StockUsage("second", 1, 0) }, result.StockUsage); + Assert.Equal( + new[] { new StockUsage("first", 1, 0), new StockUsage("second", 1, 0) }, + result.StockUsage + ); } [Fact] public void LosingTrialsDoNotConsumeStockPartsOrDrawingCounters() { var calls = new List<(string Stock, int Quantity)>(); - var result = Solve(new[] { Part("p", 2) }, new[] { Stock("wide", 20, 20, 2), Stock("narrow", 10, 10, 2) }, request => - { - calls.Add((request.Stock.Id, request.Parts.Single().Quantity)); - return request.Stock.Id == "wide" ? Candidate(request, "p", 0, 100) : Candidate(request, "p", 0, 0); - }); + var result = Solve( + new[] { Part("p", 2) }, + new[] { Stock("wide", 20, 20, 2), Stock("narrow", 10, 10, 2) }, + request => + { + calls.Add((request.Stock.Id, request.Parts.Single().Quantity)); + return request.Stock.Id == "wide" + ? Candidate(request, "p", 0, 100) + : Candidate(request, "p", 0, 0); + } + ); Assert.Equal(new[] { "narrow", "narrow" }, result.Plates.Select(plate => plate.StockId)); Assert.Equal(new[] { ("wide", 2), ("narrow", 2), ("wide", 1), ("narrow", 1) }, calls); Assert.Equal(new StockUsage("wide", 0, 2), result.StockUsage[0]); Assert.Equal(new StockUsage("narrow", 2, 0), result.StockUsage[1]); - Assert.Equal(new[] { 0, 1 }, result.Plates.SelectMany(plate => plate.Placements).Select(placement => placement.InstanceIndex)); + Assert.Equal( + new[] { 0, 1 }, + result + .Plates.SelectMany(plate => plate.Placements) + .Select(placement => placement.InstanceIndex) + ); } [Fact] public void CandidatePriorityAreaEnvelopeAndInputOrderAreComparedInDocumentedOrder() { - var priority = Solve(new[] { Part("high", 1, 0), Part("low", 1, 1) }, new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) }, - request => request.Stock.Id == "a" ? Candidate(request, "low") : Candidate(request, "high")); - var area = Solve(new[] { Part("p", 1) }, new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 1) }, - request => Candidate(request, "p")); - var envelope = Solve(new[] { Part("p", 2) }, new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) }, - request => request.Stock.Id == "a" ? CandidatePair("p", 4, 5) : CandidatePair("p", 4, 0)); - var inputOrder = Solve(new[] { Part("p", 1) }, new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) }, - request => Candidate(request, "p")); + var priority = Solve( + new[] { Part("high", 1, 0), Part("low", 1, 1) }, + new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) }, + request => + request.Stock.Id == "a" ? Candidate(request, "low") : Candidate(request, "high") + ); + var area = Solve( + new[] { Part("p", 1) }, + new[] { Stock("large", 20, 20, 1), Stock("small", 10, 10, 1) }, + request => Candidate(request, "p") + ); + var envelope = Solve( + new[] { Part("p", 2) }, + new[] { Stock("a", 10, 10, 1), Stock("b", 10, 10, 1) }, + request => request.Stock.Id == "a" ? CandidatePair("p", 4, 5) : CandidatePair("p", 4, 0) + ); + var inputOrder = Solve( + new[] { Part("p", 1) }, + new[] { Stock("first", 10, 10, 1), Stock("second", 10, 10, 1) }, + request => Candidate(request, "p") + ); Assert.Equal("b", priority.Plates[0].StockId); Assert.Equal("small", area.Plates[0].StockId); @@ -79,8 +127,17 @@ public class NestJobStockSelectionTests [Fact] public void UnlimitedStockStopsWhenDemandIsFulfilledAndPlateLimitLeavesLeftovers() { - var unlimited = Solve(new[] { Part("p", 2) }, new[] { Stock("u", 10, 10, null) }, request => Candidate(request, "p")); - var limited = Solve(new[] { Part("p", 3) }, new[] { Stock("u", 10, 10, null) }, request => Candidate(request, "p"), new NestJobOptions(maxPlates: 2)); + var unlimited = Solve( + new[] { Part("p", 2) }, + new[] { Stock("u", 10, 10, null) }, + request => Candidate(request, "p") + ); + var limited = Solve( + new[] { Part("p", 3) }, + new[] { Stock("u", 10, 10, null) }, + request => Candidate(request, "p"), + new NestJobOptions(maxPlates: 2) + ); Assert.Equal(NestJobStopReason.Completed, unlimited.StopReason); Assert.Equal(2, unlimited.Plates.Count); @@ -88,32 +145,51 @@ public class NestJobStockSelectionTests Assert.Equal(new PartFulfillment("p", 3, 2, 1), Assert.Single(limited.Fulfillment)); } - private static NestJobResult Solve(IEnumerable parts, IEnumerable stock, - Func place, NestJobOptions? options = null) => - new NestJobRunner(_ => new Nester(place)).Solve(new NestJob(parts, stock, options)); + private static NestJobResult Solve( + IEnumerable parts, + IEnumerable stock, + Func place, + NestJobOptions? options = null + ) => new NestJobRunner(_ => new Nester(place)).Solve(new NestJob(parts, stock, options)); private static NestJobPart Part(string id, int quantity, int priority = 0) => - new(id, PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 5)), quantity, priority); + new( + id, + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 5)), + quantity, + priority + ); private static NestPlateStock Stock(string id, double width, double length, int? quantity) => new(id, new Size(width, length), quantity); - private static PlateCandidate Candidate(PlatePlacementRequest request, string id, double firstX = 0, double secondX = 0) + private static PlateCandidate Candidate( + PlatePlacementRequest request, + string id, + double firstX = 0, + double secondX = 0 + ) { return new PlateCandidate(new[] { new NestJobPlacement(id, 0, firstX, 0, 0) }); } - private static PlateCandidate CandidatePair(string id, double secondX, double secondY) => new(new[] - { - new NestJobPlacement(id, 0, 0, 0, 0), - new NestJobPlacement(id, 1, secondX, secondY, 0) - }); + private static PlateCandidate CandidatePair(string id, double secondX, double secondY) => + new( + new[] + { + new NestJobPlacement(id, 0, 0, 0, 0), + new NestJobPlacement(id, 1, secondX, secondY, 0), + } + ); private static PlateCandidate Empty() => new(Array.Empty()); private sealed class Nester(Func place) : IPlateNester { - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) => place(request); + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) => place(request); } } diff --git a/OpenNest.Engine.Tests/Jobs/NestJobValidationTests.cs b/OpenNest.Engine.Tests/Jobs/NestJobValidationTests.cs index 2d751cb..336e905 100644 --- a/OpenNest.Engine.Tests/Jobs/NestJobValidationTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestJobValidationTests.cs @@ -14,10 +14,10 @@ public class NestJobValidationTests program.LineTo(0, 3); program.LineTo(-4, 0); program.LineTo(0, -3); - var job = new NestJob(new[] - { - new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1) - }, new[] { new NestPlateStock("stock", new Size(10, 10), 1) }); + var job = new NestJob( + new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1) }, + new[] { new NestPlateStock("stock", new Size(10, 10), 1) } + ); var result = Solve(job, new NestJobPlacement("part", 0, 0, 0, 0)); @@ -28,13 +28,26 @@ public class NestJobValidationTests [Fact] public void CandidateInsideAnotherRequirementsHoleDoesNotOverlapMaterial() { - var outer = new NestJobPart("outer", PartGeometrySnapshot.FromProgram(RectangleWithHole()), 1); - var inner = new NestJobPart("inner", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1); - var job = new NestJob(new[] { outer, inner }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) }); + var outer = new NestJobPart( + "outer", + PartGeometrySnapshot.FromProgram(RectangleWithHole()), + 1 + ); + var inner = new NestJobPart( + "inner", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), + 1 + ); + var job = new NestJob( + new[] { outer, inner }, + new[] { new NestPlateStock("stock", new Size(20, 20), 1) } + ); - var result = Solve(job, + var result = Solve( + job, new NestJobPlacement("outer", 0, 0, 0, 0), - new NestJobPlacement("inner", 0, 4, 4, 0)); + new NestJobPlacement("inner", 0, 4, 4, 0) + ); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Single(result.Plates); @@ -44,12 +57,23 @@ public class NestJobValidationTests [Fact] public void SmallCornerOverlapIsRejected() { - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)), 2); - var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) }); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)), + 2 + ); + var job = new NestJob( + new[] { part }, + new[] { new NestPlateStock("stock", new Size(20, 20), 1) } + ); - Assert.Throws(() => Solve(job, - new NestJobPlacement("part", 0, 0, 0, 0), - new NestJobPlacement("part", 1, 9, 9, 0))); + Assert.Throws(() => + Solve( + job, + new NestJobPlacement("part", 0, 0, 0, 0), + new NestJobPlacement("part", 1, 9, 9, 0) + ) + ); } [Theory] @@ -57,12 +81,21 @@ public class NestJobValidationTests [InlineData(10.0, 10.0)] public void BoundaryContactWithZeroSpacingIsAccepted(double x, double y) { - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)), 2); - var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) }); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(10, 10)), + 2 + ); + var job = new NestJob( + new[] { part }, + new[] { new NestPlateStock("stock", new Size(20, 20), 1) } + ); - var result = Solve(job, + var result = Solve( + job, new NestJobPlacement("part", 0, 0, 0, 0), - new NestJobPlacement("part", 1, x, y, 0)); + new NestJobPlacement("part", 1, x, y, 0) + ); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(2, Assert.Single(result.Plates).Placements.Count); @@ -72,16 +105,24 @@ public class NestJobValidationTests public void UnknownOrOverproducingCandidateFailsBeforeCommitWithoutChangingInput() { var reports = new List(); - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), + 1 + ); var stock = new NestPlateStock("stock", new Size(20, 20), 1); var job = new NestJob(new[] { part }, new[] { stock }); - var runner = new NestJobRunner(_ => new CandidateNester(new[] - { - new NestJobPlacement("part", 0, 0, 0, 0), - new NestJobPlacement("unknown", 0, 4, 0, 0) - })); + var runner = new NestJobRunner(_ => new CandidateNester( + new[] + { + new NestJobPlacement("part", 0, 0, 0, 0), + new NestJobPlacement("unknown", 0, 4, 0, 0), + } + )); - Assert.Throws(() => runner.Solve(job, new InlineProgress(reports.Add))); + Assert.Throws(() => + runner.Solve(job, new InlineProgress(reports.Add)) + ); Assert.Equal(1, job.Parts[0].Quantity); Assert.Equal(1, job.Plates[0].Quantity); @@ -91,12 +132,23 @@ public class NestJobValidationTests [Fact] public void CandidateThatOverproducesIsRejectedRatherThanClamped() { - var part = new NestJobPart("part", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), 1); - var job = new NestJob(new[] { part }, new[] { new NestPlateStock("stock", new Size(20, 20), 1) }); + var part = new NestJobPart( + "part", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(2, 2)), + 1 + ); + var job = new NestJob( + new[] { part }, + new[] { new NestPlateStock("stock", new Size(20, 20), 1) } + ); - Assert.Throws(() => Solve(job, - new NestJobPlacement("part", 0, 0, 0, 0), - new NestJobPlacement("part", 1, 4, 0, 0))); + Assert.Throws(() => + Solve( + job, + new NestJobPlacement("part", 0, 0, 0, 0), + new NestJobPlacement("part", 1, 4, 0, 0) + ) + ); Assert.Equal(1, job.Parts[0].Quantity); Assert.Equal(1, job.Plates[0].Quantity); @@ -110,14 +162,20 @@ public class NestJobValidationTests var program = new Program(); program.MoveTo(0, 0); program.LineTo(4, 0); - if (zeroLength) program.LineTo(4, 0); + if (zeroLength) + program.LineTo(4, 0); program.LineTo(4, 3); program.LineTo(0, 3); - if (zeroLength) program.LineTo(0, 0); - var job = new NestJob(new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1) }, - new[] { new NestPlateStock("stock", new Size(20, 20), 1) }); + if (zeroLength) + program.LineTo(0, 0); + var job = new NestJob( + new[] { new NestJobPart("part", PartGeometrySnapshot.FromProgram(program), 1) }, + new[] { new NestPlateStock("stock", new Size(20, 20), 1) } + ); - Assert.Throws(() => new NestJobRunner(_ => new CandidateNester(Array.Empty())).Solve(job)); + Assert.Throws(() => + new NestJobRunner(_ => new CandidateNester(Array.Empty())).Solve(job) + ); } private static NestJobResult Solve(NestJob job, params NestJobPlacement[] placements) => @@ -136,8 +194,11 @@ public class NestJobValidationTests private sealed class CandidateNester(IEnumerable placements) : IPlateNester { - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) => new(placements); + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) => new(placements); } private sealed class InlineProgress(Action report) : IProgress diff --git a/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs b/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs index 0766bca..6dc6386 100644 --- a/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs +++ b/OpenNest.Engine.Tests/Jobs/NestingEngineRegistryTests.cs @@ -32,7 +32,11 @@ public class NestingEngineRegistryTests { var before = NestingEngineRegistry.AvailableEngines.Count; - NestingEngineRegistry.Register("Default", "duplicate", () => new FixedStrategyNestingEngine("Default")); + NestingEngineRegistry.Register( + "Default", + "duplicate", + () => new FixedStrategyNestingEngine("Default") + ); Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count); } @@ -42,7 +46,9 @@ public class NestingEngineRegistryTests { var before = NestingEngineRegistry.AvailableEngines.Count; - NestingEngineRegistry.LoadPlugins(Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString())); + NestingEngineRegistry.LoadPlugins( + Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()) + ); Assert.Equal(before, NestingEngineRegistry.AvailableEngines.Count); } diff --git a/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs b/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs index aa6058b..b127b6a 100644 --- a/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs +++ b/OpenNest.Engine.Tests/Jobs/PlateNesterParityTests.cs @@ -17,8 +17,11 @@ public class PlateNesterParityTests private static readonly Size PlateSize = new(30, 50); private static readonly Spacing Edge = new(1, 1, 1, 1); - private static NestJob Job(IReadOnlyList parts, int? stockQuantity = 3, - string strategy = "Default") + private static NestJob Job( + IReadOnlyList parts, + int? stockQuantity = 3, + string strategy = "Default" + ) { var stock = new NestPlateStock("stock", PlateSize, stockQuantity, 1, Edge); return new NestJob(parts, new[] { stock }, new NestJobOptions(strategy)); @@ -45,8 +48,10 @@ public class PlateNesterParityTests Assert.Equal(lSorted[j].PartId, rSorted[j].PartId); Assert.Equal(lSorted[j].X, rSorted[j].X, 6); Assert.Equal(lSorted[j].Y, rSorted[j].Y, 6); - Assert.True(AnglesEqual(lSorted[j].Rotation, rSorted[j].Rotation), - $"rotation differs: {lSorted[j].Rotation} vs {rSorted[j].Rotation}"); + Assert.True( + AnglesEqual(lSorted[j].Rotation, rSorted[j].Rotation), + $"rotation differs: {lSorted[j].Rotation} vs {rSorted[j].Rotation}" + ); } } } @@ -54,8 +59,8 @@ public class PlateNesterParityTests private static bool AnglesEqual(double left, double right) { var delta = (left - right) % (System.Math.PI * 2); - return System.Math.Abs(delta) <= Tolerance || - System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Tolerance; + return System.Math.Abs(delta) <= Tolerance + || System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Tolerance; } [Fact] @@ -63,18 +68,32 @@ public class PlateNesterParityTests { var parts = new[] { - new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4), - new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3) + new NestJobPart( + "a", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), + 4 + ), + new NestJobPart( + "b", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), + 3 + ), }; - var legacy = Solve(new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)), Job(parts)); + var legacy = Solve( + new LegacyPlateNesterAdapter(plate => new DefaultNestEngine(plate)), + Job(parts) + ); var migrated = Solve(new DefaultPlateNester(), Job(parts)); Assert.Equal(legacy.Status, migrated.Status); Assert.Equal(NestJobStatus.Complete, migrated.Status); Assert.Equal(ByPart(legacy), ByPart(migrated)); foreach (var usage in legacy.StockUsage) - Assert.Equal(usage.Used, migrated.StockUsage.First(u => u.StockId == usage.StockId).Used); + Assert.Equal( + usage.Used, + migrated.StockUsage.First(u => u.StockId == usage.StockId).Used + ); // Automatic-rotation rectangles on a single stock size are deterministic: identical layouts. AssertLayoutsIdentical(legacy, migrated); } @@ -84,19 +103,31 @@ public class PlateNesterParityTests { var parts = new[] { - new NestJobPart("a", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 4), - new NestJobPart("b", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 3) + new NestJobPart( + "a", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), + 4 + ), + new NestJobPart( + "b", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), + 3 + ), }; - var legacy = Solve(new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)), - Job(parts, strategy: "Strip")); + var legacy = Solve( + new LegacyPlateNesterAdapter(plate => new StripNestEngine(plate)), + Job(parts, strategy: "Strip") + ); var migrated = Solve(new StripPlateNester(), Job(parts, strategy: "Strip")); Assert.Equal(legacy.Status, migrated.Status); Assert.Equal(NestJobStatus.Complete, migrated.Status); Assert.Equal(ByPart(legacy), ByPart(migrated)); - Assert.Equal(legacy.Plates.SelectMany(p => p.Placements).Count(), - migrated.Plates.SelectMany(p => p.Placements).Count()); + Assert.Equal( + legacy.Plates.SelectMany(p => p.Placements).Count(), + migrated.Plates.SelectMany(p => p.Placements).Count() + ); // Shrink-fill ordering can differ between engine instances; do not assert identical coordinates. } @@ -125,7 +156,11 @@ public class PlateNesterParityTests var parts = new[] { new NestJobPart("l", PartGeometrySnapshot.FromProgram(lshape), 3), - new NestJobPart("sq", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), 2) + new NestJobPart( + "sq", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(4, 3)), + 2 + ), }; var result = Solve(new DefaultPlateNester(), Job(parts)); @@ -161,7 +196,7 @@ public class PlateNesterParityTests var parts = new[] { new NestJobPart("holed", PartGeometrySnapshot.FromProgram(holed), 2), - new NestJobPart("arc", PartGeometrySnapshot.FromProgram(arc), 2) + new NestJobPart("arc", PartGeometrySnapshot.FromProgram(arc), 2), }; var result = Solve(new DefaultPlateNester(), Job(parts)); @@ -173,15 +208,22 @@ public class PlateNesterParityTests [Fact] public void FixedRotation_Respected() { - var part = new NestJobPart("fixed", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), - 2, rotation: RotationPolicy.Fixed(0)); + var part = new NestJobPart( + "fixed", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), + 2, + rotation: RotationPolicy.Fixed(0) + ); var result = Solve(new DefaultPlateNester(), Job(new[] { part })); Assert.Equal(NestJobStatus.Complete, result.Status); var placements = result.Plates.SelectMany(p => p.Placements).ToList(); Assert.Equal(2, placements.Count); foreach (var placement in placements) - Assert.True(AnglesEqual(placement.Rotation, 0), $"fixed rotation violated: {placement.Rotation}"); + Assert.True( + AnglesEqual(placement.Rotation, 0), + $"fixed rotation violated: {placement.Rotation}" + ); } [Fact] @@ -192,7 +234,7 @@ public class PlateNesterParityTests var parts = new[] { new NestJobPart("first", PartGeometrySnapshot.FromProgram(program), 2), - new NestJobPart("second", PartGeometrySnapshot.FromProgram(program), 1) + new NestJobPart("second", PartGeometrySnapshot.FromProgram(program), 1), }; var result = Solve(new DefaultPlateNester(), Job(parts)); @@ -229,10 +271,16 @@ public class PlateNesterParityTests // 14x9 parts on 30x20: one sheet holds fewer than five, so the runner runs multiple candidate // trials through the same nester instance. The run-scoped drawing cache must keep producing // valid, correctly-attributed placements across trials. - var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(14, 9)), 5); + var part = new NestJobPart( + "p", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(14, 9)), + 5 + ); var stock = new NestPlateStock("stock", new Size(30, 20), 3); var nester = new DefaultPlateNester(); - var result = new NestJobRunner(_ => nester).Solve(new NestJob(new[] { part }, new[] { stock })); + var result = new NestJobRunner(_ => nester).Solve( + new NestJob(new[] { part }, new[] { stock }) + ); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(5, result.Fulfillment.Single(f => f.PartId == "p").Placed); @@ -246,10 +294,17 @@ public class PlateNesterParityTests public void LegacyRemnantStrategies_StillResolveThroughAdapter() { // Remnant strategies must keep working through the legacy adapter after the factory change. - var part = new NestJobPart("p", PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), 2); + var part = new NestJobPart( + "p", + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(6, 4)), + 2 + ); foreach (var strategy in new[] { "Vertical Remnant", "Horizontal Remnant" }) { - var result = Solve(PlateNesterFactory.Create(strategy), Job(new[] { part }, strategy: strategy)); + var result = Solve( + PlateNesterFactory.Create(strategy), + Job(new[] { part }, strategy: strategy) + ); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal(2, result.Fulfillment.Single(f => f.PartId == "p").Placed); } diff --git a/OpenNest.Engine.Tests/Jobs/StockLadderTests.cs b/OpenNest.Engine.Tests/Jobs/StockLadderTests.cs index a44aa27..09a0c98 100644 --- a/OpenNest.Engine.Tests/Jobs/StockLadderTests.cs +++ b/OpenNest.Engine.Tests/Jobs/StockLadderTests.cs @@ -4,19 +4,31 @@ namespace OpenNest.Engine.Tests.Jobs; public class StockLadderTests { - private static NestJobPart Rectangle(string id, int quantity, double x = 4, double y = 4, - RotationPolicy? rotation = null) => new(id, - PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(x, y)), quantity, - rotation: rotation ?? RotationPolicy.Fixed(0)); + private static NestJobPart Rectangle( + string id, + int quantity, + double x = 4, + double y = 4, + RotationPolicy? rotation = null + ) => + new( + id, + PartGeometrySnapshot.FromProgram(TestDrawingFactory.Rectangle(x, y)), + quantity, + rotation: rotation ?? RotationPolicy.Fixed(0) + ); [Fact] public void MergesEquivalentDemandOntoLargerSheetAndReturnsFiniteStock() { - var job = new NestJob(new[] { Rectangle("a", 5) }, new[] - { - new NestPlateStock("small", new Size(10, 10), 2), - new NestPlateStock("large", new Size(10, 18), 1) - }); + var job = new NestJob( + new[] { Rectangle("a", 5) }, + new[] + { + new NestPlateStock("small", new Size(10, 10), 2), + new NestPlateStock("large", new Size(10, 18), 1), + } + ); var result = new StockLadderNestingEngine().Solve(job); Assert.Equal(NestJobStatus.Complete, result.Status); Assert.Equal("large", Assert.Single(result.Plates).StockId); @@ -36,7 +48,11 @@ public class StockLadderTests Assert.Equal(NestJobStopReason.StockExhausted, result.StopReason); Assert.Equal(4, result.Fulfillment[0].Placed); Verify(job, result); - job = new NestJob(parts, new[] { new NestPlateStock("only", new Size(10, 10)) }, new NestJobOptions(maxPlates: 1)); + job = new NestJob( + parts, + new[] { new NestPlateStock("only", new Size(10, 10)) }, + new NestJobOptions(maxPlates: 1) + ); result = new StockLadderNestingEngine().Solve(job); Assert.Equal(NestJobStopReason.PlateLimitReached, result.StopReason); Assert.Single(result.Plates); @@ -46,11 +62,14 @@ public class StockLadderTests [Fact] public void ConstrainedLargeSinglePrecedesSmallFillers() { - var job = new NestJob(new[] { Rectangle("small", 12, 2, 2), Rectangle("large", 1, 12, 6) }, new[] - { - new NestPlateStock("small-sheet", new Size(10, 10)), - new NestPlateStock("large-sheet", new Size(10, 18)) - }); + var job = new NestJob( + new[] { Rectangle("small", 12, 2, 2), Rectangle("large", 1, 12, 6) }, + new[] + { + new NestPlateStock("small-sheet", new Size(10, 10)), + new NestPlateStock("large-sheet", new Size(10, 18)), + } + ); var result = new StockLadderNestingEngine().Solve(job); Assert.Equal("large", result.Plates[0].Placements[0].PartId); Assert.Contains(result.Plates[0].Placements, p => p.PartId == "small"); @@ -59,12 +78,31 @@ public class StockLadderTests } [Theory] - [InlineData(1)] [InlineData(2)] [InlineData(3)] [InlineData(4)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] public void GeometrySpacingRotationsAndQuadrantsAreValidated(int quadrant) { - var job = new NestJob(new[] { Rectangle("a", 6, 3, 5, RotationPolicy.Fixed(System.Math.PI / 2)) }, - new[] { new NestPlateStock("sheet", new Size(12, 18), partSpacing: 0.25, - edgeSpacing: new Spacing { Left = 0.5, Right = 0.5, Top = 0.5, Bottom = 0.5 }, quadrant: quadrant) }); + var job = new NestJob( + new[] { Rectangle("a", 6, 3, 5, RotationPolicy.Fixed(System.Math.PI / 2)) }, + new[] + { + new NestPlateStock( + "sheet", + new Size(12, 18), + partSpacing: 0.25, + edgeSpacing: new Spacing + { + Left = 0.5, + Right = 0.5, + Top = 0.5, + Bottom = 0.5, + }, + quadrant: quadrant + ), + } + ); var result = new StockLadderNestingEngine().Solve(job); Assert.Equal(NestJobStatus.Complete, result.Status); Verify(job, result); @@ -73,8 +111,10 @@ public class StockLadderTests [Fact] public void ImpossibleDemandTerminatesWithoutUsingUnlimitedStock() { - var job = new NestJob(new[] { Rectangle("a", 1, 100, 100) }, - new[] { new NestPlateStock("sheet", new Size(10, 10)) }); + var job = new NestJob( + new[] { Rectangle("a", 1, 100, 100) }, + new[] { new NestPlateStock("sheet", new Size(10, 10)) } + ); var result = new StockLadderNestingEngine().Solve(job); Assert.Equal(NestJobStopReason.NoPlacementFound, result.StopReason); Assert.Empty(result.Plates); @@ -83,31 +123,56 @@ public class StockLadderTests [Fact] public void CancellationBeforeAndDuringTrialNeverReturnsPartialSuccess() { - var job = new NestJob(new[] { Rectangle("a", 1) }, new[] { new NestPlateStock("s", new Size(10, 10)) }); + var job = new NestJob( + new[] { Rectangle("a", 1) }, + new[] { new NestPlateStock("s", new Size(10, 10)) } + ); using var cts = new CancellationTokenSource(); - var engine = new StockLadderNestingEngine(() => new CallbackNester(request => - { - cts.Cancel(); - return new PlateCandidate(Array.Empty()); - })); + var engine = new StockLadderNestingEngine(() => + new CallbackNester(request => + { + cts.Cancel(); + return new PlateCandidate(Array.Empty()); + }) + ); Assert.Throws(() => engine.Solve(job, token: cts.Token)); - Assert.Throws(() => new StockLadderNestingEngine().Solve(job, token: cts.Token)); + Assert.Throws(() => + new StockLadderNestingEngine().Solve(job, token: cts.Token) + ); } [Theory] - [InlineData(false)] [InlineData(true)] + [InlineData(false)] + [InlineData(true)] public void RejectsOverlappingOrOverproducingNester(bool overproduce) { - var job = new NestJob(new[] { Rectangle("a", 2) }, new[] { new NestPlateStock("s", new Size(10, 10)) }); - var engine = new StockLadderNestingEngine(() => new CallbackNester(request => - new PlateCandidate(overproduce - ? Enumerable.Repeat(new NestJobPlacement("a", 0, 0, 0, 0), 3) - : new[] { new NestJobPlacement("a", 0, 50, 0, 0) }))); + var job = new NestJob( + new[] { Rectangle("a", 2) }, + new[] { new NestPlateStock("s", new Size(10, 10)) } + ); + var engine = new StockLadderNestingEngine(() => + new CallbackNester(request => new PlateCandidate( + overproduce + ? Enumerable.Repeat(new NestJobPlacement("a", 0, 0, 0, 0), 3) + : new[] { new NestJobPlacement("a", 0, 50, 0, 0) } + )) + ); Assert.Throws(() => engine.Solve(job)); // Direct full-demand overlap check, not masked by the single-part feasibility probe limit. - Assert.Throws(() => NestJobValidator.ValidateCandidate( - new PlateCandidate(new[] { new NestJobPlacement("a", 0, 0, 0, 0), new NestJobPlacement("a", 1, 1, 1, 0) }), - job.Plates[0], new Dictionary { ["a"] = 2 }, job.Parts.ToDictionary(p => p.Id))); + Assert.Throws(() => + NestJobValidator.ValidateCandidate( + new PlateCandidate( + new[] + { + new NestJobPlacement("a", 0, 0, 0, 0), + new NestJobPlacement("a", 1, 1, 1, 0), + } + ), + job.Plates[0], + new Dictionary { ["a"] = 2 }, + job.Parts.ToDictionary(p => p.Id) + ) + ); } [Fact] @@ -115,33 +180,52 @@ public class StockLadderTests { var part = Rectangle("a", 1); var stock = new NestPlateStock("s", new Size(10, 10)); - var sheet = new NestJobPlateResult(0, stock, new[] { new NestJobPlacement("a", 0, 0, 0, 0) }); - NestJob Job(double rate, double min) => new(new[] { part }, new[] { stock }, - new NestJobOptions(salvageRate: rate, minimumSalvageDimension: min)); + var sheet = new NestJobPlateResult( + 0, + stock, + new[] { new NestJobPlacement("a", 0, 0, 0, 0) } + ); + NestJob Job(double rate, double min) => + new( + new[] { part }, + new[] { stock }, + new NestJobOptions(salvageRate: rate, minimumSalvageDimension: min) + ); Assert.Equal(100, StockLadderNestingEngine.EstimateNetArea(Job(0.5, 0), sheet)); Assert.Equal(100, StockLadderNestingEngine.EstimateNetArea(Job(0.5, 7), sheet)); Assert.Equal(70, StockLadderNestingEngine.EstimateNetArea(Job(0.5, 5), sheet), 6); - Assert.Throws(() => new NestJobOptions(salvageRate: double.NaN)); + Assert.Throws(() => + new NestJobOptions(salvageRate: double.NaN) + ); Assert.Throws(() => new NestJobOptions(salvageRate: 1.1)); } [Fact] public void FailedRepackRetainsAllDemandAndFiniteStockAccounting() { - var job = new NestJob(new[] { Rectangle("a", 5) }, new[] - { - new NestPlateStock("small", new Size(10, 10), 2), - new NestPlateStock("large", new Size(10, 18), 1) - }); + var job = new NestJob( + new[] { Rectangle("a", 5) }, + new[] + { + new NestPlateStock("small", new Size(10, 10), 2), + new NestPlateStock("large", new Size(10, 18), 1), + } + ); var fullDemandLargeTrials = 0; - var engine = new StockLadderNestingEngine(() => new CallbackNester(request => - { - var quantity = Assert.Single(request.Parts).Quantity; - if (request.Stock.Id == "large" && quantity == 5) fullDemandLargeTrials++; - // Deliberately fail to reproduce the fifth piece on the cheaper merged sheet. - return new PlateCandidate(Enumerable.Range(0, System.Math.Min(quantity, 4)) - .Select(i => new NestJobPlacement("a", i, i % 2 * 4, i / 2 * 4, 0))); - })); + var engine = new StockLadderNestingEngine(() => + new CallbackNester(request => + { + var quantity = Assert.Single(request.Parts).Quantity; + if (request.Stock.Id == "large" && quantity == 5) + fullDemandLargeTrials++; + // Deliberately fail to reproduce the fifth piece on the cheaper merged sheet. + return new PlateCandidate( + Enumerable + .Range(0, System.Math.Min(quantity, 4)) + .Select(i => new NestJobPlacement("a", i, i % 2 * 4, i / 2 * 4, 0)) + ); + }) + ); var result = engine.Solve(job); Assert.True(fullDemandLargeTrials >= 2); // Construction AND equivalent-demand repack ran. Assert.Equal(NestJobStatus.Complete, result.Status); @@ -166,7 +250,9 @@ public class StockLadderTests var job = new NestJob(new[] { part }, new[] { new NestPlateStock("s", new Size(10, 10)) }); if (reject) { - var error = Assert.Throws(() => new StockLadderNestingEngine().Solve(job)); + var error = Assert.Throws(() => + new StockLadderNestingEngine().Solve(job) + ); Assert.Contains("Open geometry leaves the closed material region", error.Message); } else @@ -183,12 +269,21 @@ public class StockLadderTests var remaining = job.Parts.ToDictionary(p => p.Id, p => p.Quantity); foreach (var sheet in result.Plates) { - NestJobValidator.ValidateCandidate(new PlateCandidate(sheet.Placements), sheet.Stock, remaining, parts); - foreach (var pose in sheet.Placements) remaining[pose.PartId]--; + NestJobValidator.ValidateCandidate( + new PlateCandidate(sheet.Placements), + sheet.Stock, + remaining, + parts + ); + foreach (var pose in sheet.Placements) + remaining[pose.PartId]--; } foreach (var part in job.Parts) { - var poses = result.Plates.SelectMany(p => p.Placements).Where(p => p.PartId == part.Id).ToList(); + var poses = result + .Plates.SelectMany(p => p.Placements) + .Where(p => p.PartId == part.Id) + .ToList(); Assert.Equal(Enumerable.Range(0, poses.Count), poses.Select(p => p.InstanceIndex)); var fulfillment = result.Fulfillment.Single(p => p.PartId == part.Id); Assert.Equal(part.Quantity, fulfillment.Placed + fulfillment.Unplaced); @@ -204,9 +299,13 @@ public class StockLadderTests } } - private sealed class CallbackNester(Func callback) : IPlateNester + private sealed class CallbackNester(Func callback) + : IPlateNester { - public PlateCandidate Place(PlatePlacementRequest request, IProgress? progress = null, - CancellationToken token = default) => callback(request); + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress? progress = null, + CancellationToken token = default + ) => callback(request); } } diff --git a/OpenNest.Engine/BestFit/BestFitCache.cs b/OpenNest.Engine/BestFit/BestFitCache.cs index 5664388..96d0141 100644 --- a/OpenNest.Engine/BestFit/BestFitCache.cs +++ b/OpenNest.Engine/BestFit/BestFitCache.cs @@ -16,8 +16,11 @@ namespace OpenNest.Engine.BestFit public static Func CreateSlideComputer { get; set; } public static List GetOrCompute( - Drawing drawing, double plateWidth, double plateHeight, - double spacing) + Drawing drawing, + double plateWidth, + double plateHeight, + double spacing + ) { var key = new CacheKey(drawing, plateWidth, plateHeight, spacing); @@ -34,14 +37,24 @@ namespace OpenNest.Engine.BestFit { if (CreateEvaluator != null) { - try { evaluator = CreateEvaluator(canonical, spacing); } - catch { /* fall back to default evaluator */ } + try + { + evaluator = CreateEvaluator(canonical, spacing); + } + catch + { /* fall back to default evaluator */ + } } if (CreateSlideComputer != null) { - try { slideComputer = CreateSlideComputer(); } - catch { /* fall back to CPU slide computation */ } + try + { + slideComputer = CreateSlideComputer(); + } + catch + { /* fall back to CPU slide computation */ + } } var finder = new BestFitFinder(plateWidth, plateHeight, evaluator, slideComputer); @@ -58,8 +71,10 @@ namespace OpenNest.Engine.BestFit } public static void ComputeForSizes( - Drawing drawing, double spacing, - IEnumerable<(double Width, double Height)> plateSizes) + Drawing drawing, + double spacing, + IEnumerable<(double Width, double Height)> plateSizes + ) { // Skip sizes that are already cached. var needed = new List<(double Width, double Height)>(); @@ -80,8 +95,10 @@ namespace OpenNest.Engine.BestFit var maxHeight = 0.0; foreach (var size in needed) { - if (size.Width > maxWidth) maxWidth = size.Width; - if (size.Height > maxHeight) maxHeight = size.Height; + if (size.Width > maxWidth) + maxWidth = size.Width; + if (size.Height > maxHeight) + maxHeight = size.Height; } IPairEvaluator evaluator = null; @@ -94,14 +111,24 @@ namespace OpenNest.Engine.BestFit if (CreateEvaluator != null) { - try { evaluator = CreateEvaluator(canonical, spacing); } - catch { /* fall back to default evaluator */ } + try + { + evaluator = CreateEvaluator(canonical, spacing); + } + catch + { /* fall back to default evaluator */ + } } if (CreateSlideComputer != null) { - try { slideComputer = CreateSlideComputer(); } - catch { /* fall back to CPU slide computation */ } + try + { + slideComputer = CreateSlideComputer(); + } + catch + { /* fall back to CPU slide computation */ + } } // Compute candidates and evaluate once with the largest plate. @@ -114,25 +141,27 @@ namespace OpenNest.Engine.BestFit var filter = new BestFitFilter { MaxPlateWidth = size.Width, - MaxPlateHeight = size.Height + MaxPlateHeight = size.Height, }; var copy = new List(baseResults.Count); for (var i = 0; i < baseResults.Count; i++) { var r = baseResults[i]; - copy.Add(new BestFitResult - { - Candidate = r.Candidate, - RotatedArea = r.RotatedArea, - BoundingWidth = r.BoundingWidth, - BoundingHeight = r.BoundingHeight, - OptimalRotation = r.OptimalRotation, - TrueArea = r.TrueArea, - HullAngles = r.HullAngles, - Keep = r.Keep, - Reason = r.Reason - }); + copy.Add( + new BestFitResult + { + Candidate = r.Candidate, + RotatedArea = r.RotatedArea, + BoundingWidth = r.BoundingWidth, + BoundingHeight = r.BoundingHeight, + OptimalRotation = r.OptimalRotation, + TrueArea = r.TrueArea, + HullAngles = r.HullAngles, + Keep = r.Keep, + Reason = r.Reason, + } + ); } filter.Apply(copy); @@ -156,8 +185,13 @@ namespace OpenNest.Engine.BestFit } } - public static void Populate(Drawing drawing, double plateWidth, double plateHeight, - double spacing, List results) + public static void Populate( + Drawing drawing, + double plateWidth, + double plateHeight, + double spacing, + List results + ) { if (results == null || results.Count == 0) return; @@ -166,8 +200,10 @@ namespace OpenNest.Engine.BestFit _cache.TryAdd(key, results); } - public static Dictionary<(double PlateWidth, double PlateHeight, double Spacing), List> - GetAllForDrawing(Drawing drawing) + public static Dictionary< + (double PlateWidth, double PlateHeight, double Spacing), + List + > GetAllForDrawing(Drawing drawing) { var result = new Dictionary<(double, double, double), List>(); foreach (var kvp in _cache) @@ -200,10 +236,10 @@ namespace OpenNest.Engine.BestFit public bool Equals(CacheKey other) { - return ReferenceEquals(Drawing, other.Drawing) && - PlateWidth == other.PlateWidth && - PlateHeight == other.PlateHeight && - Spacing == other.Spacing; + return ReferenceEquals(Drawing, other.Drawing) + && PlateWidth == other.PlateWidth + && PlateHeight == other.PlateHeight + && Spacing == other.Spacing; } public override bool Equals(object obj) => obj is CacheKey other && Equals(other); diff --git a/OpenNest.Engine/BestFit/BestFitFilter.cs b/OpenNest.Engine/BestFit/BestFitFilter.cs index 8ea44a5..3791d9b 100644 --- a/OpenNest.Engine/BestFit/BestFitFilter.cs +++ b/OpenNest.Engine/BestFit/BestFitFilter.cs @@ -17,8 +17,10 @@ namespace OpenNest.Engine.BestFit if (!result.Keep) continue; - if (result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight) || - result.LongestSide > System.Math.Max(MaxPlateWidth, MaxPlateHeight)) + if ( + result.ShortestSide > System.Math.Min(MaxPlateWidth, MaxPlateHeight) + || result.LongestSide > System.Math.Max(MaxPlateWidth, MaxPlateHeight) + ) { result.Keep = false; result.Reason = "Exceeds plate dimensions"; @@ -30,14 +32,21 @@ namespace OpenNest.Engine.BestFit if (aspect > MaxAspectRatio && result.Utilization < UtilizationOverride) { result.Keep = false; - result.Reason = string.Format("Aspect ratio {0:F1} exceeds max {1}", aspect, MaxAspectRatio); + result.Reason = string.Format( + "Aspect ratio {0:F1} exceeds max {1}", + aspect, + MaxAspectRatio + ); continue; } if (result.Utilization < MinUtilization) { result.Keep = false; - result.Reason = string.Format("Utilization {0:P0} below minimum", result.Utilization); + result.Reason = string.Format( + "Utilization {0:P0} below minimum", + result.Utilization + ); continue; } diff --git a/OpenNest.Engine/BestFit/BestFitFinder.cs b/OpenNest.Engine/BestFit/BestFitFinder.cs index 3332ec8..c9eb3ab 100644 --- a/OpenNest.Engine/BestFit/BestFitFinder.cs +++ b/OpenNest.Engine/BestFit/BestFitFinder.cs @@ -1,12 +1,12 @@ -using OpenNest.Converters; -using OpenNest.Engine.BestFit.Tiling; -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using OpenNest.Converters; +using OpenNest.Engine.BestFit.Tiling; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.BestFit { @@ -16,20 +16,26 @@ namespace OpenNest.Engine.BestFit private readonly IDistanceComputer _distanceComputer; private readonly BestFitFilter _filter; - public BestFitFinder(double maxPlateWidth, double maxPlateHeight, - IPairEvaluator evaluator = null, ISlideComputer slideComputer = null) + public BestFitFinder( + double maxPlateWidth, + double maxPlateHeight, + IPairEvaluator evaluator = null, + ISlideComputer slideComputer = null + ) { _evaluator = evaluator ?? new PairEvaluator(); - _distanceComputer = slideComputer != null - ? (IDistanceComputer)new GpuDistanceComputer(slideComputer) - : new CpuDistanceComputer(); - var plateAspect = System.Math.Max(maxPlateWidth, maxPlateHeight) / - System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001); + _distanceComputer = + slideComputer != null + ? (IDistanceComputer)new GpuDistanceComputer(slideComputer) + : new CpuDistanceComputer(); + var plateAspect = + System.Math.Max(maxPlateWidth, maxPlateHeight) + / System.Math.Max(System.Math.Min(maxPlateWidth, maxPlateHeight), 0.001); _filter = new BestFitFilter { MaxPlateWidth = maxPlateWidth, MaxPlateHeight = maxPlateHeight, - MaxAspectRatio = System.Math.Max(5.0, plateAspect) + MaxAspectRatio = System.Math.Max(5.0, plateAspect), }; } @@ -37,20 +43,26 @@ namespace OpenNest.Engine.BestFit Drawing drawing, double spacing = 0.25, double stepSize = 0.25, - BestFitSortField sortBy = BestFitSortField.Area) + BestFitSortField sortBy = BestFitSortField.Area + ) { var strategies = BuildStrategies(drawing, spacing); var candidateBags = new ConcurrentBag>(); - Parallel.ForEach(strategies, strategy => - { - candidateBags.Add(strategy.GenerateCandidates(drawing, spacing, stepSize)); - }); + Parallel.ForEach( + strategies, + strategy => + { + candidateBags.Add(strategy.GenerateCandidates(drawing, spacing, stepSize)); + } + ); var allCandidates = candidateBags.SelectMany(c => c).ToList(); - Debug.WriteLine($"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates"); + Debug.WriteLine( + $"[BestFitFinder] {strategies.Count} strategies, {allCandidates.Count} candidates" + ); var results = _evaluator.EvaluateAll(allCandidates); @@ -65,8 +77,12 @@ namespace OpenNest.Engine.BestFit } public List FindAndTile( - Drawing drawing, Plate plate, - double spacing = 0.25, double stepSize = 0.25, int topN = 10) + Drawing drawing, + Plate plate, + double spacing = 0.25, + double stepSize = 0.25, + int topN = 10 + ) { var bestFits = FindBestFits(drawing, spacing, stepSize); var tileEvaluator = new TileEvaluator(); @@ -88,7 +104,10 @@ namespace OpenNest.Engine.BestFit foreach (var angle in angles) { - var desc = string.Format("{0:F1} deg rotated, offset slide", Angle.ToDegrees(angle)); + var desc = string.Format( + "{0:F1} deg rotated, offset slide", + Angle.ToDegrees(angle) + ); strategies.Add(new RotationSlideStrategy(angle, index++, desc, _distanceComputer)); } @@ -97,13 +116,7 @@ namespace OpenNest.Engine.BestFit private List GetRotationAngles(Drawing drawing) { - var angles = new List - { - 0, - Angle.HalfPI, - System.Math.PI, - Angle.HalfPI * 3 - }; + var angles = new List { 0, Angle.HalfPI, System.Math.PI, Angle.HalfPI * 3 }; var hullAngles = GetHullEdgeAngles(drawing); @@ -119,7 +132,8 @@ namespace OpenNest.Engine.BestFit private List GetHullEdgeAngles(Drawing drawing) { - var entities = ConvertProgram.ToGeometry(drawing.Program) + var entities = ConvertProgram + .ToGeometry(drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); @@ -220,7 +234,10 @@ namespace OpenNest.Engine.BestFit angles.Add(angle); } - private List SortResults(List results, BestFitSortField sortBy) + private List SortResults( + List results, + BestFitSortField sortBy + ) { switch (sortBy) { @@ -231,16 +248,19 @@ namespace OpenNest.Engine.BestFit case BestFitSortField.ShortestSide: return results.OrderBy(r => r.ShortestSide).ToList(); case BestFitSortField.Type: - return results.OrderBy(r => r.Candidate.StrategyIndex) - .ThenBy(r => r.Candidate.TestNumber).ToList(); + return results + .OrderBy(r => r.Candidate.StrategyIndex) + .ThenBy(r => r.Candidate.TestNumber) + .ToList(); case BestFitSortField.OriginalSequence: return results.OrderBy(r => r.Candidate.TestNumber).ToList(); case BestFitSortField.Keep: - return results.OrderByDescending(r => r.Keep) - .ThenBy(r => r.RotatedArea).ToList(); + return results + .OrderByDescending(r => r.Keep) + .ThenBy(r => r.RotatedArea) + .ToList(); case BestFitSortField.WhyKeepDrop: - return results.OrderBy(r => r.Reason) - .ThenBy(r => r.RotatedArea).ToList(); + return results.OrderBy(r => r.Reason).ThenBy(r => r.RotatedArea).ToList(); default: return results; } diff --git a/OpenNest.Engine/BestFit/BestFitResult.cs b/OpenNest.Engine/BestFit/BestFitResult.cs index f819edb..1e5e4f3 100644 --- a/OpenNest.Engine/BestFit/BestFitResult.cs +++ b/OpenNest.Engine/BestFit/BestFitResult.cs @@ -1,9 +1,9 @@ -using OpenNest.Engine; -using OpenNest.Converters; -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Generic; using System.Linq; +using OpenNest.Converters; +using OpenNest.Engine; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.BestFit { @@ -44,13 +44,17 @@ namespace OpenNest.Engine.BestFit if (!OptimalRotation.IsEqualTo(0)) { - var pairBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var pairBounds = ( + (IEnumerable)new IBoundable[] { part1, part2 } + ).GetBoundingBox(); var center = pairBounds.Center; part1.Rotate(-OptimalRotation, center); part2.Rotate(-OptimalRotation, center); } - var finalBounds = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var finalBounds = ( + (IEnumerable)new IBoundable[] { part1, part2 } + ).GetBoundingBox(); var offset = new Vector(-finalBounds.Left, -finalBounds.Bottom); part1.Offset(offset); part2.Offset(offset); @@ -106,7 +110,8 @@ namespace OpenNest.Engine.BestFit foreach (var part in parts) { - var partEntities = ConvertProgram.ToGeometry(part.Program) + var partEntities = ConvertProgram + .ToGeometry(part.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); @@ -129,6 +134,6 @@ namespace OpenNest.Engine.BestFit Type, OriginalSequence, Keep, - WhyKeepDrop + WhyKeepDrop, } } diff --git a/OpenNest.Engine/BestFit/CpuDistanceComputer.cs b/OpenNest.Engine/BestFit/CpuDistanceComputer.cs index 5fcab6d..7003daf 100644 --- a/OpenNest.Engine/BestFit/CpuDistanceComputer.cs +++ b/OpenNest.Engine/BestFit/CpuDistanceComputer.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.BestFit { @@ -10,7 +10,8 @@ namespace OpenNest.Engine.BestFit public double[] ComputeDistances( List stationaryLines, List movingTemplateLines, - SlideOffset[] offsets) + SlideOffset[] offsets + ) { var count = offsets.Length; var results = new double[count]; @@ -18,7 +19,8 @@ namespace OpenNest.Engine.BestFit var allMovingVerts = ExtractUniqueVertices(movingTemplateLines); var allStationaryVerts = ExtractUniqueVertices(stationaryLines); - var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>(); + var vertexCache = + new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>(); foreach (var offset in offsets) { @@ -26,69 +28,101 @@ namespace OpenNest.Engine.BestFit if (vertexCache.ContainsKey(key)) continue; - var leading = FilterVerticesByProjection(allMovingVerts, offset.DirX, offset.DirY, keepHigh: true); - var facing = FilterVerticesByProjection(allStationaryVerts, offset.DirX, offset.DirY, keepHigh: false); + var leading = FilterVerticesByProjection( + allMovingVerts, + offset.DirX, + offset.DirY, + keepHigh: true + ); + var facing = FilterVerticesByProjection( + allStationaryVerts, + offset.DirX, + offset.DirY, + keepHigh: false + ); vertexCache[key] = (leading, facing); } - System.Threading.Tasks.Parallel.For(0, count, i => - { - var offset = offsets[i]; - var dirX = offset.DirX; - var dirY = offset.DirY; - var oppX = -dirX; - var oppY = -dirY; - - var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)]; - - var minDist = double.MaxValue; - - for (var v = 0; v < leadingMoving.Length; v++) + System.Threading.Tasks.Parallel.For( + 0, + count, + i => { - var vx = leadingMoving[v].X + offset.Dx; - var vy = leadingMoving[v].Y + offset.Dy; + var offset = offsets[i]; + var dirX = offset.DirX; + var dirY = offset.DirY; + var oppX = -dirX; + var oppY = -dirY; - for (var j = 0; j < stationaryLines.Count; j++) + var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)]; + + var minDist = double.MaxValue; + + for (var v = 0; v < leadingMoving.Length; v++) { - var e = stationaryLines[j]; - var d = SpatialQuery.RayEdgeDistance( - vx, vy, - e.StartPoint.X, e.StartPoint.Y, - e.EndPoint.X, e.EndPoint.Y, - dirX, dirY); + var vx = leadingMoving[v].X + offset.Dx; + var vy = leadingMoving[v].Y + offset.Dy; - if (d < minDist) + for (var j = 0; j < stationaryLines.Count; j++) { - minDist = d; - if (d <= 0) { results[i] = 0; return; } + var e = stationaryLines[j]; + var d = SpatialQuery.RayEdgeDistance( + vx, + vy, + e.StartPoint.X, + e.StartPoint.Y, + e.EndPoint.X, + e.EndPoint.Y, + dirX, + dirY + ); + + if (d < minDist) + { + minDist = d; + if (d <= 0) + { + results[i] = 0; + return; + } + } } } - } - for (var v = 0; v < facingStationary.Length; v++) - { - var svx = facingStationary[v].X; - var svy = facingStationary[v].Y; - - for (var j = 0; j < movingTemplateLines.Count; j++) + for (var v = 0; v < facingStationary.Length; v++) { - var e = movingTemplateLines[j]; - var d = SpatialQuery.RayEdgeDistance( - svx, svy, - e.StartPoint.X + offset.Dx, e.StartPoint.Y + offset.Dy, - e.EndPoint.X + offset.Dx, e.EndPoint.Y + offset.Dy, - oppX, oppY); + var svx = facingStationary[v].X; + var svy = facingStationary[v].Y; - if (d < minDist) + for (var j = 0; j < movingTemplateLines.Count; j++) { - minDist = d; - if (d <= 0) { results[i] = 0; return; } + var e = movingTemplateLines[j]; + var d = SpatialQuery.RayEdgeDistance( + svx, + svy, + e.StartPoint.X + offset.Dx, + e.StartPoint.Y + offset.Dy, + e.EndPoint.X + offset.Dx, + e.EndPoint.Y + offset.Dy, + oppX, + oppY + ); + + if (d < minDist) + { + minDist = d; + if (d <= 0) + { + results[i] = 0; + return; + } + } } } - } - results[i] = minDist; - }); + results[i] = minDist; + } + ); return results; } @@ -96,7 +130,8 @@ namespace OpenNest.Engine.BestFit public double[] ComputeDistances( List stationaryEntities, List movingEntities, - SlideOffset[] offsets) + SlideOffset[] offsets + ) { var count = offsets.Length; var results = new double[count]; @@ -107,7 +142,8 @@ namespace OpenNest.Engine.BestFit var movingCurves = ExtractCurveParams(movingEntities); var stationaryCurves = ExtractCurveParams(stationaryEntities); - var vertexCache = new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>(); + var vertexCache = + new Dictionary<(double, double), (Vector[] leading, Vector[] facing)>(); foreach (var offset in offsets) { @@ -115,106 +151,169 @@ namespace OpenNest.Engine.BestFit if (vertexCache.ContainsKey(key)) continue; - var leading = FilterVerticesByProjection(allMovingVerts, offset.DirX, offset.DirY, keepHigh: true); - var facing = FilterVerticesByProjection(allStationaryVerts, offset.DirX, offset.DirY, keepHigh: false); + var leading = FilterVerticesByProjection( + allMovingVerts, + offset.DirX, + offset.DirY, + keepHigh: true + ); + var facing = FilterVerticesByProjection( + allStationaryVerts, + offset.DirX, + offset.DirY, + keepHigh: false + ); vertexCache[key] = (leading, facing); } - System.Threading.Tasks.Parallel.For(0, count, i => - { - var offset = offsets[i]; - var dirX = offset.DirX; - var dirY = offset.DirY; - var oppX = -dirX; - var oppY = -dirY; - - var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)]; - - var minDist = double.MaxValue; - - // Case 1: Leading moving vertices → stationary entities - for (var v = 0; v < leadingMoving.Length; v++) + System.Threading.Tasks.Parallel.For( + 0, + count, + i => { - var vx = leadingMoving[v].X + offset.Dx; - var vy = leadingMoving[v].Y + offset.Dy; + var offset = offsets[i]; + var dirX = offset.DirX; + var dirY = offset.DirY; + var oppX = -dirX; + var oppY = -dirY; - for (var j = 0; j < stationaryEntities.Count; j++) + var (leadingMoving, facingStationary) = vertexCache[(dirX, dirY)]; + + var minDist = double.MaxValue; + + // Case 1: Leading moving vertices → stationary entities + for (var v = 0; v < leadingMoving.Length; v++) { - var d = RayEntityDistance(vx, vy, stationaryEntities[j], 0, 0, dirX, dirY); + var vx = leadingMoving[v].X + offset.Dx; + var vy = leadingMoving[v].Y + offset.Dy; - if (d < minDist) + for (var j = 0; j < stationaryEntities.Count; j++) { - minDist = d; - if (d <= 0) { results[i] = 0; return; } - } - } - } + var d = RayEntityDistance( + vx, + vy, + stationaryEntities[j], + 0, + 0, + dirX, + dirY + ); - // Case 2: Facing stationary vertices → moving entities (opposite direction) - for (var v = 0; v < facingStationary.Length; v++) - { - var svx = facingStationary[v].X; - var svy = facingStationary[v].Y; - - for (var j = 0; j < movingEntities.Count; j++) - { - var d = RayEntityDistance(svx, svy, movingEntities[j], offset.Dx, offset.Dy, oppX, oppY); - - if (d < minDist) - { - minDist = d; - if (d <= 0) { results[i] = 0; return; } - } - } - } - - // Phase 3: Curve-to-curve direct distance. - // Vertex sampling misses the true contact between two curved entities - // when the approach angle doesn't align with a sampled vertex. - for (var m = 0; m < movingCurves.Length; m++) - { - var mc = movingCurves[m]; - var mcx = mc.Cx + offset.Dx; - var mcy = mc.Cy + offset.Dy; - - for (var s = 0; s < stationaryCurves.Length; s++) - { - var sc = stationaryCurves[s]; - var d = SpatialQuery.RayCircleDistance( - mcx, mcy, sc.Cx, sc.Cy, mc.Radius + sc.Radius, dirX, dirY); - - if (d >= minDist || d == double.MaxValue) - continue; - - if (mc.Entity is Arc || sc.Entity is Arc) - { - var mx = mcx + d * dirX; - var my = mcy + d * dirY; - var toCx = sc.Cx - mx; - var toCy = sc.Cy - my; - - if (mc.Entity is Arc mArc) + if (d < minDist) { - var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx)); - if (!Angle.IsBetweenRad(angle, mArc.StartAngle, mArc.EndAngle, mArc.IsReversed)) - continue; - } - - if (sc.Entity is Arc sArc) - { - var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx)); - if (!Angle.IsBetweenRad(angle, sArc.StartAngle, sArc.EndAngle, sArc.IsReversed)) - continue; + minDist = d; + if (d <= 0) + { + results[i] = 0; + return; + } } } - - minDist = d; - if (d <= 0) { results[i] = 0; return; } } - } - results[i] = minDist; - }); + // Case 2: Facing stationary vertices → moving entities (opposite direction) + for (var v = 0; v < facingStationary.Length; v++) + { + var svx = facingStationary[v].X; + var svy = facingStationary[v].Y; + + for (var j = 0; j < movingEntities.Count; j++) + { + var d = RayEntityDistance( + svx, + svy, + movingEntities[j], + offset.Dx, + offset.Dy, + oppX, + oppY + ); + + if (d < minDist) + { + minDist = d; + if (d <= 0) + { + results[i] = 0; + return; + } + } + } + } + + // Phase 3: Curve-to-curve direct distance. + // Vertex sampling misses the true contact between two curved entities + // when the approach angle doesn't align with a sampled vertex. + for (var m = 0; m < movingCurves.Length; m++) + { + var mc = movingCurves[m]; + var mcx = mc.Cx + offset.Dx; + var mcy = mc.Cy + offset.Dy; + + for (var s = 0; s < stationaryCurves.Length; s++) + { + var sc = stationaryCurves[s]; + var d = SpatialQuery.RayCircleDistance( + mcx, + mcy, + sc.Cx, + sc.Cy, + mc.Radius + sc.Radius, + dirX, + dirY + ); + + if (d >= minDist || d == double.MaxValue) + continue; + + if (mc.Entity is Arc || sc.Entity is Arc) + { + var mx = mcx + d * dirX; + var my = mcy + d * dirY; + var toCx = sc.Cx - mx; + var toCy = sc.Cy - my; + + if (mc.Entity is Arc mArc) + { + var angle = Angle.NormalizeRad(System.Math.Atan2(toCy, toCx)); + if ( + !Angle.IsBetweenRad( + angle, + mArc.StartAngle, + mArc.EndAngle, + mArc.IsReversed + ) + ) + continue; + } + + if (sc.Entity is Arc sArc) + { + var angle = Angle.NormalizeRad(System.Math.Atan2(-toCy, -toCx)); + if ( + !Angle.IsBetweenRad( + angle, + sArc.StartAngle, + sArc.EndAngle, + sArc.IsReversed + ) + ) + continue; + } + } + + minDist = d; + if (d <= 0) + { + results[i] = 0; + return; + } + } + } + + results[i] = minDist; + } + ); return results; } @@ -222,7 +321,9 @@ namespace OpenNest.Engine.BestFit private readonly struct CurveParams { public readonly Entity Entity; - public readonly double Cx, Cy, Radius; + public readonly double Cx, + Cy, + Radius; public CurveParams(Entity entity, double cx, double cy, double radius) { @@ -239,7 +340,9 @@ namespace OpenNest.Engine.BestFit for (var i = 0; i < entities.Count; i++) { if (entities[i] is Circle circle) - curves.Add(new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius)); + curves.Add( + new CurveParams(circle, circle.Center.X, circle.Center.Y, circle.Radius) + ); else if (entities[i] is Arc arc) curves.Add(new CurveParams(arc, arc.Center.X, arc.Center.Y, arc.Radius)); } @@ -247,36 +350,56 @@ namespace OpenNest.Engine.BestFit } private static double RayEntityDistance( - double vx, double vy, Entity entity, - double entityOffsetX, double entityOffsetY, - double dirX, double dirY) + double vx, + double vy, + Entity entity, + double entityOffsetX, + double entityOffsetY, + double dirX, + double dirY + ) { if (entity is Line line) { return SpatialQuery.RayEdgeDistance( - vx, vy, - line.StartPoint.X + entityOffsetX, line.StartPoint.Y + entityOffsetY, - line.EndPoint.X + entityOffsetX, line.EndPoint.Y + entityOffsetY, - dirX, dirY); + vx, + vy, + line.StartPoint.X + entityOffsetX, + line.StartPoint.Y + entityOffsetY, + line.EndPoint.X + entityOffsetX, + line.EndPoint.Y + entityOffsetY, + dirX, + dirY + ); } if (entity is Arc arc) { return SpatialQuery.RayArcDistance( - vx, vy, - arc.Center.X + entityOffsetX, arc.Center.Y + entityOffsetY, + vx, + vy, + arc.Center.X + entityOffsetX, + arc.Center.Y + entityOffsetY, arc.Radius, - arc.StartAngle, arc.EndAngle, arc.IsReversed, - dirX, dirY); + arc.StartAngle, + arc.EndAngle, + arc.IsReversed, + dirX, + dirY + ); } if (entity is Circle circle) { return SpatialQuery.RayCircleDistance( - vx, vy, - circle.Center.X + entityOffsetX, circle.Center.Y + entityOffsetY, + vx, + vy, + circle.Center.X + entityOffsetX, + circle.Center.Y + entityOffsetY, circle.Radius, - dirX, dirY); + dirX, + dirY + ); } return double.MaxValue; @@ -352,7 +475,11 @@ namespace OpenNest.Engine.BestFit } private static Vector[] FilterVerticesByProjection( - Vector[] vertices, double dirX, double dirY, bool keepHigh) + Vector[] vertices, + double dirX, + double dirY, + bool keepHigh + ) { if (vertices.Length == 0) return vertices; @@ -364,8 +491,10 @@ namespace OpenNest.Engine.BestFit for (var i = 0; i < vertices.Length; i++) { projections[i] = vertices[i].X * dirX + vertices[i].Y * dirY; - if (projections[i] < min) min = projections[i]; - if (projections[i] > max) max = projections[i]; + if (projections[i] < min) + min = projections[i]; + if (projections[i] > max) + max = projections[i]; } var midpoint = (min + max) / 2; diff --git a/OpenNest.Engine/BestFit/GpuDistanceComputer.cs b/OpenNest.Engine/BestFit/GpuDistanceComputer.cs index 5c4e05f..c921bca 100644 --- a/OpenNest.Engine/BestFit/GpuDistanceComputer.cs +++ b/OpenNest.Engine/BestFit/GpuDistanceComputer.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.BestFit { @@ -15,7 +15,8 @@ namespace OpenNest.Engine.BestFit public double[] ComputeDistances( List stationaryLines, List movingTemplateLines, - SlideOffset[] offsets) + SlideOffset[] offsets + ) { var stationarySegments = SpatialQuery.FlattenLines(stationaryLines); var movingSegments = SpatialQuery.FlattenLines(movingTemplateLines); @@ -31,15 +32,21 @@ namespace OpenNest.Engine.BestFit } return _slideComputer.ComputeBatchMultiDir( - stationarySegments, stationaryLines.Count, - movingSegments, movingTemplateLines.Count, - flatOffsets, count, directions); + stationarySegments, + stationaryLines.Count, + movingSegments, + movingTemplateLines.Count, + flatOffsets, + count, + directions + ); } public double[] ComputeDistances( List stationaryEntities, List movingEntities, - SlideOffset[] offsets) + SlideOffset[] offsets + ) { // GPU path doesn't support native entities yet — fall back to CPU. var cpu = new CpuDistanceComputer(); @@ -52,9 +59,12 @@ namespace OpenNest.Engine.BestFit /// private static int DirectionVectorToInt(double dirX, double dirY) { - if (dirX < -0.5) return (int)PushDirection.Left; - if (dirX > 0.5) return (int)PushDirection.Right; - if (dirY < -0.5) return (int)PushDirection.Down; + if (dirX < -0.5) + return (int)PushDirection.Left; + if (dirX > 0.5) + return (int)PushDirection.Right; + if (dirY < -0.5) + return (int)PushDirection.Down; return (int)PushDirection.Up; } } diff --git a/OpenNest.Engine/BestFit/IDistanceComputer.cs b/OpenNest.Engine/BestFit/IDistanceComputer.cs index 36e2a9d..21bd7ac 100644 --- a/OpenNest.Engine/BestFit/IDistanceComputer.cs +++ b/OpenNest.Engine/BestFit/IDistanceComputer.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.BestFit { @@ -8,11 +8,13 @@ namespace OpenNest.Engine.BestFit double[] ComputeDistances( List stationaryLines, List movingTemplateLines, - SlideOffset[] offsets); + SlideOffset[] offsets + ); double[] ComputeDistances( List stationaryEntities, List movingEntities, - SlideOffset[] offsets); + SlideOffset[] offsets + ); } } diff --git a/OpenNest.Engine/BestFit/ISlideComputer.cs b/OpenNest.Engine/BestFit/ISlideComputer.cs index 9096f9a..e6734d0 100644 --- a/OpenNest.Engine/BestFit/ISlideComputer.cs +++ b/OpenNest.Engine/BestFit/ISlideComputer.cs @@ -20,19 +20,27 @@ namespace OpenNest.Engine.BestFit /// Push direction. /// Array of minimum distances, one per offset position. double[] ComputeBatch( - double[] stationarySegments, int stationaryCount, - double[] movingTemplateSegments, int movingCount, - double[] offsets, int offsetCount, - PushDirection direction); + double[] stationarySegments, + int stationaryCount, + double[] movingTemplateSegments, + int movingCount, + double[] offsets, + int offsetCount, + PushDirection direction + ); /// /// Computes minimum directional distance for offsets with per-offset directions. /// Uploads segment data once for all offsets, reducing GPU round-trips. /// double[] ComputeBatchMultiDir( - double[] stationarySegments, int stationaryCount, - double[] movingTemplateSegments, int movingCount, - double[] offsets, int offsetCount, - int[] directions); + double[] stationarySegments, + int stationaryCount, + double[] movingTemplateSegments, + int movingCount, + double[] offsets, + int offsetCount, + int[] directions + ); } } diff --git a/OpenNest.Engine/BestFit/NfpSlideStrategy.cs b/OpenNest.Engine/BestFit/NfpSlideStrategy.cs index f93aa5b..20f3efe 100644 --- a/OpenNest.Engine/BestFit/NfpSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/NfpSlideStrategy.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.BestFit { @@ -10,8 +10,14 @@ namespace OpenNest.Engine.BestFit private readonly Polygon _stationaryHull; private readonly Vector _correction; - public NfpSlideStrategy(double part2Rotation, int type, string description, - Polygon stationaryPerimeter, Polygon stationaryHull, Vector correction) + public NfpSlideStrategy( + double part2Rotation, + int type, + string description, + Polygon stationaryPerimeter, + Polygon stationaryHull, + Vector correction + ) { _part2Rotation = part2Rotation; StrategyIndex = type; @@ -28,8 +34,13 @@ namespace OpenNest.Engine.BestFit /// Creates an NfpSlideStrategy by extracting polygon data from a drawing. /// Returns null if the drawing has no valid perimeter. /// - public static NfpSlideStrategy Create(Drawing drawing, double part2Rotation, - int type, string description, double spacing) + public static NfpSlideStrategy Create( + Drawing drawing, + double part2Rotation, + int type, + string description, + double spacing + ) { var result = PolygonHelper.ExtractPerimeterPolygon(drawing, spacing / 2); @@ -38,18 +49,32 @@ namespace OpenNest.Engine.BestFit var hull = ConvexHull.Compute(result.Polygon.Vertices); - return new NfpSlideStrategy(part2Rotation, type, description, - result.Polygon, hull, result.Correction); + return new NfpSlideStrategy( + part2Rotation, + type, + description, + result.Polygon, + hull, + result.Correction + ); } - public List GenerateCandidates(Drawing drawing, double spacing, double stepSize) + public List GenerateCandidates( + Drawing drawing, + double spacing, + double stepSize + ) { var candidates = new List(); if (stepSize <= 0) return candidates; - var orbitingPerimeter = PolygonHelper.RotatePolygon(_stationaryPerimeter, _part2Rotation, reNormalize: true); + var orbitingPerimeter = PolygonHelper.RotatePolygon( + _stationaryPerimeter, + _part2Rotation, + reNormalize: true + ); var orbitingPoly = ConvexHull.Compute(orbitingPerimeter.Vertices); var nfp = NoFitPolygon.ComputeConvex(_stationaryHull, orbitingPoly); @@ -79,9 +104,7 @@ namespace OpenNest.Engine.BestFit for (var s = 1; s < steps; s++) { var t = (double)s / steps; - var sample = new Vector( - verts[i].X + dx * t, - verts[i].Y + dy * t); + var sample = new Vector(verts[i].X + dx * t, verts[i].Y + dy * t); var sampleOffset = ApplyCorrection(sample, _correction); candidates.Add(MakeCandidate(drawing, sampleOffset, spacing, testNumber++)); } @@ -96,7 +119,12 @@ namespace OpenNest.Engine.BestFit return new Vector(nfpVertex.X - correction.X, nfpVertex.Y - correction.Y); } - private PairCandidate MakeCandidate(Drawing drawing, Vector offset, double spacing, int testNumber) + private PairCandidate MakeCandidate( + Drawing drawing, + Vector offset, + double spacing, + int testNumber + ) { return new PairCandidate { @@ -106,7 +134,7 @@ namespace OpenNest.Engine.BestFit Part2Offset = offset, StrategyIndex = StrategyIndex, TestNumber = testNumber, - Spacing = spacing + Spacing = spacing, }; } } diff --git a/OpenNest.Engine/BestFit/PairEvaluator.cs b/OpenNest.Engine/BestFit/PairEvaluator.cs index f47994d..0e6b899 100644 --- a/OpenNest.Engine/BestFit/PairEvaluator.cs +++ b/OpenNest.Engine/BestFit/PairEvaluator.cs @@ -1,11 +1,11 @@ -using OpenNest.Converters; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using OpenNest.Converters; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.BestFit { @@ -24,10 +24,13 @@ namespace OpenNest.Engine.BestFit var resultBag = new ConcurrentBag(); - Parallel.ForEach(candidates, c => - { - resultBag.Add(Evaluate(c, perimeterDrawing)); - }); + Parallel.ForEach( + candidates, + c => + { + resultBag.Add(Evaluate(c, perimeterDrawing)); + } + ); return resultBag.ToList(); } @@ -56,7 +59,10 @@ namespace OpenNest.Engine.BestFit allPoints.AddRange(GetPartVertices(part2)); // Find optimal bounding rectangle via rotating calipers - double bestArea, bestWidth, bestHeight, bestRotation; + double bestArea, + bestWidth, + bestHeight, + bestRotation; List hullAngles = null; if (allPoints.Count >= 3) @@ -71,7 +77,9 @@ namespace OpenNest.Engine.BestFit } else { - var combinedBox = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + var combinedBox = ( + (IEnumerable)new IBoundable[] { part1, part2 } + ).GetBoundingBox(); bestArea = combinedBox.Area(); bestWidth = combinedBox.Width; bestHeight = combinedBox.Length; @@ -100,14 +108,16 @@ namespace OpenNest.Engine.BestFit TrueArea = trueArea, HullAngles = hullAngles, Keep = !overlaps, - Reason = overlaps ? "Overlap detected" : "Valid" + Reason = overlaps ? "Overlap detected" : "Valid", }; } private static Drawing CreatePerimeterDrawing(Drawing source) { - var entities = ConvertProgram.ToGeometry(source.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var entities = ConvertProgram + .ToGeometry(source.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); var profile = new ShapeProfile(entities); var program = ConvertGeometry.ToProgram(profile.Perimeter); return new Drawing(source.Name, program); @@ -115,18 +125,23 @@ namespace OpenNest.Engine.BestFit private static Shape GetPerimeterShape(Part part) { - var entities = ConvertProgram.ToGeometry(part.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var entities = ConvertProgram + .ToGeometry(part.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); var shapes = ShapeBuilder.GetShapes(entities); - if (shapes.Count == 0) return null; + if (shapes.Count == 0) + return null; shapes[0].Offset(part.Location); return shapes[0]; } private static List GetPartVertices(Part part) { - var entities = ConvertProgram.ToGeometry(part.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var entities = ConvertProgram + .ToGeometry(part.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); var shapes = ShapeBuilder.GetShapes(entities); var points = new List(); diff --git a/OpenNest.Engine/BestFit/PolygonHelper.cs b/OpenNest.Engine/BestFit/PolygonHelper.cs index 087cd97..386b6ea 100644 --- a/OpenNest.Engine/BestFit/PolygonHelper.cs +++ b/OpenNest.Engine/BestFit/PolygonHelper.cs @@ -1,15 +1,19 @@ +using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; -using System.Linq; namespace OpenNest.Engine.BestFit { public static class PolygonHelper { - public static PolygonExtractionResult ExtractPerimeterPolygon(Drawing drawing, double halfSpacing) + public static PolygonExtractionResult ExtractPerimeterPolygon( + Drawing drawing, + double halfSpacing + ) { - var entities = ConvertProgram.ToGeometry(drawing.Program) + var entities = ConvertProgram + .ToGeometry(drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); @@ -25,9 +29,8 @@ namespace OpenNest.Engine.BestFit // Ensure CW winding for correct outward offset direction. definedShape.NormalizeWinding(); - var inflated = halfSpacing > 0 - ? (perimeter.OffsetOutward(halfSpacing) ?? perimeter) - : perimeter; + var inflated = + halfSpacing > 0 ? (perimeter.OffsetOutward(halfSpacing) ?? perimeter) : perimeter; // Convert to polygon with circumscribed arcs for tight nesting. var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true); @@ -57,9 +60,7 @@ namespace OpenNest.Engine.BestFit foreach (var v in polygon.Vertices) { - result.Vertices.Add(new Vector( - v.X * cos - v.Y * sin, - v.X * sin + v.Y * cos)); + result.Vertices.Add(new Vector(v.X * cos - v.Y * sin, v.X * sin + v.Y * cos)); } if (reNormalize) diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 02541ee..e3a9c9e 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.BestFit { @@ -9,14 +9,18 @@ namespace OpenNest.Engine.BestFit private static readonly (double DirX, double DirY)[] PushDirections = { - (-1, 0), // Left - (0, -1), // Down - (1, 0), // Right - (0, 1) // Up + (-1, 0), // Left + (0, -1), // Down + (1, 0), // Right + (0, 1), // Up }; - public RotationSlideStrategy(double part2Rotation, int strategyIndex, string description, - IDistanceComputer distanceComputer) + public RotationSlideStrategy( + double part2Rotation, + int strategyIndex, + string description, + IDistanceComputer distanceComputer + ) { Part2Rotation = part2Rotation; StrategyIndex = strategyIndex; @@ -28,7 +32,11 @@ namespace OpenNest.Engine.BestFit public int StrategyIndex { get; } public string Description { get; } - public List GenerateCandidates(Drawing drawing, double spacing, double stepSize) + public List GenerateCandidates( + Drawing drawing, + double spacing, + double stepSize + ) { var candidates = new List(); @@ -48,7 +56,10 @@ namespace OpenNest.Engine.BestFit return candidates; var distances = _distanceComputer.ComputeDistances( - part1Entities, part2Entities, offsets); + part1Entities, + part2Entities, + offsets + ); var testNumber = 0; @@ -60,24 +71,32 @@ namespace OpenNest.Engine.BestFit var finalPosition = new Vector( part2Template.Location.X + offsets[i].Dx + offsets[i].DirX * slideDist, - part2Template.Location.Y + offsets[i].Dy + offsets[i].DirY * slideDist); + part2Template.Location.Y + offsets[i].Dy + offsets[i].DirY * slideDist + ); - candidates.Add(new PairCandidate - { - Drawing = drawing, - Part1Rotation = 0, - Part2Rotation = Part2Rotation, - Part2Offset = finalPosition, - StrategyIndex = StrategyIndex, - TestNumber = testNumber++, - Spacing = spacing - }); + candidates.Add( + new PairCandidate + { + Drawing = drawing, + Part1Rotation = 0, + Part2Rotation = Part2Rotation, + Part2Offset = finalPosition, + StrategyIndex = StrategyIndex, + TestNumber = testNumber++, + Spacing = spacing, + } + ); } return candidates; } - private static SlideOffset[] BuildOffsets(Box bbox1, Box bbox2, double spacing, double stepSize) + private static SlideOffset[] BuildOffsets( + Box bbox1, + Box bbox2, + double spacing, + double stepSize + ) { var offsets = new List(); @@ -85,7 +104,9 @@ namespace OpenNest.Engine.BestFit { var isHorizontalPush = System.Math.Abs(dirX) > System.Math.Abs(dirY); - double perpMin, perpMax, pushStartOffset; + double perpMin, + perpMax, + pushStartOffset; if (isHorizontalPush) { diff --git a/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs b/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs index c0aa95e..bd8e79b 100644 --- a/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs +++ b/OpenNest.Engine/BestFit/Tiling/TileEvaluator.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Engine.BestFit.Tiling { @@ -16,7 +16,12 @@ namespace OpenNest.Engine.BestFit.Tiling return result1.PartsNested >= result2.PartsNested ? result1 : result2; } - private TileResult TryTile(BestFitResult bestFit, double plateWidth, double plateHeight, bool rotatePair) + private TileResult TryTile( + BestFitResult bestFit, + double plateWidth, + double plateHeight, + bool rotatePair + ) { var pairWidth = rotatePair ? bestFit.BoundingHeight : bestFit.BoundingWidth; var pairHeight = rotatePair ? bestFit.BoundingWidth : bestFit.BoundingHeight; @@ -36,13 +41,16 @@ namespace OpenNest.Engine.BestFit.Tiling { for (var col = 0; col < cols; col++) { - placements.Add(new PairPlacement - { - Position = new Vector( - col * (pairWidth + spacing), - row * (pairHeight + spacing)), - PairRotation = rotatePair ? Angle.HalfPI : 0 - }); + placements.Add( + new PairPlacement + { + Position = new Vector( + col * (pairWidth + spacing), + row * (pairHeight + spacing) + ), + PairRotation = rotatePair ? Angle.HalfPI : 0, + } + ); } } @@ -55,7 +63,7 @@ namespace OpenNest.Engine.BestFit.Tiling Columns = cols, Utilization = plateArea > 0 ? usedArea / plateArea : 0, Placements = placements, - PairRotated = rotatePair + PairRotated = rotatePair, }; } } diff --git a/OpenNest.Engine/BestFit/Tiling/TileResult.cs b/OpenNest.Engine/BestFit/Tiling/TileResult.cs index 7464d4e..a3f1181 100644 --- a/OpenNest.Engine/BestFit/Tiling/TileResult.cs +++ b/OpenNest.Engine/BestFit/Tiling/TileResult.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.BestFit.Tiling { diff --git a/OpenNest.Engine/CanonicalFrame.cs b/OpenNest.Engine/CanonicalFrame.cs index 8911d26..8b657e2 100644 --- a/OpenNest.Engine/CanonicalFrame.cs +++ b/OpenNest.Engine/CanonicalFrame.cs @@ -1,7 +1,7 @@ +using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Engine { @@ -23,8 +23,8 @@ namespace OpenNest.Engine var angle = drawing.Source?.Angle ?? 0.0; // Clone program (never mutate the source). - var pgm = (drawing.Program.Clone() as OpenNest.CNC.Program) - ?? new OpenNest.CNC.Program(); + var pgm = + (drawing.Program.Clone() as OpenNest.CNC.Program) ?? new OpenNest.CNC.Program(); if (!Tolerance.IsEqualTo(angle, 0)) pgm.Rotate(angle, pgm.BoundingBox().Center); diff --git a/OpenNest.Engine/CirclePacking/Bin.cs b/OpenNest.Engine/CirclePacking/Bin.cs index 9dc3ab3..1e05bbd 100644 --- a/OpenNest.Engine/CirclePacking/Bin.cs +++ b/OpenNest.Engine/CirclePacking/Bin.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; namespace OpenNest.CirclePacking { @@ -24,7 +24,7 @@ namespace OpenNest.CirclePacking { Location = this.Location, Size = this.Size, - Items = new List(Items) + Items = new List(Items), }; } } diff --git a/OpenNest.Engine/CirclePacking/FillEndEven.cs b/OpenNest.Engine/CirclePacking/FillEndEven.cs index 62ece31..9275273 100644 --- a/OpenNest.Engine/CirclePacking/FillEndEven.cs +++ b/OpenNest.Engine/CirclePacking/FillEndEven.cs @@ -1,21 +1,20 @@ -using OpenNest.Geometry; +using System; +using OpenNest.Geometry; using OpenNest.Math; -using System; namespace OpenNest.CirclePacking { internal class FillEndEven : FillEngine { public FillEndEven(Bin bin) - : base(bin) - { - } + : base(bin) { } public override void Fill(Item item) { var max = new Vector( Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon, - Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon); + Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon + ); var rows = System.Math.Floor((Bin.Length + Tolerance.Epsilon) / (item.Diameter)); @@ -36,10 +35,7 @@ namespace OpenNest.CirclePacking for (; y <= max.Y; y += yoffset) { - Bin.Items.Add(new Item - { - Center = new Vector(x, y) - }); + Bin.Items.Add(new Item { Center = new Vector(x, y) }); } column++; @@ -62,10 +58,7 @@ namespace OpenNest.CirclePacking for (; y <= max.Y; y += yoffset) { - Bin.Items.Add(new Item - { - Center = new Vector(x, y) - }); + Bin.Items.Add(new Item { Center = new Vector(x, y) }); } column++; diff --git a/OpenNest.Engine/CirclePacking/FillEndOdd.cs b/OpenNest.Engine/CirclePacking/FillEndOdd.cs index 347f5d9..d9a637b 100644 --- a/OpenNest.Engine/CirclePacking/FillEndOdd.cs +++ b/OpenNest.Engine/CirclePacking/FillEndOdd.cs @@ -1,15 +1,13 @@ -using OpenNest.Geometry; +using System; +using OpenNest.Geometry; using OpenNest.Math; -using System; namespace OpenNest.CirclePacking { internal class FillEndOdd : FillEngine { public FillEndOdd(Bin bin) - : base(bin) - { - } + : base(bin) { } public override void Fill(Item item) { @@ -37,7 +35,8 @@ namespace OpenNest.CirclePacking var max = new Vector( bin.Right - item.BoundingBox.Right + Tolerance.Epsilon, - bin.Top - item.BoundingBox.Top + Tolerance.Epsilon); + bin.Top - item.BoundingBox.Top + Tolerance.Epsilon + ); var primarySize = horizontal ? bin.Width : bin.Length; var count = System.Math.Floor((primarySize + Tolerance.Epsilon) / item.Diameter); @@ -64,7 +63,9 @@ namespace OpenNest.CirclePacking for (; inner <= innerMax; inner += primaryOffset) { var addedItem = item.Clone() as Item; - addedItem.Center = horizontal ? new Vector(inner, outer) : new Vector(outer, inner); + addedItem.Center = horizontal + ? new Vector(inner, outer) + : new Vector(outer, inner); bin.Items.Add(addedItem); } diff --git a/OpenNest.Engine/CirclePacking/FillEngine.cs b/OpenNest.Engine/CirclePacking/FillEngine.cs index 7fd9488..3814caf 100644 --- a/OpenNest.Engine/CirclePacking/FillEngine.cs +++ b/OpenNest.Engine/CirclePacking/FillEngine.cs @@ -1,5 +1,4 @@ - -namespace OpenNest.CirclePacking +namespace OpenNest.CirclePacking { internal abstract class FillEngine { diff --git a/OpenNest.Engine/CirclePacking/Item.cs b/OpenNest.Engine/CirclePacking/Item.cs index 4b3ad59..f68fee5 100644 --- a/OpenNest.Engine/CirclePacking/Item.cs +++ b/OpenNest.Engine/CirclePacking/Item.cs @@ -12,7 +12,7 @@ namespace OpenNest.CirclePacking { Radius = this.Radius, Center = this.Center, - Id = this.Id + Id = this.Id, }; } } diff --git a/OpenNest.Engine/DefaultNestEngine.cs b/OpenNest.Engine/DefaultNestEngine.cs index de2d237..fb31601 100644 --- a/OpenNest.Engine/DefaultNestEngine.cs +++ b/OpenNest.Engine/DefaultNestEngine.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; using OpenNest.Engine; using OpenNest.Engine.BestFit; using OpenNest.Engine.Fill; @@ -5,21 +10,18 @@ using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; using OpenNest.RectanglePacking; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; namespace OpenNest { public class DefaultNestEngine : NestEngineBase { - public DefaultNestEngine(Plate plate) : base(plate) { } + public DefaultNestEngine(Plate plate) + : base(plate) { } public override string Name => "Default"; - public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit, Extents)"; + public override string Description => + "Multi-phase nesting (Linear, Pairs, RectBestFit, Extents)"; private readonly AngleCandidateBuilder angleBuilder = new(); @@ -29,7 +31,11 @@ namespace OpenNest set => angleBuilder.ForceFullSweep = value; } - public override List BuildAngles(NestItem item, ClassificationResult classification, Box workArea) + public override List BuildAngles( + NestItem item, + ClassificationResult classification, + Box workArea + ) { return angleBuilder.Build(item, classification, workArea); } @@ -41,8 +47,12 @@ namespace OpenNest // --- Public Fill API --- - public override List Fill(NestItem item, Box workArea, - IProgress progress, CancellationToken token) + public override List Fill( + NestItem item, + Box workArea, + IProgress progress, + CancellationToken token + ) { PhaseResults.Clear(); AngleResults.Clear(); @@ -67,18 +77,23 @@ namespace OpenNest var fast = TryFillSmallQuantity(canonicalItem, workArea); if (fast != null && fast.Count >= canonicalItem.Quantity) { - Debug.WriteLine($"[Fill] Fast path: placed {fast.Count} parts for qty={canonicalItem.Quantity}"); + Debug.WriteLine( + $"[Fill] Fast path: placed {fast.Count} parts for qty={canonicalItem.Quantity}" + ); WinnerPhase = NestPhase.Pairs; fast = RebindAndUnCanonicalize(fast, originalDrawing, sourceAngle); - ReportProgress(progress, new ProgressReport - { - Phase = WinnerPhase, - PlateNumber = PlateNumber, - Parts = fast, - WorkArea = workArea, - Description = $"Fast path: {fast.Count} parts", - IsOverallBest = true, - }); + ReportProgress( + progress, + new ProgressReport + { + Phase = WinnerPhase, + PlateNumber = PlateNumber, + Parts = fast, + WorkArea = workArea, + Description = $"Fast path: {fast.Count} parts", + IsOverallBest = true, + } + ); return fast; } } @@ -88,16 +103,24 @@ namespace OpenNest { effectiveWorkArea = ShrinkWorkArea(canonicalItem, workArea, Plate.PartSpacing); if (effectiveWorkArea != workArea) - Debug.WriteLine($"[Fill] Low-qty shrink: {canonicalItem.Quantity} requested, " + - $"from {workArea.Width:F1}x{workArea.Length:F1} " + - $"to {effectiveWorkArea.Width:F1}x{effectiveWorkArea.Length:F1}"); + Debug.WriteLine( + $"[Fill] Low-qty shrink: {canonicalItem.Quantity} requested, " + + $"from {workArea.Width:F1}x{workArea.Length:F1} " + + $"to {effectiveWorkArea.Width:F1}x{effectiveWorkArea.Length:F1}" + ); } var best = RunFillPipeline(canonicalItem, effectiveWorkArea, progress, token); - if (canonicalItem.Quantity > 0 && best.Count < canonicalItem.Quantity && effectiveWorkArea != workArea) + if ( + canonicalItem.Quantity > 0 + && best.Count < canonicalItem.Quantity + && effectiveWorkArea != workArea + ) { - Debug.WriteLine($"[Fill] Low-qty fallback: got {best.Count}, need {canonicalItem.Quantity}, retrying full area"); + Debug.WriteLine( + $"[Fill] Low-qty fallback: got {best.Count}, need {canonicalItem.Quantity}, retrying full area" + ); PhaseResults.Clear(); AngleResults.Clear(); best = RunFillPipeline(canonicalItem, workArea, progress, token); @@ -108,15 +131,18 @@ namespace OpenNest best = RebindAndUnCanonicalize(best, originalDrawing, sourceAngle); - ReportProgress(progress, new ProgressReport - { - Phase = WinnerPhase, - PlateNumber = PlateNumber, - Parts = best, - WorkArea = workArea, - Description = BuildProgressSummary(), - IsOverallBest = true, - }); + ReportProgress( + progress, + new ProgressReport + { + Phase = WinnerPhase, + PlateNumber = PlateNumber, + Parts = best, + WorkArea = workArea, + Description = BuildProgressSummary(), + IsOverallBest = true, + } + ); return best; } @@ -126,7 +152,11 @@ namespace OpenNest /// original Drawing (so consumers see the user's drawing identity, not the transient canonical copy) /// and composes sourceAngle onto each Part's rotation via CanonicalFrame.FromCanonical. /// - private static List RebindAndUnCanonicalize(List parts, Drawing original, double sourceAngle) + private static List RebindAndUnCanonicalize( + List parts, + Drawing original, + double sourceAngle + ) { if (parts == null || parts.Count == 0) return parts; @@ -164,8 +194,10 @@ namespace OpenNest private static List TryPlaceSingle(Drawing drawing, Box workArea) { var part = Part.CreateAtOrigin(drawing); - if (part.BoundingBox.Width > workArea.Width + Tolerance.Epsilon || - part.BoundingBox.Length > workArea.Length + Tolerance.Epsilon) + if ( + part.BoundingBox.Width > workArea.Width + Tolerance.Epsilon + || part.BoundingBox.Length > workArea.Length + Tolerance.Epsilon + ) return null; part.Offset(workArea.Location - part.BoundingBox.Location); @@ -175,7 +207,11 @@ namespace OpenNest private List TryPlaceBestFitPair(Drawing drawing, Box workArea) { var bestFits = BestFitCache.GetOrCompute( - drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing); + drawing, + Plate.Size.Length, + Plate.Size.Width, + Plate.PartSpacing + ); // Build pair candidates with a canonical drawing so their geometry matches // the coordinate frame of the cached fit results. @@ -189,9 +225,15 @@ namespace OpenNest continue; // Skip pairs that can't possibly fit the work area in either orientation. - if (fit.ShortestSide > System.Math.Min(workArea.Width, workArea.Length) + Tolerance.Epsilon) + if ( + fit.ShortestSide + > System.Math.Min(workArea.Width, workArea.Length) + Tolerance.Epsilon + ) continue; - if (fit.LongestSide > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon) + if ( + fit.LongestSide + > System.Math.Max(workArea.Width, workArea.Length) + Tolerance.Epsilon + ) continue; var landscape = fit.BuildParts(canonicalDrawing); @@ -247,8 +289,10 @@ namespace OpenNest private static bool TryOffsetToWorkArea(List parts, Box workArea) { var bbox = ((IEnumerable)parts).GetBoundingBox(); - if (bbox.Width > workArea.Width + Tolerance.Epsilon || - bbox.Length > workArea.Length + Tolerance.Epsilon) + if ( + bbox.Width > workArea.Width + Tolerance.Epsilon + || bbox.Length > workArea.Length + Tolerance.Epsilon + ) return false; var offset = workArea.Location - bbox.Location; @@ -271,7 +315,10 @@ namespace OpenNest return workArea; var bin = new Bin { Size = new Size(workArea.Width, workArea.Length) }; - var packItem = new Item { Size = new Size(bbox.Width + spacing, bbox.Length + spacing) }; + var packItem = new Item + { + Size = new Size(bbox.Width + spacing, bbox.Length + spacing), + }; var packer = new FillBestFit(bin); packer.Fill(packItem); var fullCount = bin.Items.Count; @@ -303,8 +350,12 @@ namespace OpenNest return new Box(workArea.X, workArea.Y, newLength, newWidth); } - private List RunFillPipeline(NestItem item, Box workArea, - IProgress progress, CancellationToken token) + private List RunFillPipeline( + NestItem item, + Box workArea, + IProgress progress, + CancellationToken token + ) { var context = new FillContext { @@ -326,8 +377,12 @@ namespace OpenNest return context.CurrentBest ?? new List(); } - public override List Fill(List groupParts, Box workArea, - IProgress progress, CancellationToken token) + public override List Fill( + List groupParts, + Box workArea, + IProgress progress, + CancellationToken token + ) { if (groupParts == null || groupParts.Count == 0) return new List(); @@ -346,25 +401,34 @@ namespace OpenNest var best = FillHelpers.FillPattern(engine, groupParts, angles, workArea, Comparer); PhaseResults.Add(new PhaseResult(NestPhase.Linear, best?.Count ?? 0, 0)); - Debug.WriteLine($"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}"); + Debug.WriteLine( + $"[Fill(groupParts,Box)] Linear pattern: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}" + ); - ReportProgress(progress, new ProgressReport - { - Phase = NestPhase.Linear, - PlateNumber = PlateNumber, - Parts = best, - WorkArea = workArea, - Description = BuildProgressSummary(), - IsOverallBest = true, - }); + ReportProgress( + progress, + new ProgressReport + { + Phase = NestPhase.Linear, + PlateNumber = PlateNumber, + Parts = best, + WorkArea = workArea, + Description = BuildProgressSummary(), + IsOverallBest = true, + } + ); return best ?? new List(); } // --- Pack API --- - public override List PackArea(Box box, List items, - IProgress progress, CancellationToken token) + public override List PackArea( + Box box, + List items, + IProgress progress, + CancellationToken token + ) { var binItems = BinConverter.ToItems(items, Plate.PartSpacing, Plate.Area()); var bin = BinConverter.CreateBin(box, Plate.PartSpacing); @@ -399,7 +463,10 @@ namespace OpenNest sw.Stop(); var phaseResult = new PhaseResult( - strategy.Phase, result?.Count ?? 0, sw.ElapsedMilliseconds); + strategy.Phase, + result?.Count ?? 0, + sw.ElapsedMilliseconds + ); context.PhaseResults.Add(phaseResult); // Keep engine's PhaseResults in sync so BuildProgressSummary() works @@ -409,7 +476,11 @@ namespace OpenNest // FillContext.ReportProgress updates CurrentBest during the // strategy's angle sweep. This catches strategies that return a // result without reporting it (e.g. RectBestFit). - var improved = context.Policy.Comparer.IsBetter(result, context.CurrentBest, context.WorkArea); + var improved = context.Policy.Comparer.IsBetter( + result, + context.CurrentBest, + context.WorkArea + ); if (improved) { context.CurrentBest = result; @@ -419,15 +490,18 @@ namespace OpenNest if (improved && context.CurrentBest != null && context.CurrentBest.Count > 0) { - ReportProgress(context.Progress, new ProgressReport - { - Phase = context.WinnerPhase, - PlateNumber = PlateNumber, - Parts = context.CurrentBest, - WorkArea = context.WorkArea, - Description = BuildProgressSummary(), - IsOverallBest = true, - }); + ReportProgress( + context.Progress, + new ProgressReport + { + Phase = context.WinnerPhase, + PlateNumber = PlateNumber, + Parts = context.CurrentBest, + WorkArea = context.WorkArea, + Description = BuildProgressSummary(), + IsOverallBest = true, + } + ); } } } @@ -438,6 +512,5 @@ namespace OpenNest RecordProductiveAngles(context.AngleResults); } - } } diff --git a/OpenNest.Engine/Fill/AngleCandidateBuilder.cs b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs index 738ad54..8945433 100644 --- a/OpenNest.Engine/Fill/AngleCandidateBuilder.cs +++ b/OpenNest.Engine/Fill/AngleCandidateBuilder.cs @@ -1,9 +1,9 @@ -using OpenNest.Engine.ML; -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using OpenNest.Engine.ML; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.Fill { @@ -25,7 +25,11 @@ namespace OpenNest.Engine.Fill return new List { 0 }; case PartType.Rectangle: - return new List { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI }; + return new List + { + classification.PrimaryAngle, + classification.PrimaryAngle + Angle.HalfPI, + }; default: return BuildIrregularAngles(item, classification.PrimaryAngle, workArea); @@ -84,7 +88,11 @@ namespace OpenNest.Engine.Fill } private static List ApplyMlPrediction( - NestItem item, Box workArea, double[] baseAngles, List fallback) + NestItem item, + Box workArea, + double[] baseAngles, + List fallback + ) { var features = FeatureExtractor.Extract(item.Drawing); if (features == null) @@ -108,7 +116,9 @@ namespace OpenNest.Engine.Fill mlAngles.Add(a); } - Debug.WriteLine($"[AngleCandidateBuilder] ML: {fallback.Count} sweep + {predicted.Count} predicted = {mlAngles.Count} total"); + Debug.WriteLine( + $"[AngleCandidateBuilder] ML: {fallback.Count} sweep + {predicted.Count} predicted = {mlAngles.Count} total" + ); return mlAngles; } @@ -121,7 +131,9 @@ namespace OpenNest.Engine.Fill pruned.Add(a); } - Debug.WriteLine($"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)"); + Debug.WriteLine( + $"[AngleCandidateBuilder] Pruned to {pruned.Count} angles (known-good)" + ); return pruned; } diff --git a/OpenNest.Engine/Fill/BestCombination.cs b/OpenNest.Engine/Fill/BestCombination.cs index 2b6c534..e43058e 100644 --- a/OpenNest.Engine/Fill/BestCombination.cs +++ b/OpenNest.Engine/Fill/BestCombination.cs @@ -6,7 +6,11 @@ namespace OpenNest internal static class BestCombination { - public static CombinationResult FindFrom2(double length1, double length2, double overallLength) + public static CombinationResult FindFrom2( + double length1, + double length2, + double overallLength + ) { overallLength += Tolerance.Epsilon; var count1 = 0; diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index 3d70535..394720e 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest.Engine.Fill @@ -14,8 +14,8 @@ namespace OpenNest.Engine.Fill { public static double Push(List movingParts, Plate plate, PushDirection direction) { - var obstacleParts = plate.Parts - .Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts)) + var obstacleParts = plate + .Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts)) .ToList(); return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); @@ -26,8 +26,8 @@ namespace OpenNest.Engine.Fill /// public static double Push(List movingParts, Plate plate, double angle) { - var obstacleParts = plate.Parts - .Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts)) + var obstacleParts = plate + .Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts)) .ToList(); var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); @@ -37,8 +37,13 @@ namespace OpenNest.Engine.Fill /// /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). /// - public static double Push(List movingParts, List obstacleParts, - Box workArea, double partSpacing, Vector direction) + public static double Push( + List movingParts, + List obstacleParts, + Box workArea, + double partSpacing, + Vector direction + ) { var opposite = -direction; @@ -84,33 +89,59 @@ namespace OpenNest.Engine.Fill for (var i = 0; i < obstacleBoxes.Length; i++) { var obstacleSpacingBox = obstacleSpacingBoxes[i]; - var reverseGap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, opposite); + var reverseGap = SpatialQuery.DirectionalGap( + movingSpacingBox, + obstacleSpacingBox, + opposite + ); if (reverseGap > 0) continue; - var gap = SpatialQuery.DirectionalGap(movingSpacingBox, obstacleSpacingBox, direction); + var gap = SpatialQuery.DirectionalGap( + movingSpacingBox, + obstacleSpacingBox, + direction + ); if (gap >= distance) continue; - if (!SpatialQuery.PerpendicularOverlap(movingSpacingBox, obstacleSpacingBox, direction)) + if ( + !SpatialQuery.PerpendicularOverlap( + movingSpacingBox, + obstacleSpacingBox, + direction + ) + ) continue; - movingEntities ??= halfSpacing > 0 - ? (needCutouts - ? PartGeometry.GetOffsetPartEntities(moving, halfSpacing) - : PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing)) - : (needCutouts - ? PartGeometry.GetPartEntities(moving) - : PartGeometry.GetPerimeterEntities(moving)); + movingEntities ??= + halfSpacing > 0 + ? ( + needCutouts + ? PartGeometry.GetOffsetPartEntities(moving, halfSpacing) + : PartGeometry.GetOffsetPerimeterEntities(moving, halfSpacing) + ) + : ( + needCutouts + ? PartGeometry.GetPartEntities(moving) + : PartGeometry.GetPerimeterEntities(moving) + ); - obstacleEntities[i] ??= halfSpacing > 0 - ? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing) - : PartGeometry.GetPerimeterEntities(obstacleParts[i]); + obstacleEntities[i] ??= + halfSpacing > 0 + ? PartGeometry.GetOffsetPerimeterEntities(obstacleParts[i], halfSpacing) + : PartGeometry.GetPerimeterEntities(obstacleParts[i]); - var d = SpatialQuery.DirectionalDistance(movingEntities, obstacleEntities[i], direction); - if (d <= Tolerance.Epsilon + var d = SpatialQuery.DirectionalDistance( + movingEntities, + obstacleEntities[i], + direction + ); + if ( + d <= Tolerance.Epsilon && partSpacing <= Tolerance.Epsilon - && CanNudgeWithoutOverlap(moving, obstacleParts[i], direction)) + && CanNudgeWithoutOverlap(moving, obstacleParts[i], direction) + ) { continue; } @@ -133,8 +164,12 @@ namespace OpenNest.Engine.Fill private static Box SpacingBounds(Box box, double spacing) { - return new Box(box.Left - spacing, box.Bottom - spacing, - box.Length + 2 * spacing, box.Width + 2 * spacing); + return new Box( + box.Left - spacing, + box.Bottom - spacing, + box.Length + 2 * spacing, + box.Width + 2 * spacing + ); } private static bool IntersectsAny(Part candidate, List parts) @@ -162,8 +197,13 @@ namespace OpenNest.Engine.Fill } } - public static double Push(List movingParts, List obstacleParts, - Box workArea, double partSpacing, PushDirection direction) + public static double Push( + List movingParts, + List obstacleParts, + Box workArea, + double partSpacing, + PushDirection direction + ) { var vector = SpatialQuery.DirectionToOffset(direction, 1.0); return Push(movingParts, obstacleParts, workArea, partSpacing, vector); @@ -174,17 +214,32 @@ namespace OpenNest.Engine.Fill /// Much faster but less precise — use as a coarse positioning pass before /// a full geometry Push. /// - public static double PushBoundingBox(List movingParts, Plate plate, PushDirection direction) + public static double PushBoundingBox( + List movingParts, + Plate plate, + PushDirection direction + ) { - var obstacleParts = plate.Parts - .Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts)) + var obstacleParts = plate + .Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts)) .ToList(); - return PushBoundingBox(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); + return PushBoundingBox( + movingParts, + obstacleParts, + plate.WorkArea(), + plate.PartSpacing, + direction + ); } - public static double PushBoundingBox(List movingParts, List obstacleParts, - Box workArea, double partSpacing, PushDirection direction) + public static double PushBoundingBox( + List movingParts, + List obstacleParts, + Box workArea, + double partSpacing, + PushDirection direction + ) { var obstacleBoxes = new Box[obstacleParts.Count]; for (var i = 0; i < obstacleParts.Count; i++) @@ -206,7 +261,11 @@ namespace OpenNest.Engine.Fill for (var i = 0; i < obstacleBoxes.Length; i++) { - var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); + var reverseGap = SpatialQuery.DirectionalGap( + movingBox, + obstacleBoxes[i], + opposite + ); if (reverseGap > 0) continue; @@ -219,7 +278,8 @@ namespace OpenNest.Engine.Fill var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); var d = gap - partSpacing - 0.002; - if (d < 0) d = 0; + if (d < 0) + d = 0; if (d < distance) distance = d; } @@ -240,8 +300,13 @@ namespace OpenNest.Engine.Fill /// Repeatedly pushes parts left then down until total movement per /// iteration falls below the given threshold. /// - public static void Settle(List parts, Box workArea, double partSpacing, - double threshold = 0.01, int maxIterations = 20) + public static void Settle( + List parts, + Box workArea, + double partSpacing, + double threshold = 0.01, + int maxIterations = 20 + ) { if (parts.Count < 2) return; diff --git a/OpenNest.Engine/Fill/DefaultFillComparer.cs b/OpenNest.Engine/Fill/DefaultFillComparer.cs index cb6e627..656511e 100644 --- a/OpenNest.Engine/Fill/DefaultFillComparer.cs +++ b/OpenNest.Engine/Fill/DefaultFillComparer.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index 579944f..ffeffec 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -1,10 +1,10 @@ -using OpenNest.Engine.Strategies; -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; +using OpenNest.Engine.Strategies; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.Fill { @@ -23,9 +23,12 @@ namespace OpenNest.Engine.Fill halfSpacing = partSpacing / 2; } - public List Fill(Drawing drawing, double rotationAngle = 0, + public List Fill( + Drawing drawing, + double rotationAngle = 0, CancellationToken token = default, - Action, string> reportProgress = null) + Action, string> reportProgress = null + ) { var pair = BuildPair(drawing, rotationAngle); if (pair == null) @@ -64,8 +67,10 @@ namespace OpenNest.Engine.Fill var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI); // Check that each part fits in the work area individually. - if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon || - part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon) + if ( + part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon + || part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon + ) return null; // Slide part2 toward part1 from the right using geometry-aware distance. @@ -80,7 +85,11 @@ namespace OpenNest.Engine.Fill // Slide part2 left toward part1. var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left); var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right); - var dist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left); + var dist = SpatialQuery.DirectionalDistance( + movingLines, + stationaryLines, + PushDirection.Left + ); if (dist < double.MaxValue && dist > 0) { @@ -93,8 +102,10 @@ namespace OpenNest.Engine.Fill return null; // Verify pair fits in work area. - if (pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon || - pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon) + if ( + pair.Value.Bbox.Width > workArea.Width + Tolerance.Epsilon + || pair.Value.Bbox.Length > workArea.Length + Tolerance.Epsilon + ) return null; return pair; @@ -121,8 +132,14 @@ namespace OpenNest.Engine.Fill // Find minimum distance from test pair sliding down toward original pair. var copyDistance = FindVerticalCopyDistance( - pair.Part1, pair.Part2, testPart1, testPart2, - boundary1, boundary2, pairHeight); + pair.Part1, + pair.Part2, + testPart1, + testPart2, + boundary1, + boundary2, + pairHeight + ); if (copyDistance <= 0) return column; @@ -144,25 +161,56 @@ namespace OpenNest.Engine.Fill } private double FindVerticalCopyDistance( - Part origPart1, Part origPart2, - Part testPart1, Part testPart2, - PartBoundary boundary1, PartBoundary boundary2, - double pairHeight) + Part origPart1, + Part origPart2, + Part testPart1, + Part testPart2, + PartBoundary boundary1, + PartBoundary boundary2, + double pairHeight + ) { // Check all 4 combinations: test parts sliding down toward original parts. var slidePairs = new[] { - (moving: boundary1, movingLoc: testPart1.Location, stationary: boundary1, stationaryLoc: origPart1.Location), - (moving: boundary1, movingLoc: testPart1.Location, stationary: boundary2, stationaryLoc: origPart2.Location), - (moving: boundary2, movingLoc: testPart2.Location, stationary: boundary1, stationaryLoc: origPart1.Location), - (moving: boundary2, movingLoc: testPart2.Location, stationary: boundary2, stationaryLoc: origPart2.Location), + ( + moving: boundary1, + movingLoc: testPart1.Location, + stationary: boundary1, + stationaryLoc: origPart1.Location + ), + ( + moving: boundary1, + movingLoc: testPart1.Location, + stationary: boundary2, + stationaryLoc: origPart2.Location + ), + ( + moving: boundary2, + movingLoc: testPart2.Location, + stationary: boundary1, + stationaryLoc: origPart1.Location + ), + ( + moving: boundary2, + movingLoc: testPart2.Location, + stationary: boundary2, + stationaryLoc: origPart2.Location + ), }; var minSlide = double.MaxValue; foreach (var (moving, movingLoc, stationary, stationaryLoc) in slidePairs) { - var d = SlideDistance(moving, movingLoc, stationary, stationaryLoc, PushDirection.Down); - if (d < minSlide) minSlide = d; + var d = SlideDistance( + moving, + movingLoc, + stationary, + stationaryLoc, + PushDirection.Down + ); + if (d < minSlide) + minSlide = d; } if (minSlide >= double.MaxValue || minSlide < 0) @@ -177,18 +225,24 @@ namespace OpenNest.Engine.Fill } private static double SlideDistance( - PartBoundary movingBoundary, Vector movingLocation, - PartBoundary stationaryBoundary, Vector stationaryLocation, - PushDirection direction) + PartBoundary movingBoundary, + Vector movingLocation, + PartBoundary stationaryBoundary, + Vector stationaryLocation, + PushDirection direction + ) { var opposite = SpatialQuery.OppositeDirection(direction); var movingEdges = movingBoundary.GetEdges(direction); var stationaryEdges = stationaryBoundary.GetEdges(opposite); return SpatialQuery.DirectionalDistance( - movingEdges, movingLocation, - stationaryEdges, stationaryLocation, - direction); + movingEdges, + movingLocation, + stationaryEdges, + stationaryLocation, + direction + ); } // --- Step 3: Iterative Adjustment --- @@ -249,7 +303,11 @@ namespace OpenNest.Engine.Fill return TryShiftDirection(pair, -adjustment, originalPairWidth); } - private PartPair? TryShiftDirection(PartPair pair, double verticalShift, double originalPairWidth) + private PartPair? TryShiftDirection( + PartPair pair, + double verticalShift, + double originalPairWidth + ) { // Clone parts so we don't mutate the originals. var p1 = (Part)pair.Part1.Clone(); diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index b21e289..5c18151 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -1,8 +1,8 @@ -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.Fill { @@ -34,9 +34,7 @@ namespace OpenNest.Engine.Fill private static PushDirection GetPushDirection(NestDirection direction) { - return direction == NestDirection.Horizontal - ? PushDirection.Left - : PushDirection.Down; + return direction == NestDirection.Horizontal ? PushDirection.Left : PushDirection.Down; } private static double GetDimension(Box box, NestDirection direction) @@ -75,10 +73,15 @@ namespace OpenNest.Engine.Fill var stationaryEntities = PartGeometry.GetOffsetPerimeterEntities(partA, HalfSpacing); var movingEntities = PartGeometry.GetOffsetPerimeterEntities( - partA.CloneAtOffset(offset), HalfSpacing); + partA.CloneAtOffset(offset), + HalfSpacing + ); var slideDistance = SpatialQuery.DirectionalDistance( - movingEntities, stationaryEntities, pushDir); + movingEntities, + stationaryEntities, + pushDir + ); if (slideDistance >= double.MaxValue || slideDistance < 0) return bboxDim + PartSpacing; @@ -140,12 +143,19 @@ namespace OpenNest.Engine.Fill continue; stationaryEntities[i] ??= PartGeometry.GetOffsetPerimeterEntities( - parts[i], HalfSpacing); + parts[i], + HalfSpacing + ); movingEntities[j] ??= PartGeometry.GetOffsetPerimeterEntities( - parts[j].CloneAtOffset(offset), HalfSpacing); + parts[j].CloneAtOffset(offset), + HalfSpacing + ); var slideDistance = SpatialQuery.DirectionalDistance( - movingEntities[j], stationaryEntities[i], pushDir); + movingEntities[j], + stationaryEntities[i], + pushDir + ); if (slideDistance >= double.MaxValue || slideDistance < 0) continue; @@ -209,10 +219,12 @@ namespace OpenNest.Engine.Fill { var part = basePart.CloneAtOffset(offset); - if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon && - part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon && - part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon && - part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon) + if ( + part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon + && part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon + && part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon + && part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon + ) { result.Add(part); } @@ -258,7 +270,11 @@ namespace OpenNest.Engine.Fill return result; } - private static bool HasOverlappingParts(List parts, out int overlapA, out int overlapB) + private static bool HasOverlappingParts( + List parts, + out int overlapA, + out int overlapB + ) { for (var i = 0; i < parts.Count; i++) { @@ -268,10 +284,10 @@ namespace OpenNest.Engine.Fill { var b2 = parts[j].BoundingBox; - var overlapX = System.Math.Min(b1.Right, b2.Right) - - System.Math.Max(b1.Left, b2.Left); - var overlapY = System.Math.Min(b1.Top, b2.Top) - - System.Math.Max(b1.Bottom, b2.Bottom); + var overlapX = + System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left); + var overlapY = + System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom); if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon) continue; @@ -305,8 +321,10 @@ namespace OpenNest.Engine.Fill template.Offset(WorkArea.Location - template.BoundingBox.Location); - if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon || - template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon) + if ( + template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon + || template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon + ) return pattern; pattern.Parts.Add(template); @@ -367,8 +385,14 @@ namespace OpenNest.Engine.Fill return gridResult; } - private void LogOverlap(string step, NestDirection tilingDir, - Pattern pattern, List parts, int idxA, int idxB) + private void LogOverlap( + string step, + NestDirection tilingDir, + Pattern pattern, + List parts, + int idxA, + int idxB + ) { var pa = parts[idxA]; var pb = parts[idxB]; @@ -377,22 +401,32 @@ namespace OpenNest.Engine.Fill Debug.WriteLine($"[FillLinear] OVERLAP FALLBACK ({Label ?? "unknown"})"); Debug.WriteLine($" Step: {step}, TilingDir: {tilingDir}"); - Debug.WriteLine($" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}"); - Debug.WriteLine($" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}"); + Debug.WriteLine( + $" WorkArea: ({WorkArea.X:F4},{WorkArea.Y:F4}) {WorkArea.Width:F4}x{WorkArea.Length:F4}, Spacing: {PartSpacing}" + ); + Debug.WriteLine( + $" Pattern: {pattern.Parts.Count} parts, bbox {pattern.BoundingBox.Width:F4}x{pattern.BoundingBox.Length:F4}" + ); Debug.WriteLine($" Total parts after tiling: {parts.Count}"); Debug.WriteLine($" Overlapping pair [{idxA}] vs [{idxB}]:"); - Debug.WriteLine($" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° " + - $"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})"); - Debug.WriteLine($" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° " + - $"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})"); + Debug.WriteLine( + $" [{idxA}]: drawing={pa.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pa.Rotation):F2}° " + + $"loc=({pa.Location.X:F4},{pa.Location.Y:F4}) bbox=({ba.Left:F4},{ba.Bottom:F4})-({ba.Right:F4},{ba.Top:F4})" + ); + Debug.WriteLine( + $" [{idxB}]: drawing={pb.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(pb.Rotation):F2}° " + + $"loc=({pb.Location.X:F4},{pb.Location.Y:F4}) bbox=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4})" + ); // Log all pattern seed parts for reproduction Debug.WriteLine($" Pattern seed parts:"); for (var i = 0; i < pattern.Parts.Count; i++) { var p = pattern.Parts[i]; - Debug.WriteLine($" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° " + - $"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}"); + Debug.WriteLine( + $" [{i}]: drawing={p.BaseDrawing?.Name ?? "?"} rot={Angle.ToDegrees(p.Rotation):F2}° " + + $"loc=({p.Location.X:F4},{p.Location.Y:F4}) bbox={p.BoundingBox.Width:F4}x{p.BoundingBox.Length:F4}" + ); } } @@ -446,8 +480,10 @@ namespace OpenNest.Engine.Fill var offset = WorkArea.Location - pattern.BoundingBox.Location; var basePattern = pattern.Clone(offset); - if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon || - basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon) + if ( + basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon + || basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon + ) return new List(); return FillGrid(basePattern, primaryAxis); diff --git a/OpenNest.Engine/Fill/FillResultCache.cs b/OpenNest.Engine/Fill/FillResultCache.cs index 0eb2f35..bc5d009 100644 --- a/OpenNest.Engine/Fill/FillResultCache.cs +++ b/OpenNest.Engine/Fill/FillResultCache.cs @@ -76,9 +76,10 @@ public static class FillResultCache } public bool Equals(CacheKey other) => - ReferenceEquals(Drawing, other.Drawing) && - Width == other.Width && Height == other.Height && - Spacing == other.Spacing; + ReferenceEquals(Drawing, other.Drawing) + && Width == other.Width + && Height == other.Height + && Spacing == other.Spacing; public override bool Equals(object obj) => obj is CacheKey other && Equals(other); diff --git a/OpenNest.Engine/Fill/FillScore.cs b/OpenNest.Engine/Fill/FillScore.cs index 957768c..78ef417 100644 --- a/OpenNest.Engine/Fill/FillScore.cs +++ b/OpenNest.Engine/Fill/FillScore.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { @@ -37,10 +37,14 @@ namespace OpenNest.Engine.Fill totalPartArea += part.BaseDrawing.Area; var bb = part.BoundingBox; - if (bb.Left < minX) minX = bb.Left; - if (bb.Bottom < minY) minY = bb.Bottom; - if (bb.Right > maxX) maxX = bb.Right; - if (bb.Top > maxY) maxY = bb.Top; + if (bb.Left < minX) + minX = bb.Left; + if (bb.Bottom < minY) + minY = bb.Bottom; + if (bb.Right > maxX) + maxX = bb.Right; + if (bb.Top > maxY) + maxY = bb.Top; } var bboxArea = (maxX - minX) * (maxY - minY); @@ -63,8 +67,11 @@ namespace OpenNest.Engine.Fill } public static bool operator >(FillScore a, FillScore b) => a.CompareTo(b) > 0; + public static bool operator <(FillScore a, FillScore b) => a.CompareTo(b) < 0; + public static bool operator >=(FillScore a, FillScore b) => a.CompareTo(b) >= 0; + public static bool operator <=(FillScore a, FillScore b) => a.CompareTo(b) <= 0; } } diff --git a/OpenNest.Engine/Fill/GridDedup.cs b/OpenNest.Engine/Fill/GridDedup.cs index 90eaf24..70d9517 100644 --- a/OpenNest.Engine/Fill/GridDedup.cs +++ b/OpenNest.Engine/Fill/GridDedup.cs @@ -29,7 +29,9 @@ public class GridDedup /// /// Gets or creates a GridDedup from FillContext.SharedState. /// - public static GridDedup GetOrCreate(System.Collections.Generic.Dictionary sharedState) + public static GridDedup GetOrCreate( + System.Collections.Generic.Dictionary sharedState + ) { if (sharedState.TryGetValue(SharedStateKey, out var existing)) return (GridDedup)existing; @@ -41,7 +43,11 @@ public class GridDedup private readonly struct GridKey : IEquatable { - private readonly int _patternW, _patternL, _workW, _workL, _dir; + private readonly int _patternW, + _patternL, + _workW, + _workL, + _dir; public GridKey(Box patternBox, Box workArea, NestDirection dir) { @@ -53,9 +59,11 @@ public class GridDedup } public bool Equals(GridKey other) => - _patternW == other._patternW && _patternL == other._patternL && - _workW == other._workW && _workL == other._workL && - _dir == other._dir; + _patternW == other._patternW + && _patternL == other._patternL + && _workW == other._workW + && _workL == other._workL + && _dir == other._dir; public override bool Equals(object obj) => obj is GridKey other && Equals(other); diff --git a/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs b/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs index 0fba3ac..510de13 100644 --- a/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs +++ b/OpenNest.Engine/Fill/HorizontalRemnantComparer.cs @@ -28,7 +28,7 @@ namespace OpenNest.Engine.Fill return candExtent < currExtent; return FillScore.Compute(candidate, workArea).Density - > FillScore.Compute(current, workArea).Density; + > FillScore.Compute(current, workArea).Density; } private static double YExtent(List parts) @@ -39,8 +39,10 @@ namespace OpenNest.Engine.Fill foreach (var part in parts) { var bb = part.BoundingBox; - if (bb.Bottom < minY) minY = bb.Bottom; - if (bb.Top > maxY) maxY = bb.Top; + if (bb.Bottom < minY) + minY = bb.Bottom; + if (bb.Top > maxY) + maxY = bb.Top; } return maxY - minY; diff --git a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs index 2669804..ee8a267 100644 --- a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs +++ b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs @@ -1,9 +1,9 @@ -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { @@ -33,7 +33,8 @@ namespace OpenNest.Engine.Fill CancellationToken token = default, IProgress progress = null, int plateNumber = 0, - Func> widthFillFunc = null) + Func> widthFillFunc = null + ) { if (items == null || items.Count == 0) return new IterativeShrinkResult(); @@ -48,19 +49,20 @@ namespace OpenNest.Engine.Fill if (item.Quantity <= 0) { var bbox = item.Drawing.Program.BoundingBox(); - var estimatedMax = bbox.Area() > 0 - ? (int)(workArea.Area() / bbox.Area()) * 2 - : 1000; + var estimatedMax = + bbox.Area() > 0 ? (int)(workArea.Area() / bbox.Area()) * 2 : 1000; - workItems.Add(new NestItem - { - Drawing = item.Drawing, - Quantity = System.Math.Max(1, estimatedMax), - Priority = item.Priority, - StepAngle = item.StepAngle, - RotationStart = item.RotationStart, - RotationEnd = item.RotationEnd - }); + workItems.Add( + new NestItem + { + Drawing = item.Drawing, + Quantity = System.Math.Max(1, estimatedMax), + Priority = item.Priority, + StepAngle = item.StepAngle, + RotationStart = item.RotationStart, + RotationEnd = item.RotationEnd, + } + ); } else { @@ -86,10 +88,32 @@ namespace OpenNest.Engine.Fill ShrinkResult widthResult = null; Parallel.Invoke( - () => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Length, token, - targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar), - () => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token, - targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar) + () => + heightResult = ShrinkFiller.Shrink( + fillFunc, + ni, + box, + spacing, + ShrinkAxis.Length, + token, + targetCount: target, + progress: progress, + plateNumber: plateNumber, + placedParts: placedSoFar + ), + () => + widthResult = ShrinkFiller.Shrink( + wFillFunc, + ni, + box, + spacing, + ShrinkAxis.Width, + token, + targetCount: target, + progress: progress, + plateNumber: plateNumber, + placedParts: placedSoFar + ) ); var heightScore = FillScore.Compute(heightResult.Parts, box); @@ -112,15 +136,18 @@ namespace OpenNest.Engine.Fill var allParts = new List(placedSoFar.Count + best.Count); allParts.AddRange(placedSoFar); allParts.AddRange(best); - NestEngineBase.ReportProgress(progress, new ProgressReport - { - Phase = NestPhase.Custom, - PlateNumber = plateNumber, - Parts = allParts, - WorkArea = box, - Description = $"Shrink: {best.Count} parts placed", - IsOverallBest = true, - }); + NestEngineBase.ReportProgress( + progress, + new ProgressReport + { + Phase = NestPhase.Custom, + PlateNumber = plateNumber, + Parts = allParts, + WorkArea = box, + Description = $"Shrink: {best.Count} parts placed", + IsOverallBest = true, + } + ); } // Accumulate for the next item's progress reports. @@ -136,8 +163,7 @@ namespace OpenNest.Engine.Fill var leftovers = new List(); foreach (var item in items) { - var placedCount = placed.Count(p => - ReferenceEquals(p.BaseDrawing, item.Drawing)); + var placedCount = placed.Count(p => ReferenceEquals(p.BaseDrawing, item.Drawing)); if (item.Quantity <= 0) continue; // unlimited items are always "satisfied" — no leftover @@ -145,15 +171,17 @@ namespace OpenNest.Engine.Fill var remaining = item.Quantity - placedCount; if (remaining > 0) { - leftovers.Add(new NestItem - { - Drawing = item.Drawing, - Quantity = remaining, - Priority = item.Priority, - StepAngle = item.StepAngle, - RotationStart = item.RotationStart, - RotationEnd = item.RotationEnd - }); + leftovers.Add( + new NestItem + { + Drawing = item.Drawing, + Quantity = remaining, + Priority = item.Priority, + StepAngle = item.StepAngle, + RotationStart = item.RotationStart, + RotationEnd = item.RotationEnd, + } + ); } } @@ -165,29 +193,43 @@ namespace OpenNest.Engine.Fill /// a staircase profile that maximizes usable remnant area. /// internal static void SortColumnsByHeight(List parts, double spacing) => - SortStrips(parts, spacing, - primaryEdge: b => b.Left, extentEdge: b => b.Right, - sortMetric: MaxTop, stripMin: MinLeft, stripMax: MaxRight, - makeOffset: d => new Vector(d, 0)); + SortStrips( + parts, + spacing, + primaryEdge: b => b.Left, + extentEdge: b => b.Right, + sortMetric: MaxTop, + stripMin: MinLeft, + stripMax: MaxRight, + makeOffset: d => new Vector(d, 0) + ); /// /// Sorts pair rows by width (narrowest first on the bottom) to create /// a staircase profile on the right side that maximizes usable remnant area. /// internal static void SortRowsByWidth(List parts, double spacing) => - SortStrips(parts, spacing, - primaryEdge: b => b.Bottom, extentEdge: b => b.Top, - sortMetric: MaxRight, stripMin: MinBottom, stripMax: MaxTop, - makeOffset: d => new Vector(0, d)); + SortStrips( + parts, + spacing, + primaryEdge: b => b.Bottom, + extentEdge: b => b.Top, + sortMetric: MaxRight, + stripMin: MinBottom, + stripMax: MaxTop, + makeOffset: d => new Vector(0, d) + ); private static void SortStrips( - List parts, double spacing, + List parts, + double spacing, Func primaryEdge, Func extentEdge, Func, double> sortMetric, Func, double> stripMin, Func, double> stripMax, - Func makeOffset) + Func makeOffset + ) { if (parts == null || parts.Count <= 1) return; @@ -250,7 +292,8 @@ namespace OpenNest.Engine.Fill { var max = double.MinValue; foreach (var p in col) - if (p.BoundingBox.Top > max) max = p.BoundingBox.Top; + if (p.BoundingBox.Top > max) + max = p.BoundingBox.Top; return max; } @@ -258,7 +301,8 @@ namespace OpenNest.Engine.Fill { var max = double.MinValue; foreach (var p in col) - if (p.BoundingBox.Right > max) max = p.BoundingBox.Right; + if (p.BoundingBox.Right > max) + max = p.BoundingBox.Right; return max; } @@ -266,7 +310,8 @@ namespace OpenNest.Engine.Fill { var min = double.MaxValue; foreach (var p in col) - if (p.BoundingBox.Left < min) min = p.BoundingBox.Left; + if (p.BoundingBox.Left < min) + min = p.BoundingBox.Left; return min; } @@ -274,7 +319,8 @@ namespace OpenNest.Engine.Fill { var min = double.MaxValue; foreach (var p in row) - if (p.BoundingBox.Bottom < min) min = p.BoundingBox.Bottom; + if (p.BoundingBox.Bottom < min) + min = p.BoundingBox.Bottom; return min; } } diff --git a/OpenNest.Engine/Fill/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs index 11f7f2a..89c563a 100644 --- a/OpenNest.Engine/Fill/PairFiller.cs +++ b/OpenNest.Engine/Fill/PairFiller.cs @@ -1,7 +1,3 @@ -using OpenNest.Engine.BestFit; -using OpenNest.Engine.Strategies; -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; @@ -9,6 +5,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using OpenNest.Engine; +using OpenNest.Engine.BestFit; +using OpenNest.Engine.Strategies; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.Fill { @@ -44,28 +44,49 @@ namespace OpenNest.Engine.Fill this.dedup = dedup ?? new GridDedup(); } - public PairFillResult Fill(NestItem item, Box workArea, + public PairFillResult Fill( + NestItem item, + Box workArea, CancellationToken token = default, - Action, string> reportProgress = null) + Action, string> reportProgress = null + ) { var bestFits = BestFitCache.GetOrCompute( - item.Drawing, plateSize.Length, plateSize.Width, partSpacing); + item.Drawing, + plateSize.Length, + plateSize.Width, + partSpacing + ); var candidates = SelectPairCandidates(bestFits, workArea); - Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); - Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}"); + Debug.WriteLine( + $"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}" + ); + Debug.WriteLine( + $"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}" + ); var targetCount = item.Quantity > 0 ? item.Quantity : 0; - var parts = EvaluateCandidates(candidates, item.Drawing, workArea, targetCount, - token, reportProgress); + var parts = EvaluateCandidates( + candidates, + item.Drawing, + workArea, + targetCount, + token, + reportProgress + ); return new PairFillResult { Parts = parts, BestFits = bestFits }; } private List EvaluateCandidates( - List candidates, Drawing drawing, - Box workArea, int targetCount, - CancellationToken token, Action, string> reportProgress) + List candidates, + Drawing drawing, + Box workArea, + int targetCount, + CancellationToken token, + Action, string> reportProgress + ) { List best = null; var sinceImproved = 0; @@ -89,14 +110,23 @@ namespace OpenNest.Engine.Fill var minCountToBeat = best?.Count ?? 0; var results = new List[batchCount]; - Parallel.For(0, batchCount, + Parallel.For( + 0, + batchCount, new ParallelOptions { CancellationToken = token }, j => { results[j] = EvaluateCandidate( - candidates[batchStart + j], drawing, batchWorkArea, - minCountToBeat, maxUtilization, partArea, token); - }); + candidates[batchStart + j], + drawing, + batchWorkArea, + minCountToBeat, + maxUtilization, + partArea, + token + ); + } + ); for (var j = 0; j < batchCount; j++) { @@ -104,20 +134,29 @@ namespace OpenNest.Engine.Fill { best = results[j]; sinceImproved = 0; - effectiveWorkArea = TryReduceWorkArea(best, targetCount, workArea, effectiveWorkArea); + effectiveWorkArea = TryReduceWorkArea( + best, + targetCount, + workArea, + effectiveWorkArea + ); } else { sinceImproved++; } - reportProgress?.Invoke(best, - $"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts"); + reportProgress?.Invoke( + best, + $"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts" + ); } if (batchEnd >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit) { - Debug.WriteLine($"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates"); + Debug.WriteLine( + $"[PairFiller] Early exit at {batchEnd}/{candidates.Count} — no improvement in last {sinceImproved} candidates" + ); break; } } @@ -135,7 +174,12 @@ namespace OpenNest.Engine.Fill return best ?? new List(); } - private static Box TryReduceWorkArea(List parts, int targetCount, Box workArea, Box effectiveWorkArea) + private static Box TryReduceWorkArea( + List parts, + int targetCount, + Box workArea, + Box effectiveWorkArea + ) { if (targetCount <= 0 || parts.Count <= targetCount) return effectiveWorkArea; @@ -144,7 +188,9 @@ namespace OpenNest.Engine.Fill if (reduced.Area() >= effectiveWorkArea.Area()) return effectiveWorkArea; - Debug.WriteLine($"[PairFiller] Reduced work area to {reduced.Width:F2}x{reduced.Length:F2} (trimmed to {targetCount + 1} parts)"); + Debug.WriteLine( + $"[PairFiller] Reduced work area to {reduced.Width:F2}x{reduced.Length:F2} (trimmed to {targetCount + 1} parts)" + ); return reduced; } @@ -158,23 +204,30 @@ namespace OpenNest.Engine.Fill if (parts.Count <= targetCount) return workArea; - var sorted = parts - .OrderByDescending(p => p.BoundingBox.Top) - .ToList(); + var sorted = parts.OrderByDescending(p => p.BoundingBox.Top).ToList(); var trimCount = sorted.Count - targetCount; var remaining = sorted.Skip(trimCount).ToList(); var newTop = remaining.Max(p => p.BoundingBox.Top); - return new Box(workArea.X, workArea.Y, + return new Box( + workArea.X, + workArea.Y, workArea.Length, - System.Math.Min(newTop - workArea.Y, workArea.Width)); + System.Math.Min(newTop - workArea.Y, workArea.Width) + ); } - private List EvaluateCandidate(BestFitResult candidate, Drawing drawing, - Box workArea, int minCountToBeat, double maxUtilization, double partArea, - CancellationToken token) + private List EvaluateCandidate( + BestFitResult candidate, + Drawing drawing, + Box workArea, + int minCountToBeat, + double maxUtilization, + double partArea, + CancellationToken token + ) { var pairParts = candidate.BuildParts(drawing); var angles = BuildTilingAngles(candidate); @@ -211,10 +264,16 @@ namespace OpenNest.Engine.Fill { var topCount = grids[0].Parts.Count; var optimisticRemnant = EstimateRemnantUpperBound( - grids[0].Parts, workArea, maxUtilization, partArea); + grids[0].Parts, + workArea, + maxUtilization, + partArea + ); if (topCount + optimisticRemnant <= minCountToBeat) { - Debug.WriteLine($"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}"); + Debug.WriteLine( + $"[PairFiller] Skipping candidate: grid {topCount} + estimate {optimisticRemnant} <= best {minCountToBeat}" + ); return null; } } @@ -230,7 +289,11 @@ namespace OpenNest.Engine.Fill if (best != null) { var remnantBound = EstimateRemnantUpperBound( - gridParts, workArea, maxUtilization, partArea); + gridParts, + workArea, + maxUtilization, + partArea + ); if (gridParts.Count + remnantBound <= best.Count) break; // sorted descending, so remaining are even smaller } @@ -255,8 +318,12 @@ namespace OpenNest.Engine.Fill return best; } - private int EstimateRemnantUpperBound(List gridParts, Box workArea, - double maxUtilization, double partArea) + private int EstimateRemnantUpperBound( + List gridParts, + Box workArea, + double maxUtilization, + double partArea + ) { var gridBox = ((IEnumerable)gridParts).GetBoundingBox(); @@ -271,8 +338,12 @@ namespace OpenNest.Engine.Fill return (int)(remnantArea * maxUtilization / partArea) + 1; } - private List FillRemnant(List gridParts, Drawing drawing, - Box workArea, CancellationToken token) + private List FillRemnant( + List gridParts, + Drawing drawing, + Box workArea, + CancellationToken token + ) { var gridBox = ((IEnumerable)gridParts).GetBoundingBox(); var partBox = drawing.Program.BoundingBox(); @@ -322,14 +393,19 @@ namespace OpenNest.Engine.Fill token.ThrowIfCancellationRequested(); var result = FillHelpers.FillWithDirectionPreference( dir => filler.Fill(drawing, angle, dir), - null, comparer, remnantBox); + null, + comparer, + remnantBox + ); if (result != null && result.Count > (parts?.Count ?? 0)) parts = result; } - Debug.WriteLine($"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " + - $"{remnantBox.Width:F2}x{remnantBox.Length:F2}"); + Debug.WriteLine( + $"[PairFiller] Remnant: {parts?.Count ?? 0} parts in " + + $"{remnantBox.Width:F2}x{remnantBox.Length:F2}" + ); if (parts != null && parts.Count > 0) { @@ -365,16 +441,19 @@ namespace OpenNest.Engine.Fill if (workShortSide < plateShortSide * 0.5) { // Strip mode: prioritize candidates that fit the narrow dimension. - var stripCandidates = kept - .Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon - && r.Utilization >= MinStripUtilization) + var stripCandidates = kept.Where(r => + r.ShortestSide <= workShortSide + Tolerance.Epsilon + && r.Utilization >= MinStripUtilization + ) .ToList(); SortByEstimatedCount(stripCandidates, workArea); var top = stripCandidates.Take(MaxStripCandidates).ToList(); - Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})"); + Debug.WriteLine( + $"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})" + ); return top; } @@ -389,16 +468,18 @@ namespace OpenNest.Engine.Fill var w = workArea.Width; var l = workArea.Length; - candidates.Sort((a, b) => - { - var aCount = EstimateTileCount(a, w, l); - var bCount = EstimateTileCount(b, w, l); + candidates.Sort( + (a, b) => + { + var aCount = EstimateTileCount(a, w, l); + var bCount = EstimateTileCount(b, w, l); - if (aCount != bCount) - return bCount.CompareTo(aCount); + if (aCount != bCount) + return bCount.CompareTo(aCount); - return b.Utilization.CompareTo(a.Utilization); - }); + return b.Utilization.CompareTo(a.Utilization); + } + ); } private int EstimateTileCount(BestFitResult r, double areaW, double areaL) @@ -410,7 +491,8 @@ namespace OpenNest.Engine.Fill private int EstimateCount(double pairW, double pairH, double areaW, double areaL) { - if (pairW <= 0 || pairH <= 0) return 0; + if (pairW <= 0 || pairH <= 0) + return 0; var cols = (int)((areaW + partSpacing) / (pairW + partSpacing)); var rows = (int)((areaL + partSpacing) / (pairH + partSpacing)); return cols * rows * 2; diff --git a/OpenNest.Engine/Fill/PartBoundary.cs b/OpenNest.Engine/Fill/PartBoundary.cs index ffedbbc..3a6952e 100644 --- a/OpenNest.Engine/Fill/PartBoundary.cs +++ b/OpenNest.Engine/Fill/PartBoundary.cs @@ -1,7 +1,7 @@ -using OpenNest.Converters; -using OpenNest.Geometry; using System.Collections.Generic; using System.Linq; +using OpenNest.Converters; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { @@ -23,7 +23,8 @@ namespace OpenNest.Engine.Fill public PartBoundary(Part part, double spacing) { - var entities = ConvertProgram.ToGeometry(part.Program) + var entities = ConvertProgram + .ToGeometry(part.Program) .Where(e => e.Layer == SpecialLayers.Cut) .ToList(); @@ -39,21 +40,29 @@ namespace OpenNest.Engine.Fill { // Circumscribe arcs so polygon vertices are always outside // the true arc — guarantees the boundary never under-estimates. - var polygon = offsetEntity.ToPolygonWithTolerance(PolygonTolerance, circumscribe: true); + var polygon = offsetEntity.ToPolygonWithTolerance( + PolygonTolerance, + circumscribe: true + ); polygon.RemoveSelfIntersections(); _polygons.Add(polygon); } } PrecomputeDirectionalEdges( - out _leftEdges, out _rightEdges, out _upEdges, out _downEdges); + out _leftEdges, + out _rightEdges, + out _upEdges, + out _downEdges + ); } private void PrecomputeDirectionalEdges( out (Vector start, Vector end)[] leftEdges, out (Vector start, Vector end)[] rightEdges, out (Vector start, Vector end)[] upEdges, - out (Vector start, Vector end)[] downEdges) + out (Vector start, Vector end)[] downEdges + ) { var left = new List<(Vector, Vector)>(); var right = new List<(Vector, Vector)>(); @@ -86,10 +95,14 @@ namespace OpenNest.Engine.Fill var dy = verts[i].Y - verts[i - 1].Y; var edge = (verts[i - 1], verts[i]); - if (-sign * dy > 0) left.Add(edge); - if (sign * dy > 0) right.Add(edge); - if (-sign * dx > 0) up.Add(edge); - if (sign * dx > 0) down.Add(edge); + if (-sign * dy > 0) + left.Add(edge); + if (sign * dy > 0) + right.Add(edge); + if (-sign * dx > 0) + up.Add(edge); + if (sign * dx > 0) + down.Add(edge); } } @@ -145,11 +158,16 @@ namespace OpenNest.Engine.Fill { switch (direction) { - case PushDirection.Left: return _leftEdges; - case PushDirection.Right: return _rightEdges; - case PushDirection.Up: return _upEdges; - case PushDirection.Down: return _downEdges; - default: return _leftEdges; + case PushDirection.Left: + return _leftEdges; + case PushDirection.Right: + return _rightEdges; + case PushDirection.Up: + return _upEdges; + case PushDirection.Down: + return _downEdges; + default: + return _leftEdges; } } diff --git a/OpenNest.Engine/Fill/Pattern.cs b/OpenNest.Engine/Fill/Pattern.cs index 1432fd4..98493fa 100644 --- a/OpenNest.Engine/Fill/Pattern.cs +++ b/OpenNest.Engine/Fill/Pattern.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { diff --git a/OpenNest.Engine/Fill/PatternTiler.cs b/OpenNest.Engine/Fill/PatternTiler.cs index fe50c80..6cc372c 100644 --- a/OpenNest.Engine/Fill/PatternTiler.cs +++ b/OpenNest.Engine/Fill/PatternTiler.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { diff --git a/OpenNest.Engine/Fill/RemnantFiller.cs b/OpenNest.Engine/Fill/RemnantFiller.cs index 35015d1..ddf9dee 100644 --- a/OpenNest.Engine/Fill/RemnantFiller.cs +++ b/OpenNest.Engine/Fill/RemnantFiller.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Threading; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { @@ -31,7 +31,8 @@ namespace OpenNest.Engine.Fill List items, Func> fillFunc, CancellationToken token = default, - IProgress progress = null) + IProgress progress = null + ) { if (items == null || items.Count == 0) return new List(); @@ -60,13 +61,19 @@ namespace OpenNest.Engine.Fill private static Dictionary BuildLocalQuantities(List items) { - var localQty = new Dictionary(items.Count, ReferenceEqualityComparer.Instance); + var localQty = new Dictionary( + items.Count, + ReferenceEqualityComparer.Instance + ); foreach (var item in items) localQty[item.Drawing] = item.Quantity; return localQty; } - private static double FindMinItemDimension(List items, Dictionary localQty) + private static double FindMinItemDimension( + List items, + Dictionary localQty + ) { var minDim = double.MaxValue; foreach (var item in items) @@ -87,7 +94,8 @@ namespace OpenNest.Engine.Fill Dictionary localQty, Func> fillFunc, List allParts, - CancellationToken token) + CancellationToken token + ) { foreach (var item in items) { @@ -147,21 +155,30 @@ namespace OpenNest.Engine.Fill foreach (var p in parts) { var bb = p.BoundingBox; - if (bb.Left < left) left = bb.Left; - if (bb.Bottom < bottom) bottom = bb.Bottom; - if (bb.Right > right) right = bb.Right; - if (bb.Top > top) top = bb.Top; + if (bb.Left < left) + left = bb.Left; + if (bb.Bottom < bottom) + bottom = bb.Bottom; + if (bb.Right > right) + right = bb.Right; + if (bb.Top > top) + top = bb.Top; } - return new Box(left - spacing, bottom - spacing, - right - left + spacing * 2, top - bottom + spacing * 2); + return new Box( + left - spacing, + bottom - spacing, + right - left + spacing * 2, + top - bottom + spacing * 2 + ); } private static List TryFillInRemnants( NestItem item, int qty, List freeBoxes, - Func> fillFunc) + Func> fillFunc + ) { var itemBbox = item.Drawing.Program.BoundingBox(); diff --git a/OpenNest.Engine/Fill/RemnantFinder.cs b/OpenNest.Engine/Fill/RemnantFinder.cs index 8d230c4..bd6c3ed 100644 --- a/OpenNest.Engine/Fill/RemnantFinder.cs +++ b/OpenNest.Engine/Fill/RemnantFinder.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; namespace OpenNest.Engine.Fill { @@ -90,12 +90,14 @@ namespace OpenNest.Engine.Fill results.Add(new TieredRemnant(remnant, 1)); } - results.Sort((a, b) => - { - if (a.Priority != b.Priority) - return a.Priority.CompareTo(b.Priority); - return b.Box.Area().CompareTo(a.Box.Area()); - }); + results.Sort( + (a, b) => + { + if (a.Priority != b.Priority) + return a.Priority.CompareTo(b.Priority); + return b.Box.Area().CompareTo(a.Box.Area()); + } + ); return results; } @@ -125,11 +127,7 @@ namespace OpenNest.Engine.Fill ys.Add(obs.Top); } - var grid = new CellGrid - { - XCoords = xs.ToList(), - YCoords = ys.ToList(), - }; + var grid = new CellGrid { XCoords = xs.ToList(), YCoords = ys.ToList() }; grid.Cols = grid.XCoords.Count - 1; grid.Rows = grid.YCoords.Count - 1; @@ -146,9 +144,12 @@ namespace OpenNest.Engine.Fill { for (var c = 0; c < grid.Cols; c++) { - var cell = new Box(grid.XCoords[c], grid.YCoords[r], + var cell = new Box( + grid.XCoords[c], + grid.YCoords[r], grid.XCoords[c + 1] - grid.XCoords[c], - grid.YCoords[r + 1] - grid.YCoords[r]); + grid.YCoords[r + 1] - grid.YCoords[r] + ); grid.Empty[r, c] = !OverlapsAny(cell, clipped); } @@ -175,8 +176,12 @@ namespace OpenNest.Engine.Fill { foreach (var obs in obstacles) { - if (cell.Left < obs.Right && cell.Right > obs.Left && - cell.Bottom < obs.Top && cell.Top > obs.Bottom) + if ( + cell.Left < obs.Right + && cell.Right > obs.Left + && cell.Bottom < obs.Top + && cell.Top > obs.Bottom + ) return true; } @@ -227,24 +232,26 @@ namespace OpenNest.Engine.Fill private static bool IsContainedIn(Box inner, Box outer) { var eps = Math.Tolerance.Epsilon; - return inner.Left >= outer.Left - eps && - inner.Right <= outer.Right + eps && - inner.Bottom >= outer.Bottom - eps && - inner.Top <= outer.Top + eps; + return inner.Left >= outer.Left - eps + && inner.Right <= outer.Right + eps + && inner.Bottom >= outer.Bottom - eps + && inner.Top <= outer.Top + eps; } private void SortByEdgeProximity(List boxes) { - boxes.Sort((a, b) => - { - var aEdge = TouchesEdge(a) ? 1 : 0; - var bEdge = TouchesEdge(b) ? 1 : 0; + boxes.Sort( + (a, b) => + { + var aEdge = TouchesEdge(a) ? 1 : 0; + var bEdge = TouchesEdge(b) ? 1 : 0; - if (aEdge != bEdge) - return bEdge.CompareTo(aEdge); + if (aEdge != bEdge) + return bEdge.CompareTo(aEdge); - return b.Area().CompareTo(a.Area()); - }); + return b.Area().CompareTo(a.Area()); + } + ); } private bool TouchesEdge(Box box) @@ -264,30 +271,47 @@ namespace OpenNest.Engine.Fill foreach (var obs in Obstacles) { - if (obs.Left < envLeft) envLeft = obs.Left; - if (obs.Bottom < envBottom) envBottom = obs.Bottom; - if (obs.Right > envRight) envRight = obs.Right; - if (obs.Top > envTop) envTop = obs.Top; + if (obs.Left < envLeft) + envLeft = obs.Left; + if (obs.Bottom < envBottom) + envBottom = obs.Bottom; + if (obs.Right > envRight) + envRight = obs.Right; + if (obs.Top > envTop) + envTop = obs.Top; } return new Box(envLeft, envBottom, envRight - envLeft, envTop - envBottom); } - private static void SplitAtEnvelope(Box remnant, Box envelope, double minDim, List results) + private static void SplitAtEnvelope( + Box remnant, + Box envelope, + double minDim, + List results + ) { var eps = Math.Tolerance.Epsilon; // Fully within the envelope. - if (remnant.Left >= envelope.Left - eps && remnant.Right <= envelope.Right + eps && - remnant.Bottom >= envelope.Bottom - eps && remnant.Top <= envelope.Top + eps) + if ( + remnant.Left >= envelope.Left - eps + && remnant.Right <= envelope.Right + eps + && remnant.Bottom >= envelope.Bottom - eps + && remnant.Top <= envelope.Top + eps + ) { results.Add(new TieredRemnant(remnant, 0)); return; } // Fully outside the envelope (no overlap). - if (remnant.Left >= envelope.Right - eps || remnant.Right <= envelope.Left + eps || - remnant.Bottom >= envelope.Top - eps || remnant.Top <= envelope.Bottom + eps) + if ( + remnant.Left >= envelope.Right - eps + || remnant.Right <= envelope.Left + eps + || remnant.Bottom >= envelope.Top - eps + || remnant.Top <= envelope.Bottom + eps + ) { results.Add(new TieredRemnant(remnant, 2)); return; @@ -300,36 +324,116 @@ namespace OpenNest.Engine.Fill var innerTop = System.Math.Min(remnant.Top, envelope.Top); // Inner portion (priority 0). - TryAdd(results, innerLeft, innerBottom, innerRight - innerLeft, innerTop - innerBottom, 0, minDim); + TryAdd( + results, + innerLeft, + innerBottom, + innerRight - innerLeft, + innerTop - innerBottom, + 0, + minDim + ); // Edge extensions (priority 1). if (remnant.Right > envelope.Right + eps) - TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, remnant.Width, 1, minDim); + TryAdd( + results, + envelope.Right, + remnant.Bottom, + remnant.Right - envelope.Right, + remnant.Width, + 1, + minDim + ); if (remnant.Left < envelope.Left - eps) - TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, remnant.Width, 1, minDim); + TryAdd( + results, + remnant.Left, + remnant.Bottom, + envelope.Left - remnant.Left, + remnant.Width, + 1, + minDim + ); if (remnant.Top > envelope.Top + eps) - TryAdd(results, innerLeft, envelope.Top, innerRight - innerLeft, remnant.Top - envelope.Top, 1, minDim); + TryAdd( + results, + innerLeft, + envelope.Top, + innerRight - innerLeft, + remnant.Top - envelope.Top, + 1, + minDim + ); if (remnant.Bottom < envelope.Bottom - eps) - TryAdd(results, innerLeft, remnant.Bottom, innerRight - innerLeft, envelope.Bottom - remnant.Bottom, 1, minDim); + TryAdd( + results, + innerLeft, + remnant.Bottom, + innerRight - innerLeft, + envelope.Bottom - remnant.Bottom, + 1, + minDim + ); // Corner extensions (priority 2). if (remnant.Right > envelope.Right + eps && remnant.Top > envelope.Top + eps) - TryAdd(results, envelope.Right, envelope.Top, remnant.Right - envelope.Right, remnant.Top - envelope.Top, 2, minDim); + TryAdd( + results, + envelope.Right, + envelope.Top, + remnant.Right - envelope.Right, + remnant.Top - envelope.Top, + 2, + minDim + ); if (remnant.Right > envelope.Right + eps && remnant.Bottom < envelope.Bottom - eps) - TryAdd(results, envelope.Right, remnant.Bottom, remnant.Right - envelope.Right, envelope.Bottom - remnant.Bottom, 2, minDim); + TryAdd( + results, + envelope.Right, + remnant.Bottom, + remnant.Right - envelope.Right, + envelope.Bottom - remnant.Bottom, + 2, + minDim + ); if (remnant.Left < envelope.Left - eps && remnant.Top > envelope.Top + eps) - TryAdd(results, remnant.Left, envelope.Top, envelope.Left - remnant.Left, remnant.Top - envelope.Top, 2, minDim); + TryAdd( + results, + remnant.Left, + envelope.Top, + envelope.Left - remnant.Left, + remnant.Top - envelope.Top, + 2, + minDim + ); if (remnant.Left < envelope.Left - eps && remnant.Bottom < envelope.Bottom - eps) - TryAdd(results, remnant.Left, remnant.Bottom, envelope.Left - remnant.Left, envelope.Bottom - remnant.Bottom, 2, minDim); + TryAdd( + results, + remnant.Left, + remnant.Bottom, + envelope.Left - remnant.Left, + envelope.Bottom - remnant.Bottom, + 2, + minDim + ); } - private static void TryAdd(List results, double x, double y, double w, double h, int priority, double minDim) + private static void TryAdd( + List results, + double x, + double y, + double w, + double h, + int priority, + double minDim + ) { if (w >= minDim && h >= minDim) results.Add(new TieredRemnant(new Box(x, y, w, h), priority)); @@ -379,10 +483,14 @@ namespace OpenNest.Engine.Fill var top = stack.Pop(); startCol = top.startCol; - candidates.Add(new Box( - grid.XCoords[top.startCol], grid.YCoords[r - top.h + 1], - grid.XCoords[c] - grid.XCoords[top.startCol], - grid.YCoords[r + 1] - grid.YCoords[r - top.h + 1])); + candidates.Add( + new Box( + grid.XCoords[top.startCol], + grid.YCoords[r - top.h + 1], + grid.XCoords[c] - grid.XCoords[top.startCol], + grid.YCoords[r + 1] - grid.YCoords[r - top.h + 1] + ) + ); } if (h > 0) diff --git a/OpenNest.Engine/Fill/RotationAnalysis.cs b/OpenNest.Engine/Fill/RotationAnalysis.cs index 5ce61ce..b43be1a 100644 --- a/OpenNest.Engine/Fill/RotationAnalysis.cs +++ b/OpenNest.Engine/Fill/RotationAnalysis.cs @@ -1,8 +1,8 @@ +using System.Collections.Generic; +using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; -using System.Linq; namespace OpenNest.Engine.Fill { @@ -14,7 +14,8 @@ namespace OpenNest.Engine.Fill /// public static double FindBestRotation(NestItem item) { - var entities = ConvertProgram.ToGeometry(item.Drawing.Program) + var entities = ConvertProgram + .ToGeometry(item.Drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); @@ -62,7 +63,8 @@ namespace OpenNest.Engine.Fill foreach (var part in parts) { - var entities = ConvertProgram.ToGeometry(part.Program) + var entities = ConvertProgram + .ToGeometry(part.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); diff --git a/OpenNest.Engine/Fill/ShrinkFiller.cs b/OpenNest.Engine/Fill/ShrinkFiller.cs index 33ce056..5fefd8a 100644 --- a/OpenNest.Engine/Fill/ShrinkFiller.cs +++ b/OpenNest.Engine/Fill/ShrinkFiller.cs @@ -1,13 +1,17 @@ -using OpenNest.Geometry; -using OpenNest.RectanglePacking; using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using OpenNest.Geometry; +using OpenNest.RectanglePacking; namespace OpenNest.Engine.Fill { - public enum ShrinkAxis { Width, Length } + public enum ShrinkAxis + { + Width, + Length, + } public class ShrinkResult { @@ -23,14 +27,16 @@ namespace OpenNest.Engine.Fill { public static ShrinkResult Shrink( Func> fillFunc, - NestItem item, Box box, + NestItem item, + Box box, double spacing, ShrinkAxis axis, CancellationToken token = default, int targetCount = 0, IProgress progress = null, int plateNumber = 0, - List placedParts = null) + List placedParts = null + ) { var startBox = box; if (targetCount > 0) @@ -38,8 +44,7 @@ namespace OpenNest.Engine.Fill var parts = fillFunc(item, startBox); - if (targetCount > 0 && startBox != box - && (parts == null || parts.Count < targetCount)) + if (targetCount > 0 && startBox != box && (parts == null || parts.Count < targetCount)) { parts = fillFunc(item, box); } @@ -47,9 +52,8 @@ namespace OpenNest.Engine.Fill if (parts == null || parts.Count == 0) return new ShrinkResult { Parts = parts ?? new List(), Dimension = 0 }; - var shrinkTarget = targetCount > 0 - ? System.Math.Min(targetCount, parts.Count) - : parts.Count; + var shrinkTarget = + targetCount > 0 ? System.Math.Min(targetCount, parts.Count) : parts.Count; if (parts.Count > shrinkTarget) parts = TrimToCount(parts, shrinkTarget, axis); @@ -62,16 +66,22 @@ namespace OpenNest.Engine.Fill } private static void ReportShrinkProgress( - IProgress progress, int plateNumber, - List placedParts, List bestParts, - Box workArea, ShrinkAxis axis, double dim) + IProgress progress, + int plateNumber, + List placedParts, + List bestParts, + Box workArea, + ShrinkAxis axis, + double dim + ) { if (progress == null) return; - var allParts = placedParts != null && placedParts.Count > 0 - ? new List(placedParts.Count + bestParts.Count) - : new List(bestParts.Count); + var allParts = + placedParts != null && placedParts.Count > 0 + ? new List(placedParts.Count + bestParts.Count) + : new List(bestParts.Count); if (placedParts != null && placedParts.Count > 0) allParts.AddRange(placedParts); @@ -79,14 +89,17 @@ namespace OpenNest.Engine.Fill var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}"; - NestEngineBase.ReportProgress(progress, new ProgressReport - { - Phase = NestPhase.Custom, - PlateNumber = plateNumber, - Parts = allParts, - WorkArea = workArea, - Description = desc, - }); + NestEngineBase.ReportProgress( + progress, + new ProgressReport + { + Phase = NestPhase.Custom, + PlateNumber = plateNumber, + Parts = allParts, + WorkArea = workArea, + Description = desc, + } + ); } /// @@ -94,8 +107,14 @@ namespace OpenNest.Engine.Fill /// that fits roughly the target count. Scales the shrink axis proportionally /// from the full-area count down to the target, with margin. /// - internal static Box EstimateStartBox(NestItem item, Box box, - double spacing, ShrinkAxis axis, int targetCount, double marginFactor = 1.3) + internal static Box EstimateStartBox( + NestItem item, + Box box, + double spacing, + ShrinkAxis axis, + int targetCount, + double marginFactor = 1.3 + ) { var bbox = item.Drawing.Program.BoundingBox(); if (bbox.Width <= 0 || bbox.Length <= 0) @@ -105,7 +124,10 @@ namespace OpenNest.Engine.Fill // Use FillBestFit for a fast, accurate rectangle count on the full box. var bin = new Bin { Size = new Size(box.Width, box.Length) }; - var packItem = new Item { Size = new Size(bbox.Width + spacing, bbox.Length + spacing) }; + var packItem = new Item + { + Size = new Size(bbox.Width + spacing, bbox.Length + spacing), + }; var packer = new FillBestFit(bin); packer.Fill(packItem); var fullCount = bin.Items.Count; @@ -130,9 +152,7 @@ namespace OpenNest.Engine.Fill { var placedBox = parts.Cast().GetBoundingBox(); - return axis == ShrinkAxis.Width - ? placedBox.Right - box.X - : placedBox.Top - box.Y; + return axis == ShrinkAxis.Width ? placedBox.Right - box.X : placedBox.Top - box.Y; } /// diff --git a/OpenNest.Engine/Fill/StripeFiller.cs b/OpenNest.Engine/Fill/StripeFiller.cs index 1e1fdc6..912e945 100644 --- a/OpenNest.Engine/Fill/StripeFiller.cs +++ b/OpenNest.Engine/Fill/StripeFiller.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using OpenNest.Engine; @@ -7,7 +8,6 @@ using OpenNest.Engine.BestFit; using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; -using System.Diagnostics; namespace OpenNest.Engine.Fill; @@ -31,8 +31,8 @@ public class StripeFiller /// Factory to create the engine used for filling the remnant strip. /// Defaults to NestEngineRegistry.Create (uses the user's selected engine). /// - public Func CreateRemnantEngine { get; set; } - = NestEngineRegistry.Create; + public Func CreateRemnantEngine { get; set; } = + NestEngineRegistry.Create; public StripeFiller(FillContext context, NestDirection primaryAxis) { @@ -64,15 +64,27 @@ public class StripeFiller foreach (var axis in new[] { NestDirection.Horizontal, NestDirection.Vertical }) { - var perpAxis = axis == NestDirection.Horizontal - ? NestDirection.Vertical : NestDirection.Horizontal; + var perpAxis = + axis == NestDirection.Horizontal + ? NestDirection.Vertical + : NestDirection.Horizontal; var sheetSpan = GetDimension(workArea, axis); var dirLabel = axis == NestDirection.Horizontal ? "Row" : "Col"; var expandResult = ConvergeStripeAngle( - pairParts, sheetSpan, spacing, axis, _context.Token); + pairParts, + sheetSpan, + spacing, + axis, + _context.Token + ); var shrinkResult = ConvergeStripeAngleShrink( - pairParts, sheetSpan, spacing, axis, _context.Token); + pairParts, + sheetSpan, + spacing, + axis, + _context.Token + ); foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult }) { @@ -84,9 +96,11 @@ public class StripeFiller if (result == null || result.Count == 0) continue; - Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " + - $"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " + - $"grid={result.Count} parts"); + Debug.WriteLine( + $"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " + + $"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " + + $"grid={result.Count} parts" + ); if (_comparer.IsBetter(result, bestParts, workArea)) { @@ -95,15 +109,21 @@ public class StripeFiller } } - _context.ReportProgress(bestParts, - $"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts"); + _context.ReportProgress( + bestParts, + $"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts" + ); } return bestParts ?? new List(); } - private List BuildGrid(List pairParts, double angle, - NestDirection primaryAxis, NestDirection perpAxis) + private List BuildGrid( + List pairParts, + double angle, + NestDirection primaryAxis, + NestDirection perpAxis + ) { var workArea = _context.WorkArea; var spacing = _context.Plate.PartSpacing; @@ -123,8 +143,10 @@ public class StripeFiller var partsPerStripe = stripeParts.Count; - Debug.WriteLine($"[StripeFiller] Stripe: {partsPerStripe} parts, " + - $"box={stripeBox.Width:F2}x{stripeBox.Length:F2}"); + Debug.WriteLine( + $"[StripeFiller] Stripe: {partsPerStripe} parts, " + + $"box={stripeBox.Width:F2}x{stripeBox.Length:F2}" + ); var stripePattern = new Pattern(); stripePattern.Parts.AddRange(stripeParts); @@ -141,8 +163,10 @@ public class StripeFiller var completeCount = gridParts.Count / partsPerStripe * partsPerStripe; if (completeCount < gridParts.Count) { - Debug.WriteLine($"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} " + - $"(dropped {gridParts.Count - completeCount} partial)"); + Debug.WriteLine( + $"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} " + + $"(dropped {gridParts.Count - completeCount} partial)" + ); gridParts = gridParts.GetRange(0, completeCount); } } @@ -184,12 +208,10 @@ public class StripeFiller _context.Item.Drawing, _context.Plate.Size.Length, _context.Plate.Size.Width, - _context.Plate.PartSpacing); + _context.Plate.PartSpacing + ); - return bestFits - .Where(r => r.Keep) - .Take(MaxPairCandidates) - .ToList(); + return bestFits.Where(r => r.Keep).Take(MaxPairCandidates).ToList(); } private static Box MakeStripeBox(Box workArea, double perpDim, NestDirection primaryAxis) @@ -208,7 +230,8 @@ public class StripeFiller var gridBox = gridParts.GetBoundingBox(); var minDim = System.Math.Min( drawing.Program.BoundingBox().Width, - drawing.Program.BoundingBox().Length); + drawing.Program.BoundingBox().Length + ); Box remnantBox; @@ -229,7 +252,9 @@ public class StripeFiller remnantBox = new Box(remnantX, workArea.Y, remnantWidth, workArea.Width); } - Debug.WriteLine($"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}"); + Debug.WriteLine( + $"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}" + ); var cachedResult = FillResultCache.Get(drawing, remnantBox, spacing); if (cachedResult != null) @@ -246,7 +271,10 @@ public class StripeFiller _context.Token.ThrowIfCancellationRequested(); var result = FillHelpers.FillWithDirectionPreference( dir => filler.Fill(drawing, angle, dir), - null, _comparer, remnantBox); + null, + _comparer, + remnantBox + ); if (result != null && result.Count > (best?.Count ?? 0)) best = result; @@ -264,7 +292,10 @@ public class StripeFiller } public static double FindAngleForTargetSpan( - List patternParts, double targetSpan, NestDirection axis) + List patternParts, + double targetSpan, + NestDirection axis + ) { var bestAngle = 0.0; var bestDiff = double.MaxValue; @@ -292,8 +323,7 @@ public class StripeFiller var (a1, s1) = samples[i]; var (a2, s2) = samples[i + 1]; - if ((s1 <= targetSpan && targetSpan <= s2) || - (s2 <= targetSpan && targetSpan <= s1)) + if ((s1 <= targetSpan && targetSpan <= s2) || (s2 <= targetSpan && targetSpan <= s1)) { var result = BisectForTarget(patternParts, a1, a2, targetSpan, axis); var resultSpan = GetRotatedSpan(patternParts, result, axis); @@ -332,8 +362,12 @@ public class StripeFiller /// Returns (angle, waste, pairCount). /// public static (double Angle, double Waste, int Count) ConvergeStripeAngle( - List patternParts, double sheetSpan, double spacing, - NestDirection axis, CancellationToken token = default) + List patternParts, + double sheetSpan, + double spacing, + NestDirection axis, + CancellationToken token = default + ) { var startAngle = OrientShortSideAlong(patternParts, axis); return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token); @@ -344,8 +378,12 @@ public class StripeFiller /// Complements ConvergeStripeAngle which only expands. /// public static (double Angle, double Waste, int Count) ConvergeStripeAngleShrink( - List patternParts, double sheetSpan, double spacing, - NestDirection axis, CancellationToken token = default) + List patternParts, + double sheetSpan, + double spacing, + NestDirection axis, + CancellationToken token = default + ) { var baseAngle = OrientShortSideAlong(patternParts, axis); var naturalPattern = FillHelpers.BuildRotatedPattern(patternParts, baseAngle); @@ -366,8 +404,13 @@ public class StripeFiller } private static (double Angle, double Waste, int Count) ConvergeFromAngle( - List patternParts, double startAngle, double sheetSpan, - double spacing, NestDirection axis, CancellationToken token) + List patternParts, + double startAngle, + double sheetSpan, + double spacing, + NestDirection axis, + CancellationToken token + ) { var bestWaste = double.MaxValue; var bestAngle = startAngle; @@ -381,15 +424,18 @@ public class StripeFiller var rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle); var pairSpan = GetDimension(rotated.BoundingBox, axis); - var perpDim = axis == NestDirection.Horizontal - ? rotated.BoundingBox.Width : rotated.BoundingBox.Length; + var perpDim = + axis == NestDirection.Horizontal + ? rotated.BoundingBox.Width + : rotated.BoundingBox.Length; if (pairSpan + spacing <= 0) break; - var stripeBox = axis == NestDirection.Horizontal - ? new Box(0, 0, sheetSpan, perpDim) - : new Box(0, 0, perpDim, sheetSpan); + var stripeBox = + axis == NestDirection.Horizontal + ? new Box(0, 0, sheetSpan, perpDim) + : new Box(0, 0, perpDim, sheetSpan); var engine = new FillLinear(stripeBox, spacing) { Label = "Stripe-EstimateRow" }; var filled = engine.Fill(rotated, axis); var n = filled?.Count ?? 0; @@ -400,8 +446,10 @@ public class StripeFiller var filledBox = ((IEnumerable)filled).GetBoundingBox(); var remaining = sheetSpan - GetDimension(filledBox, axis); - Debug.WriteLine($"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, " + - $"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}"); + Debug.WriteLine( + $"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, " + + $"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}" + ); if (remaining < bestWaste) { @@ -414,7 +462,8 @@ public class StripeFiller break; var bboxN = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing)); - if (bboxN <= 0) bboxN = 1; + if (bboxN <= 0) + bboxN = 1; var delta = remaining / bboxN; var targetSpan = pairSpan + delta; @@ -429,8 +478,12 @@ public class StripeFiller } private static double BisectForTarget( - List patternParts, double lo, double hi, - double targetSpan, NestDirection axis) + List patternParts, + double lo, + double hi, + double targetSpan, + NestDirection axis + ) { var bestAngle = lo; var bestDiff = double.MaxValue; @@ -451,8 +504,10 @@ public class StripeFiller break; var loSpan = GetRotatedSpan(patternParts, lo, axis); - if ((loSpan < targetSpan && span < targetSpan) || - (loSpan > targetSpan && span > targetSpan)) + if ( + (loSpan < targetSpan && span < targetSpan) + || (loSpan > targetSpan && span > targetSpan) + ) lo = mid; else hi = mid; @@ -461,8 +516,7 @@ public class StripeFiller return bestAngle; } - private static double GetRotatedSpan( - List patternParts, double angle, NestDirection axis) + private static double GetRotatedSpan(List patternParts, double angle, NestDirection axis) { var rotated = FillHelpers.BuildRotatedPattern(patternParts, angle); return axis == NestDirection.Horizontal diff --git a/OpenNest.Engine/Fill/VerticalRemnantComparer.cs b/OpenNest.Engine/Fill/VerticalRemnantComparer.cs index a96f8ce..534b918 100644 --- a/OpenNest.Engine/Fill/VerticalRemnantComparer.cs +++ b/OpenNest.Engine/Fill/VerticalRemnantComparer.cs @@ -28,7 +28,7 @@ namespace OpenNest.Engine.Fill return candExtent < currExtent; return FillScore.Compute(candidate, workArea).Density - > FillScore.Compute(current, workArea).Density; + > FillScore.Compute(current, workArea).Density; } private static double XExtent(List parts) @@ -39,8 +39,10 @@ namespace OpenNest.Engine.Fill foreach (var part in parts) { var bb = part.BoundingBox; - if (bb.Left < minX) minX = bb.Left; - if (bb.Right > maxX) maxX = bb.Right; + if (bb.Left < minX) + minX = bb.Left; + if (bb.Right > maxX) + maxX = bb.Right; } return maxX - minX; diff --git a/OpenNest.Engine/HorizontalRemnantEngine.cs b/OpenNest.Engine/HorizontalRemnantEngine.cs index 16b31eb..7045882 100644 --- a/OpenNest.Engine/HorizontalRemnantEngine.cs +++ b/OpenNest.Engine/HorizontalRemnantEngine.cs @@ -14,7 +14,8 @@ namespace OpenNest /// public class HorizontalRemnantEngine : DefaultNestEngine { - public HorizontalRemnantEngine(Plate plate) : base(plate) { } + public HorizontalRemnantEngine(Plate plate) + : base(plate) { } public override string Name => "Horizontal Remnant"; @@ -26,9 +27,17 @@ namespace OpenNest public override ShrinkAxis TrimAxis => ShrinkAxis.Length; - public override List BuildAngles(NestItem item, ClassificationResult classification, Box workArea) + public override List BuildAngles( + NestItem item, + ClassificationResult classification, + Box workArea + ) { - var baseAngles = new List { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI }; + var baseAngles = new List + { + classification.PrimaryAngle, + classification.PrimaryAngle + Angle.HalfPI, + }; baseAngles.Sort((a, b) => RotatedHeight(item, a).CompareTo(RotatedHeight(item, b))); return baseAngles; } diff --git a/OpenNest.Engine/IFillComparer.cs b/OpenNest.Engine/IFillComparer.cs index 5d38294..62a6f0f 100644 --- a/OpenNest.Engine/IFillComparer.cs +++ b/OpenNest.Engine/IFillComparer.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine { diff --git a/OpenNest.Engine/Jobs/Adapters/DrawingJobMapper.cs b/OpenNest.Engine/Jobs/Adapters/DrawingJobMapper.cs index c02185f..9e9f1b8 100644 --- a/OpenNest.Engine/Jobs/Adapters/DrawingJobMapper.cs +++ b/OpenNest.Engine/Jobs/Adapters/DrawingJobMapper.cs @@ -10,24 +10,46 @@ public static class DrawingJobMapper { ArgumentNullException.ThrowIfNull(drawing); var constraints = drawing.Constraints; - return new NestJobPart(partId, PartGeometrySnapshot.FromProgram(drawing.Program), quantity, drawing.Priority, - constraints == null ? RotationPolicy.Automatic : - RotationPolicy.FromLegacy(constraints.StepAngle, constraints.StartAngle, constraints.EndAngle)); + return new NestJobPart( + partId, + PartGeometrySnapshot.FromProgram(drawing.Program), + quantity, + drawing.Priority, + constraints == null + ? RotationPolicy.Automatic + : RotationPolicy.FromLegacy( + constraints.StepAngle, + constraints.StartAngle, + constraints.EndAngle + ) + ); } public static NestJobPart FromItem(string partId, NestItem item) { ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(item.Drawing); - return new NestJobPart(partId, PartGeometrySnapshot.FromProgram(item.Drawing.Program), item.Quantity, - item.Priority, RotationPolicy.FromLegacy(item.StepAngle, item.RotationStart, item.RotationEnd)); + return new NestJobPart( + partId, + PartGeometrySnapshot.FromProgram(item.Drawing.Program), + item.Quantity, + item.Priority, + RotationPolicy.FromLegacy(item.StepAngle, item.RotationStart, item.RotationEnd) + ); } /// Available stock is explicit; the legacy plate repeat count is not inventory. public static NestPlateStock FromPlate(string stockId, Plate plate, int? quantity) { ArgumentNullException.ThrowIfNull(plate); - return new NestPlateStock(stockId, plate.Size, quantity, plate.PartSpacing, plate.EdgeSpacing, plate.Quadrant); + return new NestPlateStock( + stockId, + plate.Size, + quantity, + plate.PartSpacing, + plate.EdgeSpacing, + plate.Quadrant + ); } public static Program ToProgram(PartGeometrySnapshot geometry) @@ -40,9 +62,17 @@ public static class DrawingJobMapper { CodeType.RapidMove => (Motion)new RapidMove(motion.X, motion.Y), CodeType.LinearMove => new LinearMove(motion.X, motion.Y) { Layer = motion.Layer }, - CodeType.ArcMove => new ArcMove(motion.X, motion.Y, motion.CenterX, motion.CenterY, motion.Rotation) - { Layer = motion.Layer }, - _ => throw new NotSupportedException("Unsupported snapshot motion.") + CodeType.ArcMove => new ArcMove( + motion.X, + motion.Y, + motion.CenterX, + motion.CenterY, + motion.Rotation + ) + { + Layer = motion.Layer, + }, + _ => throw new NotSupportedException("Unsupported snapshot motion."), }; code.Suppressed = motion.Suppressed; program.Codes.Add(code); @@ -58,20 +88,21 @@ public static class DrawingJobMapper { StepAngle = LegacyStep(part.Rotation), StartAngle = part.Rotation.Start, - EndAngle = part.Rotation.End + EndAngle = part.Rotation.End, }; return drawing; } // A fixed angle needs a nonzero legacy step so it is not misread as automatic. - internal static double LegacyStep(RotationPolicy policy) => policy.Kind == RotationPolicyKind.Fixed - ? OpenNest.Math.Angle.TwoPI : policy.Step; + internal static double LegacyStep(RotationPolicy policy) => + policy.Kind == RotationPolicyKind.Fixed ? OpenNest.Math.Angle.TwoPI : policy.Step; - internal static Plate CreatePlate(NestPlateStock stock) => new(stock.Size) - { - Quantity = 1, - PartSpacing = stock.PartSpacing, - EdgeSpacing = stock.EdgeSpacing, - Quadrant = stock.Quadrant - }; + internal static Plate CreatePlate(NestPlateStock stock) => + new(stock.Size) + { + Quantity = 1, + PartSpacing = stock.PartSpacing, + EdgeSpacing = stock.EdgeSpacing, + Quadrant = stock.Quadrant, + }; } diff --git a/OpenNest.Engine/Jobs/Adapters/LegacyPlateNesterAdapter.cs b/OpenNest.Engine/Jobs/Adapters/LegacyPlateNesterAdapter.cs index d1f782e..45a8c88 100644 --- a/OpenNest.Engine/Jobs/Adapters/LegacyPlateNesterAdapter.cs +++ b/OpenNest.Engine/Jobs/Adapters/LegacyPlateNesterAdapter.cs @@ -23,8 +23,11 @@ public sealed class LegacyPlateNesterAdapter : IPlateNester /// process-global NestEngineRegistry. public static IPlateNester Create(string strategy) => PlateNesterFactory.Create(strategy); - public PlateCandidate Place(PlatePlacementRequest request, IProgress progress = null, - CancellationToken token = default) + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress progress = null, + CancellationToken token = default + ) { ArgumentNullException.ThrowIfNull(request); token.ThrowIfCancellationRequested(); @@ -35,37 +38,50 @@ public sealed class LegacyPlateNesterAdapter : IPlateNester { var drawing = DrawingJobMapper.CreateDrawing(requirement); identities.Add(drawing, requirement.Id); - items.Add(new NestItem - { - Drawing = drawing, - Quantity = requirement.Quantity, - Priority = requirement.Priority, - StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation), - RotationStart = requirement.Rotation.Start, - RotationEnd = requirement.Rotation.End - }); + items.Add( + new NestItem + { + Drawing = drawing, + Quantity = requirement.Quantity, + Priority = requirement.Priority, + StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation), + RotationStart = requirement.Rotation.Start, + RotationEnd = requirement.Rotation.End, + } + ); } - var engine = engineFactory(plate) ?? throw new InvalidOperationException("Legacy engine factory returned null."); - var legacyProgress = progress == null ? null : new LegacyProgress(progress, request.Stock.Id); + var engine = + engineFactory(plate) + ?? throw new InvalidOperationException("Legacy engine factory returned null."); + var legacyProgress = + progress == null ? null : new LegacyProgress(progress, request.Stock.Id); var parts = engine.Nest(items, legacyProgress, token); token.ThrowIfCancellationRequested(); - if (parts == null) throw new InvalidOperationException("Legacy engine returned null placements."); + if (parts == null) + throw new InvalidOperationException("Legacy engine returned null placements."); var placements = new List(); foreach (var part in parts) { if (part?.BaseDrawing == null || !identities.TryGetValue(part.BaseDrawing, out var id)) - throw new InvalidOperationException("Legacy placement does not reference a private requirement drawing."); - placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation)); + throw new InvalidOperationException( + "Legacy placement does not reference a private requirement drawing." + ); + placements.Add( + new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation) + ); } return new PlateCandidate(placements); } - private sealed class LegacyProgress(IProgress progress, string stockId) : IProgress + private sealed class LegacyProgress(IProgress progress, string stockId) + : IProgress { public void Report(NestProgress value) { ArgumentNullException.ThrowIfNull(value); - progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value)); + progress.Report( + new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value) + ); } } } diff --git a/OpenNest.Engine/Jobs/Adapters/NestResultMaterializer.cs b/OpenNest.Engine/Jobs/Adapters/NestResultMaterializer.cs index a4c16d4..c632f84 100644 --- a/OpenNest.Engine/Jobs/Adapters/NestResultMaterializer.cs +++ b/OpenNest.Engine/Jobs/Adapters/NestResultMaterializer.cs @@ -27,15 +27,23 @@ public static class NestResultMaterializer ArgumentNullException.ThrowIfNull(job); ArgumentNullException.ThrowIfNull(result); var nest = new Nest(); - var drawings = job.Parts.ToDictionary(p => p.Id, DrawingJobMapper.CreateDrawing, StringComparer.Ordinal); - foreach (var drawing in drawings.Values) nest.Drawings.Add(drawing); + var drawings = job.Parts.ToDictionary( + p => p.Id, + DrawingJobMapper.CreateDrawing, + StringComparer.Ordinal + ); + foreach (var drawing in drawings.Values) + nest.Drawings.Add(drawing); foreach (var sheet in result.Plates) { var plate = DrawingJobMapper.CreatePlate(sheet.Stock); foreach (var pose in sheet.Placements) { if (!drawings.TryGetValue(pose.PartId, out var drawing)) - throw new ArgumentException("Result contains a requirement not present in the job.", nameof(result)); + throw new ArgumentException( + "Result contains a requirement not present in the job.", + nameof(result) + ); // Do not use CreateAtOrigin: it normalizes bounds and would change the snapshot frame. var part = new Part(drawing); part.Rotate(pose.Rotation); diff --git a/OpenNest.Engine/Jobs/CandidateProgressBridge.cs b/OpenNest.Engine/Jobs/CandidateProgressBridge.cs index 9dd2c8d..02cd577 100644 --- a/OpenNest.Engine/Jobs/CandidateProgressBridge.cs +++ b/OpenNest.Engine/Jobs/CandidateProgressBridge.cs @@ -9,18 +9,25 @@ namespace OpenNest; /// internal static class CandidateProgressBridge { - internal static IProgress Create(IProgress progress, string stockId) + internal static IProgress Create( + IProgress progress, + string stockId + ) { - if (progress == null) return null; + if (progress == null) + return null; return new LegacyToJob(progress, stockId); } - private sealed class LegacyToJob(IProgress progress, string stockId) : IProgress + private sealed class LegacyToJob(IProgress progress, string stockId) + : IProgress { public void Report(NestProgress value) { ArgumentNullException.ThrowIfNull(value); - progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value)); + progress.Report( + new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, -1, 0, 0, value) + ); } } } diff --git a/OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs b/OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs index adcaca4..8ec65a9 100644 --- a/OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs +++ b/OpenNest.Engine/Jobs/FixedStrategyNestingEngine.cs @@ -20,10 +20,18 @@ public sealed class FixedStrategyNestingEngine : INestingEngine this.strategy = strategy; } - public NestJobResult Solve(NestJob job, IProgress progress = null, CancellationToken token = default) + public NestJobResult Solve( + NestJob job, + IProgress progress = null, + CancellationToken token = default + ) { ArgumentNullException.ThrowIfNull(job); - var forced = new NestJob(job.Parts, job.Plates, new NestJobOptions(strategy, job.Options.MaxPlates)); + var forced = new NestJob( + job.Parts, + job.Plates, + new NestJobOptions(strategy, job.Options.MaxPlates) + ); return runner.Solve(forced, progress, token); } } diff --git a/OpenNest.Engine/Jobs/INestingEngine.cs b/OpenNest.Engine/Jobs/INestingEngine.cs index aea5678..4867e85 100644 --- a/OpenNest.Engine/Jobs/INestingEngine.cs +++ b/OpenNest.Engine/Jobs/INestingEngine.cs @@ -6,5 +6,9 @@ namespace OpenNest; /// Synchronous whole-job solver. Cancellation throws, rather than returning partial success. public interface INestingEngine { - NestJobResult Solve(NestJob job, IProgress progress = null, CancellationToken token = default); + NestJobResult Solve( + NestJob job, + IProgress progress = null, + CancellationToken token = default + ); } diff --git a/OpenNest.Engine/Jobs/IPlateNester.cs b/OpenNest.Engine/Jobs/IPlateNester.cs index 010a5a1..29453fa 100644 --- a/OpenNest.Engine/Jobs/IPlateNester.cs +++ b/OpenNest.Engine/Jobs/IPlateNester.cs @@ -6,6 +6,9 @@ namespace OpenNest; /// Places on one sheet only. Must not change stock, demand, or caller-owned domain objects. public interface IPlateNester { - PlateCandidate Place(PlatePlacementRequest request, IProgress progress = null, - CancellationToken token = default); + PlateCandidate Place( + PlatePlacementRequest request, + IProgress progress = null, + CancellationToken token = default + ); } diff --git a/OpenNest.Engine/Jobs/NestJob.cs b/OpenNest.Engine/Jobs/NestJob.cs index bed9d6b..9436eb3 100644 --- a/OpenNest.Engine/Jobs/NestJob.cs +++ b/OpenNest.Engine/Jobs/NestJob.cs @@ -7,13 +7,19 @@ namespace OpenNest; /// One material/unit system's requirements. Collections are copied; all nested values are immutable. public sealed class NestJob { - public NestJob(IEnumerable parts, IEnumerable plates, NestJobOptions options = null) + public NestJob( + IEnumerable parts, + IEnumerable plates, + NestJobOptions options = null + ) { Parts = Own(parts); Plates = Own(plates); Options = options ?? new NestJobOptions(); - if (Parts.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Parts.Count || - Plates.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Plates.Count) + if ( + Parts.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Parts.Count + || Plates.Select(p => p.Id).Distinct(StringComparer.Ordinal).Count() != Plates.Count + ) throw new ArgumentException("Part and stock IDs must each be unique."); } diff --git a/OpenNest.Engine/Jobs/NestJobCandidateComparer.cs b/OpenNest.Engine/Jobs/NestJobCandidateComparer.cs index 025764e..06907c9 100644 --- a/OpenNest.Engine/Jobs/NestJobCandidateComparer.cs +++ b/OpenNest.Engine/Jobs/NestJobCandidateComparer.cs @@ -15,34 +15,49 @@ public sealed class NestJobCandidateComparer } /// Returns positive when the left trial is preferred. - public int Compare(PlateCandidate left, NestPlateStock leftStock, int leftIndex, - PlateCandidate right, NestPlateStock rightStock, int rightIndex) + public int Compare( + PlateCandidate left, + NestPlateStock leftStock, + int leftIndex, + PlateCandidate right, + NestPlateStock rightStock, + int rightIndex + ) { - var priorities = parts.Select(part => part.Priority).Distinct().OrderBy(priority => priority); + var priorities = parts + .Select(part => part.Priority) + .Distinct() + .OrderBy(priority => priority); foreach (var priority in priorities) { var leftCount = Count(left, priority); var rightCount = Count(right, priority); - if (leftCount != rightCount) return leftCount.CompareTo(rightCount); + if (leftCount != rightCount) + return leftCount.CompareTo(rightCount); } var area = Area(rightStock).CompareTo(Area(leftStock)); - if (area != 0) return area; + if (area != 0) + return area; var envelope = Envelope(right).CompareTo(Envelope(left)); - if (envelope != 0) return envelope; + if (envelope != 0) + return envelope; return rightIndex.CompareTo(leftIndex); } - private int Count(PlateCandidate candidate, int priority) => candidate.Placements.Count(placement => - parts.First(part => part.Id == placement.PartId).Priority == priority); + private int Count(PlateCandidate candidate, int priority) => + candidate.Placements.Count(placement => + parts.First(part => part.Id == placement.PartId).Priority == priority + ); private static double Area(NestPlateStock stock) => stock.Size.Width * stock.Size.Length; private static double Envelope(PlateCandidate candidate) { - if (candidate.Placements.Count == 0) return 0; + if (candidate.Placements.Count == 0) + return 0; var xs = candidate.Placements.Select(placement => placement.X); var ys = candidate.Placements.Select(placement => placement.Y); return (xs.Max() - xs.Min()) * (ys.Max() - ys.Min()); diff --git a/OpenNest.Engine/Jobs/NestJobOptions.cs b/OpenNest.Engine/Jobs/NestJobOptions.cs index b9449c4..8b6396c 100644 --- a/OpenNest.Engine/Jobs/NestJobOptions.cs +++ b/OpenNest.Engine/Jobs/NestJobOptions.cs @@ -5,11 +5,16 @@ namespace OpenNest; /// Immutable per-job options; selection never changes the legacy global registry. public sealed class NestJobOptions { - public NestJobOptions(string placementStrategy = "Default", int? maxPlates = null, - double salvageRate = 0, double minimumSalvageDimension = 0) + public NestJobOptions( + string placementStrategy = "Default", + int? maxPlates = null, + double salvageRate = 0, + double minimumSalvageDimension = 0 + ) { ArgumentException.ThrowIfNullOrWhiteSpace(placementStrategy); - if (maxPlates <= 0) throw new ArgumentOutOfRangeException(nameof(maxPlates)); + if (maxPlates <= 0) + throw new ArgumentOutOfRangeException(nameof(maxPlates)); if (!double.IsFinite(salvageRate) || salvageRate < 0 || salvageRate > 1) throw new ArgumentOutOfRangeException(nameof(salvageRate)); if (!double.IsFinite(minimumSalvageDimension) || minimumSalvageDimension < 0) @@ -22,11 +27,13 @@ public sealed class NestJobOptions /// Fraction of eligible edge-offcut area credited by StockLadder (0..1). public double SalvageRate { get; } + /// Both offcut dimensions must meet this caller-supplied minimum in job units. /// Zero disables credit; scraps and holes are never credited. public double MinimumSalvageDimension { get; } public string PlacementStrategy { get; } + /// Maximum physical sheets to commit, or null for no explicit cap. public int? MaxPlates { get; } } diff --git a/OpenNest.Engine/Jobs/NestJobPart.cs b/OpenNest.Engine/Jobs/NestJobPart.cs index f65bbf2..fceb42b 100644 --- a/OpenNest.Engine/Jobs/NestJobPart.cs +++ b/OpenNest.Engine/Jobs/NestJobPart.cs @@ -5,12 +5,18 @@ namespace OpenNest; /// An immutable requirement, independent of drawing names, UI state, and drawing quantity counters. public sealed class NestJobPart { - public NestJobPart(string id, PartGeometrySnapshot geometry, int quantity, int priority = 0, - RotationPolicy rotation = null) + public NestJobPart( + string id, + PartGeometrySnapshot geometry, + int quantity, + int priority = 0, + RotationPolicy rotation = null + ) { ArgumentException.ThrowIfNullOrWhiteSpace(id); ArgumentNullException.ThrowIfNull(geometry); - if (quantity <= 0) throw new ArgumentOutOfRangeException(nameof(quantity)); + if (quantity <= 0) + throw new ArgumentOutOfRangeException(nameof(quantity)); Id = id; Geometry = geometry; Quantity = quantity; @@ -20,6 +26,7 @@ public sealed class NestJobPart public string Id { get; } public PartGeometrySnapshot Geometry { get; } + /// Positive number requested; never decremented by placement code. public int Quantity { get; } public int Priority { get; } diff --git a/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs b/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs index d119b3b..2826bfd 100644 --- a/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs +++ b/OpenNest.Engine/Jobs/NestJobPlacementValidator.cs @@ -11,33 +11,54 @@ internal static class NestJobPlacementValidator { private const double Epsilon = 0.0000001; - internal static void ValidateCandidate(PlateCandidate candidate, NestPlateStock stock, - IReadOnlyDictionary remaining, IReadOnlyDictionary parts) + internal static void ValidateCandidate( + PlateCandidate candidate, + NestPlateStock stock, + IReadOnlyDictionary remaining, + IReadOnlyDictionary parts + ) { - if (candidate == null) throw new InvalidOperationException("The plate nester returned a null candidate."); + if (candidate == null) + throw new InvalidOperationException("The plate nester returned a null candidate."); var counts = new Dictionary(StringComparer.Ordinal); var placed = new List(); foreach (var placement in candidate.Placements) { - if (placement.PartId == null || !remaining.TryGetValue(placement.PartId, out var available) || - !parts.TryGetValue(placement.PartId, out var part)) - throw new InvalidOperationException("Candidate references an unknown requirement ID."); - if (!double.IsFinite(placement.X) || !double.IsFinite(placement.Y) || !double.IsFinite(placement.Rotation)) + if ( + placement.PartId == null + || !remaining.TryGetValue(placement.PartId, out var available) + || !parts.TryGetValue(placement.PartId, out var part) + ) + throw new InvalidOperationException( + "Candidate references an unknown requirement ID." + ); + if ( + !double.IsFinite(placement.X) + || !double.IsFinite(placement.Y) + || !double.IsFinite(placement.Rotation) + ) throw new InvalidOperationException("Candidate poses must be finite."); counts.TryGetValue(placement.PartId, out var count); - if (count >= available) throw new InvalidOperationException("Candidate overproduces a requirement."); + if (count >= available) + throw new InvalidOperationException("Candidate overproduces a requirement."); if (!RotationIsAllowed(part.Rotation, placement.Rotation)) - throw new InvalidOperationException("Candidate rotation is not allowed for the requirement."); + throw new InvalidOperationException( + "Candidate rotation is not allowed for the requirement." + ); var shape = Transform(CreateShape(part.Geometry), placement); if (!FitsWorkArea(shape, stock)) - throw new InvalidOperationException("Candidate placement falls outside the usable stock area."); + throw new InvalidOperationException( + "Candidate placement falls outside the usable stock area." + ); foreach (var other in placed) { if (Overlaps(shape, other)) throw new InvalidOperationException("Candidate placements overlap."); if (stock.PartSpacing > 0 && Distance(shape, other) < stock.PartSpacing - Epsilon) - throw new InvalidOperationException("Candidate placements violate required part spacing."); + throw new InvalidOperationException( + "Candidate placements violate required part spacing." + ); } placed.Add(shape); @@ -52,10 +73,12 @@ internal static class NestJobPlacementValidator private static bool RotationIsAllowed(RotationPolicy policy, double rotation) { - if (policy.Kind == RotationPolicyKind.Automatic) return true; + if (policy.Kind == RotationPolicyKind.Automatic) + return true; if (policy.Kind == RotationPolicyKind.Fixed) return AnglesEqual(rotation, policy.Start); - if (rotation < policy.Start - Epsilon || rotation > policy.End + Epsilon) return false; + if (rotation < policy.Start - Epsilon || rotation > policy.End + Epsilon) + return false; var steps = (rotation - policy.Start) / policy.Step; return System.Math.Abs(steps - System.Math.Round(steps)) <= Epsilon; } @@ -63,7 +86,8 @@ internal static class NestJobPlacementValidator private static bool AnglesEqual(double left, double right) { var delta = (left - right) % (System.Math.PI * 2); - return System.Math.Abs(delta) <= Epsilon || System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Epsilon; + return System.Math.Abs(delta) <= Epsilon + || System.Math.Abs(System.Math.Abs(delta) - System.Math.PI * 2) <= Epsilon; } private static ShapeTopology CreateShape(PartGeometrySnapshot geometry) @@ -75,7 +99,8 @@ internal static class NestJobPlacementValidator cutEntities.Add(entity); var contours = ShapeBuilder.GetShapes(cutEntities); - if (contours.Count == 0) throw new ArgumentException("Geometry must contain a closed contour."); + if (contours.Count == 0) + throw new ArgumentException("Geometry must contain a closed contour."); var closedEntities = new List(); var marks = new List(); foreach (var contour in contours) @@ -85,7 +110,8 @@ internal static class NestJobPlacementValidator ValidateContour(contour); closedEntities.AddRange(contour.Entities); } - else marks.Add(contour); + else + marks.Add(contour); } if (closedEntities.Count == 0) throw new ArgumentException("Geometry must contain a closed outer contour."); @@ -126,25 +152,32 @@ internal static class NestJobPlacementValidator for (var index = 0; index < parameters.Count; index++) { Check(PointAt(parameters[index])); - if (index > 0) Check(PointAt((parameters[index - 1] + parameters[index]) / 2)); + if (index > 0) + Check(PointAt((parameters[index - 1] + parameters[index]) / 2)); } void AddParameter(Vector point) { - if (!point.IsValid()) throw new ArgumentException("Indeterminate mark intersection."); + if (!point.IsValid()) + throw new ArgumentException("Indeterminate mark intersection."); var value = entity is Line line ? line.StartPoint.DistanceTo(point) / line.Length - : Angle.NormalizeRad(((Arc)entity).IsReversed - ? ((Arc)entity).StartAngle - ((Arc)entity).Center.AngleTo(point) - : ((Arc)entity).Center.AngleTo(point) - ((Arc)entity).StartAngle) / ((Arc)entity).SweepAngle(); - if (value >= 0 && value <= 1) parameters.Add(value); + : Angle.NormalizeRad( + ((Arc)entity).IsReversed + ? ((Arc)entity).StartAngle - ((Arc)entity).Center.AngleTo(point) + : ((Arc)entity).Center.AngleTo(point) - ((Arc)entity).StartAngle + ) / ((Arc)entity).SweepAngle(); + if (value >= 0 && value <= 1) + parameters.Add(value); } Vector PointAt(double value) { - if (entity is Line line) return line.StartPoint + (line.EndPoint - line.StartPoint) * value; + if (entity is Line line) + return line.StartPoint + (line.EndPoint - line.StartPoint) * value; var arc = (Arc)entity; var angle = arc.StartAngle + (arc.IsReversed ? -1 : 1) * arc.SweepAngle() * value; - return arc.Center + new Vector(System.Math.Cos(angle), System.Math.Sin(angle)) * arc.Radius; + return arc.Center + + new Vector(System.Math.Cos(angle), System.Math.Sin(angle)) * arc.Radius; } void Check(Vector point) { @@ -153,14 +186,20 @@ internal static class NestJobPlacementValidator // Exact analytic boundary contact is allowed; near-boundary uncertainty is not. var onBoundary = false; foreach (var edge in boundaries[index].Entities) - if (edge.ClosestPointTo(point).DistanceTo(point) <= Epsilon) onBoundary = true; - if (onBoundary) continue; + if (edge.ClosestPointTo(point).DistanceTo(point) <= Epsilon) + onBoundary = true; + if (onBoundary) + continue; foreach (var edge in polygons[index].ToLines()) if (edge.ClosestPointTo(point).DistanceTo(point) <= 2 * chordTolerance) - throw new ArgumentException("Internal mark is too close to a material boundary."); + throw new ArgumentException( + "Internal mark is too close to a material boundary." + ); var inside = StrictlyInside(polygons[index], point); if (index == 0 ? !inside : inside) - throw new ArgumentException("Open geometry leaves the closed material region."); + throw new ArgumentException( + "Open geometry leaves the closed material region." + ); } } } @@ -184,19 +223,25 @@ internal static class NestJobPlacementValidator Line line => line.StartPoint, Arc arc => arc.StartPoint(), Circle circle => circle.Center.Offset(circle.Radius, 0), - _ => throw new ArgumentException("Unsupported internal geometry.") + _ => throw new ArgumentException("Unsupported internal geometry."), }; if (!StrictlyInside(polygons[0], point)) - throw new ArgumentException("Open or disconnected geometry lies outside the closed perimeter."); + throw new ArgumentException( + "Open or disconnected geometry lies outside the closed perimeter." + ); for (var index = 0; index < boundaries.Count; index++) { if (index > 0 && polygons[index].ContainsPoint(point)) throw new ArgumentException("Internal geometry lies in a cutout."); foreach (var edge in polygons[index].ToLines()) if (edge.ClosestPointTo(point).DistanceTo(point) <= 2 * chordTolerance) - throw new ArgumentException("Internal geometry is too close to a material boundary."); + throw new ArgumentException( + "Internal geometry is too close to a material boundary." + ); if (entity.Intersects(boundaries[index])) - throw new ArgumentException("Internal geometry crosses or touches a material boundary."); + throw new ArgumentException( + "Internal geometry crosses or touches a material boundary." + ); } } } @@ -232,9 +277,11 @@ internal static class NestJobPlacementValidator private static bool FitsWorkArea(ShapeTopology shape, NestPlateStock stock) { var workArea = WorkArea(stock); - if (!FitsWorkArea(shape.Perimeter, workArea)) return false; + if (!FitsWorkArea(shape.Perimeter, workArea)) + return false; foreach (var cutout in shape.Cutouts) - if (!FitsWorkArea(cutout, workArea)) return false; + if (!FitsWorkArea(cutout, workArea)) + return false; return true; } @@ -242,16 +289,21 @@ internal static class NestJobPlacementValidator { var left = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length; var bottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width; - return new Box(left + stock.EdgeSpacing.Left, bottom + stock.EdgeSpacing.Bottom, + return new Box( + left + stock.EdgeSpacing.Left, + bottom + stock.EdgeSpacing.Bottom, stock.Size.Length - stock.EdgeSpacing.Left - stock.EdgeSpacing.Right, - stock.Size.Width - stock.EdgeSpacing.Bottom - stock.EdgeSpacing.Top); + stock.Size.Width - stock.EdgeSpacing.Bottom - stock.EdgeSpacing.Top + ); } private static bool FitsWorkArea(Shape contour, Box workArea) { var bounds = contour.BoundingBox; - return bounds.Left >= workArea.Left - Epsilon && bounds.Right <= workArea.Right + Epsilon && - bounds.Bottom >= workArea.Bottom - Epsilon && bounds.Top <= workArea.Top + Epsilon; + return bounds.Left >= workArea.Left - Epsilon + && bounds.Right <= workArea.Right + Epsilon + && bounds.Bottom >= workArea.Bottom - Epsilon + && bounds.Top <= workArea.Top + Epsilon; } private static bool Overlaps(ShapeTopology left, ShapeTopology right) @@ -265,7 +317,12 @@ internal static class NestJobPlacementValidator // Collision checks this by clipping triangulated polygons and rejecting zero-area // slivers, so it catches containment and small corner intersections that a witness // probe can miss, while contact stays legal; cutouts are subtracted from both sides. - return Collision.HasOverlap(leftPoly, rightPoly, ToPolygons(left.Cutouts), ToPolygons(right.Cutouts)); + return Collision.HasOverlap( + leftPoly, + rightPoly, + ToPolygons(left.Cutouts), + ToPolygons(right.Cutouts) + ); } /// @@ -274,13 +331,15 @@ internal static class NestJobPlacementValidator private static bool StrictlyInside(Polygon polygon, Vector point) { var n = polygon.IsClosed() ? polygon.Vertices.Count - 1 : polygon.Vertices.Count; - if (n < 3) return false; + if (n < 3) + return false; var winding = 0; for (var i = 0; i < n; i++) { var p1 = polygon.Vertices[i]; var p2 = polygon.Vertices[(i + 1) % n]; - if (OnSegment(p1, p2, point)) return false; + if (OnSegment(p1, p2, point)) + return false; if (p1.Y <= point.Y) { if (p2.Y > point.Y && IsLeft(p1, p2, point) > 0) @@ -297,9 +356,12 @@ internal static class NestJobPlacementValidator private static bool OnSegment(Vector a, Vector b, Vector p) { var cross = (b.X - a.X) * (p.Y - a.Y) - (b.Y - a.Y) * (p.X - a.X); - if (!cross.IsEqualTo(0.0)) return false; - return System.Math.Min(a.X, b.X) - Epsilon <= p.X && p.X <= System.Math.Max(a.X, b.X) + Epsilon && - System.Math.Min(a.Y, b.Y) - Epsilon <= p.Y && p.Y <= System.Math.Max(a.Y, b.Y) + Epsilon; + if (!cross.IsEqualTo(0.0)) + return false; + return System.Math.Min(a.X, b.X) - Epsilon <= p.X + && p.X <= System.Math.Max(a.X, b.X) + Epsilon + && System.Math.Min(a.Y, b.Y) - Epsilon <= p.Y + && p.Y <= System.Math.Max(a.Y, b.Y) + Epsilon; } private static double IsLeft(Vector p1, Vector p2, Vector p) => @@ -309,8 +371,11 @@ internal static class NestJobPlacementValidator { var result = double.PositiveInfinity; foreach (var leftContour in AllContours(left)) - foreach (var rightContour in AllContours(right)) - result = System.Math.Min(result, BoundaryDistance(ToPolygon(leftContour), ToPolygon(rightContour))); + foreach (var rightContour in AllContours(right)) + result = System.Math.Min( + result, + BoundaryDistance(ToPolygon(leftContour), ToPolygon(rightContour)) + ); return result; } @@ -343,11 +408,24 @@ internal static class NestJobPlacementValidator { foreach (var rightLine in right.ToLines()) { - if (leftLine.Intersects(rightLine)) return 0; - result = System.Math.Min(result, leftLine.ClosestPointTo(rightLine.StartPoint).DistanceTo(rightLine.StartPoint)); - result = System.Math.Min(result, leftLine.ClosestPointTo(rightLine.EndPoint).DistanceTo(rightLine.EndPoint)); - result = System.Math.Min(result, rightLine.ClosestPointTo(leftLine.StartPoint).DistanceTo(leftLine.StartPoint)); - result = System.Math.Min(result, rightLine.ClosestPointTo(leftLine.EndPoint).DistanceTo(leftLine.EndPoint)); + if (leftLine.Intersects(rightLine)) + return 0; + result = System.Math.Min( + result, + leftLine.ClosestPointTo(rightLine.StartPoint).DistanceTo(rightLine.StartPoint) + ); + result = System.Math.Min( + result, + leftLine.ClosestPointTo(rightLine.EndPoint).DistanceTo(rightLine.EndPoint) + ); + result = System.Math.Min( + result, + rightLine.ClosestPointTo(leftLine.StartPoint).DistanceTo(leftLine.StartPoint) + ); + result = System.Math.Min( + result, + rightLine.ClosestPointTo(leftLine.EndPoint).DistanceTo(leftLine.EndPoint) + ); } } return result; diff --git a/OpenNest.Engine/Jobs/NestJobProgress.cs b/OpenNest.Engine/Jobs/NestJobProgress.cs index a40ea6c..15a5c4e 100644 --- a/OpenNest.Engine/Jobs/NestJobProgress.cs +++ b/OpenNest.Engine/Jobs/NestJobProgress.cs @@ -1,10 +1,20 @@ namespace OpenNest; -public enum NestJobStage { EvaluatingCandidate, PlateCommitted } +public enum NestJobStage +{ + EvaluatingCandidate, + PlateCommitted, +} /// /// Whole-job progress. Counts change only after a physical sheet commits; LegacyProgress is optional /// non-authoritative detail from a plate nester while its candidate remains under evaluation. /// -public sealed record NestJobProgress(NestJobStage Stage, string StockId, int PlateIndex, - int CommittedPlates, int CommittedParts, NestProgress LegacyProgress = null); +public sealed record NestJobProgress( + NestJobStage Stage, + string StockId, + int PlateIndex, + int CommittedPlates, + int CommittedParts, + NestProgress LegacyProgress = null +); diff --git a/OpenNest.Engine/Jobs/NestJobResult.cs b/OpenNest.Engine/Jobs/NestJobResult.cs index 0f5225d..c0356bc 100644 --- a/OpenNest.Engine/Jobs/NestJobResult.cs +++ b/OpenNest.Engine/Jobs/NestJobResult.cs @@ -3,15 +3,32 @@ using System.Collections.Generic; namespace OpenNest; -public enum NestJobStatus { Complete, Incomplete } -public enum NestJobStopReason { Completed, StockExhausted, NoPlacementFound, PlateLimitReached } +public enum NestJobStatus +{ + Complete, + Incomplete, +} + +public enum NestJobStopReason +{ + Completed, + StockExhausted, + NoPlacementFound, + PlateLimitReached, +} /// /// Rotate about the snapshot origin, then translate by X/Y into the selected plate quadrant frame. /// Rotation is in radians. InstanceIndex is zero-based and unique within a part requirement across the job. /// The runner assigns final instance indices when committing a candidate. /// -public sealed record NestJobPlacement(string PartId, int InstanceIndex, double X, double Y, double Rotation); +public sealed record NestJobPlacement( + string PartId, + int InstanceIndex, + double X, + double Y, + double Rotation +); /// Requested = Placed + Unplaced for a requirement ID. public sealed record PartFulfillment(string PartId, int Requested, int Placed, int Unplaced); @@ -22,7 +39,11 @@ public sealed record StockUsage(string StockId, int Used, int? Remaining); /// One physical sheet, with owned ordered placements and immutable stock/settings snapshot. public sealed class NestJobPlateResult { - public NestJobPlateResult(int plateIndex, NestPlateStock stock, IEnumerable placements) + public NestJobPlateResult( + int plateIndex, + NestPlateStock stock, + IEnumerable placements + ) { ArgumentNullException.ThrowIfNull(stock); PlateIndex = plateIndex; @@ -39,9 +60,13 @@ public sealed class NestJobPlateResult /// Detached result values in commit/input order; no mutable Drawing, Plate, or NestItem escapes. public sealed class NestJobResult { - public NestJobResult(NestJobStatus status, NestJobStopReason stopReason, - IEnumerable plates, IEnumerable fulfillment, - IEnumerable stockUsage) + public NestJobResult( + NestJobStatus status, + NestJobStopReason stopReason, + IEnumerable plates, + IEnumerable fulfillment, + IEnumerable stockUsage + ) { Status = status; StopReason = stopReason; diff --git a/OpenNest.Engine/Jobs/NestJobRunner.cs b/OpenNest.Engine/Jobs/NestJobRunner.cs index f239fcf..7ea3483 100644 --- a/OpenNest.Engine/Jobs/NestJobRunner.cs +++ b/OpenNest.Engine/Jobs/NestJobRunner.cs @@ -20,20 +20,32 @@ public sealed class NestJobRunner : INestingEngine this.plateNesterFactory = plateNesterFactory; } - public NestJobResult Solve(NestJob job, IProgress progress = null, - CancellationToken token = default) + public NestJobResult Solve( + NestJob job, + IProgress progress = null, + CancellationToken token = default + ) { ArgumentNullException.ThrowIfNull(job); token.ThrowIfCancellationRequested(); NestJobValidator.Validate(job); var plates = new List(); - var remaining = job.Parts.ToDictionary(part => part.Id, part => part.Quantity, StringComparer.Ordinal); + var remaining = job.Parts.ToDictionary( + part => part.Id, + part => part.Quantity, + StringComparer.Ordinal + ); var placed = job.Parts.ToDictionary(part => part.Id, _ => 0, StringComparer.Ordinal); var parts = job.Parts.ToDictionary(part => part.Id, StringComparer.Ordinal); var used = job.Plates.ToDictionary(stock => stock.Id, _ => 0, StringComparer.Ordinal); var comparer = new NestJobCandidateComparer(job.Parts); - var nester = job.Parts.Count == 0 ? null : plateNesterFactory(job.Options.PlacementStrategy) ?? - throw new NotSupportedException($"Unknown placement strategy: {job.Options.PlacementStrategy}."); + var nester = + job.Parts.Count == 0 + ? null + : plateNesterFactory(job.Options.PlacementStrategy) + ?? throw new NotSupportedException( + $"Unknown placement strategy: {job.Options.PlacementStrategy}." + ); var reason = NestJobStopReason.Completed; while (remaining.Values.Any(count => count > 0)) { @@ -49,21 +61,55 @@ public sealed class NestJobRunner : INestingEngine for (var index = 0; index < job.Plates.Count; index++) { var stock = job.Plates[index]; - if (stock.Quantity is int quantity && used[stock.Id] >= quantity) continue; + if (stock.Quantity is int quantity && used[stock.Id] >= quantity) + continue; hasAvailableStock = true; - var request = new PlatePlacementRequest(stock, job.Parts.Where(part => remaining[part.Id] > 0) - .Select(part => new NestJobPart(part.Id, part.Geometry, remaining[part.Id], part.Priority, part.Rotation))); - progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id, - plates.Count, plates.Count, placed.Values.Sum())); + var request = new PlatePlacementRequest( + stock, + job.Parts.Where(part => remaining[part.Id] > 0) + .Select(part => new NestJobPart( + part.Id, + part.Geometry, + remaining[part.Id], + part.Priority, + part.Rotation + )) + ); + progress?.Report( + new NestJobProgress( + NestJobStage.EvaluatingCandidate, + stock.Id, + plates.Count, + plates.Count, + placed.Values.Sum() + ) + ); token.ThrowIfCancellationRequested(); - var candidateProgress = progress == null ? null : new CandidateProgress(progress, stock.Id, - plates.Count, plates.Count, placed.Values.Sum()); + var candidateProgress = + progress == null + ? null + : new CandidateProgress( + progress, + stock.Id, + plates.Count, + plates.Count, + placed.Values.Sum() + ); var candidate = nester.Place(request, candidateProgress, token); token.ThrowIfCancellationRequested(); NestJobValidator.ValidateCandidate(candidate, stock, remaining, parts); var trial = new CandidateTrial(candidate, stock, index); - if (winner == null || comparer.Compare(trial.Candidate, trial.Stock, trial.StockIndex, - winner.Candidate, winner.Stock, winner.StockIndex) > 0) + if ( + winner == null + || comparer.Compare( + trial.Candidate, + trial.Stock, + trial.StockIndex, + winner.Candidate, + winner.Stock, + winner.StockIndex + ) > 0 + ) winner = trial; } @@ -86,27 +132,65 @@ public sealed class NestJobRunner : INestingEngine } used[winner.Stock.Id]++; plates.Add(new NestJobPlateResult(plates.Count, winner.Stock, committed)); - progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, winner.Stock.Id, - plates.Count - 1, plates.Count, placed.Values.Sum())); + progress?.Report( + new NestJobProgress( + NestJobStage.PlateCommitted, + winner.Stock.Id, + plates.Count - 1, + plates.Count, + placed.Values.Sum() + ) + ); } token.ThrowIfCancellationRequested(); - return new NestJobResult(reason == NestJobStopReason.Completed ? NestJobStatus.Complete : NestJobStatus.Incomplete, - reason, plates, job.Parts.Select(part => new PartFulfillment(part.Id, part.Quantity, placed[part.Id], remaining[part.Id])), - job.Plates.Select(stock => new StockUsage(stock.Id, used[stock.Id], - stock.Quantity is int quantity ? quantity - used[stock.Id] : null))); + return new NestJobResult( + reason == NestJobStopReason.Completed + ? NestJobStatus.Complete + : NestJobStatus.Incomplete, + reason, + plates, + job.Parts.Select(part => new PartFulfillment( + part.Id, + part.Quantity, + placed[part.Id], + remaining[part.Id] + )), + job.Plates.Select(stock => new StockUsage( + stock.Id, + used[stock.Id], + stock.Quantity is int quantity ? quantity - used[stock.Id] : null + )) + ); } - private sealed record CandidateTrial(PlateCandidate Candidate, NestPlateStock Stock, int StockIndex); + private sealed record CandidateTrial( + PlateCandidate Candidate, + NestPlateStock Stock, + int StockIndex + ); - private sealed class CandidateProgress(IProgress progress, string stockId, int plateIndex, - int committedPlates, int committedParts) : IProgress + private sealed class CandidateProgress( + IProgress progress, + string stockId, + int plateIndex, + int committedPlates, + int committedParts + ) : IProgress { public void Report(NestJobProgress value) { ArgumentNullException.ThrowIfNull(value); - progress.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stockId, plateIndex, - committedPlates, committedParts, value.LegacyProgress)); + progress.Report( + new NestJobProgress( + NestJobStage.EvaluatingCandidate, + stockId, + plateIndex, + committedPlates, + committedParts, + value.LegacyProgress + ) + ); } } } diff --git a/OpenNest.Engine/Jobs/NestJobValidator.cs b/OpenNest.Engine/Jobs/NestJobValidator.cs index bf2191a..09278fc 100644 --- a/OpenNest.Engine/Jobs/NestJobValidator.cs +++ b/OpenNest.Engine/Jobs/NestJobValidator.cs @@ -13,35 +13,65 @@ public static class NestJobValidator foreach (var stock in job.Plates) { var edges = stock.EdgeSpacing; - if (!Positive(stock.Size.Width) || !Positive(stock.Size.Length) || - !Nonnegative(stock.PartSpacing) || !Nonnegative(edges.Left) || !Nonnegative(edges.Right) || - !Nonnegative(edges.Top) || !Nonnegative(edges.Bottom) || stock.Quadrant < 1 || stock.Quadrant > 4 || - edges.Left + edges.Right >= stock.Size.Length || edges.Top + edges.Bottom >= stock.Size.Width) - throw new ArgumentException($"Invalid stock dimensions/settings: {stock.Id}.", nameof(job)); + if ( + !Positive(stock.Size.Width) + || !Positive(stock.Size.Length) + || !Nonnegative(stock.PartSpacing) + || !Nonnegative(edges.Left) + || !Nonnegative(edges.Right) + || !Nonnegative(edges.Top) + || !Nonnegative(edges.Bottom) + || stock.Quadrant < 1 + || stock.Quadrant > 4 + || edges.Left + edges.Right >= stock.Size.Length + || edges.Top + edges.Bottom >= stock.Size.Width + ) + throw new ArgumentException( + $"Invalid stock dimensions/settings: {stock.Id}.", + nameof(job) + ); } foreach (var part in job.Parts) { - if (part.Geometry.Motions.Count == 0 || part.Geometry.Motions.Any(m => - !double.IsFinite(m.X) || !double.IsFinite(m.Y) || - !double.IsFinite(m.CenterX) || !double.IsFinite(m.CenterY))) - throw new ArgumentException($"Geometry must contain finite motions: {part.Id}.", nameof(job)); + if ( + part.Geometry.Motions.Count == 0 + || part.Geometry.Motions.Any(m => + !double.IsFinite(m.X) + || !double.IsFinite(m.Y) + || !double.IsFinite(m.CenterX) + || !double.IsFinite(m.CenterY) + ) + ) + throw new ArgumentException( + $"Geometry must contain finite motions: {part.Id}.", + nameof(job) + ); try { NestJobPlacementValidator.ValidateGeometry(part.Geometry); } catch (ArgumentException exception) { - throw new ArgumentException($"Geometry must contain usable closed edges: {part.Id}. {exception.Message}", nameof(job), exception); + throw new ArgumentException( + $"Geometry must contain usable closed edges: {part.Id}. {exception.Message}", + nameof(job), + exception + ); } } } - internal static void ValidateCandidate(PlateCandidate candidate, NestPlateStock stock, - IReadOnlyDictionary remaining, IReadOnlyDictionary parts) + internal static void ValidateCandidate( + PlateCandidate candidate, + NestPlateStock stock, + IReadOnlyDictionary remaining, + IReadOnlyDictionary parts + ) { NestJobPlacementValidator.ValidateCandidate(candidate, stock, remaining, parts); } private static bool Positive(double value) => double.IsFinite(value) && value > 0; + private static bool Nonnegative(double value) => double.IsFinite(value) && value >= 0; } diff --git a/OpenNest.Engine/Jobs/NestPlateStock.cs b/OpenNest.Engine/Jobs/NestPlateStock.cs index b0d1130..f7fcbf8 100644 --- a/OpenNest.Engine/Jobs/NestPlateStock.cs +++ b/OpenNest.Engine/Jobs/NestPlateStock.cs @@ -6,11 +6,18 @@ namespace OpenNest; /// Immutable stock settings. Size and spacing are copied value types, not caller-owned settings. public sealed class NestPlateStock { - public NestPlateStock(string id, Size size, int? quantity = null, double partSpacing = 0, - Spacing edgeSpacing = default, int quadrant = 1) + public NestPlateStock( + string id, + Size size, + int? quantity = null, + double partSpacing = 0, + Spacing edgeSpacing = default, + int quadrant = 1 + ) { ArgumentException.ThrowIfNullOrWhiteSpace(id); - if (quantity < 0) throw new ArgumentOutOfRangeException(nameof(quantity)); + if (quantity < 0) + throw new ArgumentOutOfRangeException(nameof(quantity)); Id = id; Size = size; Quantity = quantity; @@ -21,6 +28,7 @@ public sealed class NestPlateStock public string Id { get; } public Size Size { get; } + /// Available physical sheets: null is unlimited, zero is legal but unavailable. public int? Quantity { get; } public double PartSpacing { get; } diff --git a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs index c71d045..66166dc 100644 --- a/OpenNest.Engine/Jobs/NestingEngineRegistry.cs +++ b/OpenNest.Engine/Jobs/NestingEngineRegistry.cs @@ -20,20 +20,35 @@ public static class NestingEngineRegistry static NestingEngineRegistry() { - Register("StockLadder", "Caller-stock constrained-first fill and equivalent-demand area repacking", - () => new StockLadderNestingEngine()); + Register( + "StockLadder", + "Caller-stock constrained-first fill and equivalent-demand area repacking", + () => new StockLadderNestingEngine() + ); - Register("Default", "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)", - () => new FixedStrategyNestingEngine("Default")); + Register( + "Default", + "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)", + () => new FixedStrategyNestingEngine("Default") + ); - Register("Strip", "Strip-based nesting for mixed-drawing layouts", - () => new FixedStrategyNestingEngine("Strip")); + Register( + "Strip", + "Strip-based nesting for mixed-drawing layouts", + () => new FixedStrategyNestingEngine("Strip") + ); - Register("Vertical Remnant", "Optimizes for largest right-side vertical drop", - () => new FixedStrategyNestingEngine("Vertical Remnant")); + Register( + "Vertical Remnant", + "Optimizes for largest right-side vertical drop", + () => new FixedStrategyNestingEngine("Vertical Remnant") + ); - Register("Horizontal Remnant", "Optimizes for largest top-side horizontal drop", - () => new FixedStrategyNestingEngine("Horizontal Remnant")); + Register( + "Horizontal Remnant", + "Optimizes for largest top-side horizontal drop", + () => new FixedStrategyNestingEngine("Horizontal Remnant") + ); } public static IReadOnlyList AvailableEngines => engines; @@ -73,24 +88,32 @@ public static class NestingEngineRegistry if (ctor == null) { - Debug.WriteLine($"[NestingEngineRegistry] Skipping {type.Name}: no parameterless constructor"); + Debug.WriteLine( + $"[NestingEngineRegistry] Skipping {type.Name}: no parameterless constructor" + ); continue; } try { Register(type.Name, string.Empty, () => (INestingEngine)ctor.Invoke(null)); - Debug.WriteLine($"[NestingEngineRegistry] Loaded plugin engine: {type.Name}"); + Debug.WriteLine( + $"[NestingEngineRegistry] Loaded plugin engine: {type.Name}" + ); } catch (Exception ex) { - Debug.WriteLine($"[NestingEngineRegistry] Failed to register {type.Name}: {ex.Message}"); + Debug.WriteLine( + $"[NestingEngineRegistry] Failed to register {type.Name}: {ex.Message}" + ); } } } catch (Exception ex) { - Debug.WriteLine($"[NestingEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}"); + Debug.WriteLine( + $"[NestingEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}" + ); } } } diff --git a/OpenNest.Engine/Jobs/PartGeometrySnapshot.cs b/OpenNest.Engine/Jobs/PartGeometrySnapshot.cs index e8cb326..88414fd 100644 --- a/OpenNest.Engine/Jobs/PartGeometrySnapshot.cs +++ b/OpenNest.Engine/Jobs/PartGeometrySnapshot.cs @@ -6,8 +6,16 @@ using OpenNest.CNC; namespace OpenNest; /// Exact immutable CNC motion values. Rapid moves retain contour/hole boundaries; arcs are not tessellated. -public sealed record PartGeometryMotion(CodeType Type, double X, double Y, double CenterX, - double CenterY, RotationType Rotation, LayerType Layer, bool Suppressed); +public sealed record PartGeometryMotion( + CodeType Type, + double X, + double Y, + double CenterX, + double CenterY, + RotationType Rotation, + LayerType Layer, + bool Suppressed +); /// /// Owned geometry only: no Drawing, quantity, events, or mutable CNC references are retained. @@ -29,16 +37,44 @@ public sealed class PartGeometrySnapshot public static PartGeometrySnapshot FromProgram(Program program) { ArgumentNullException.ThrowIfNull(program); - var motions = program.Codes.Select(code => code switch - { - ArcMove arc => new PartGeometryMotion(arc.Type, arc.EndPoint.X, arc.EndPoint.Y, - arc.CenterPoint.X, arc.CenterPoint.Y, arc.Rotation, arc.Layer, arc.Suppressed), - LinearMove line => new PartGeometryMotion(line.Type, line.EndPoint.X, line.EndPoint.Y, - 0, 0, default, line.Layer, line.Suppressed), - RapidMove rapid => new PartGeometryMotion(rapid.Type, rapid.EndPoint.X, rapid.EndPoint.Y, - 0, 0, default, default, rapid.Suppressed), - _ => throw new NotSupportedException("Geometry snapshots currently support only flat rapid/linear/arc programs.") - }); + var motions = program.Codes.Select(code => + code switch + { + ArcMove arc => new PartGeometryMotion( + arc.Type, + arc.EndPoint.X, + arc.EndPoint.Y, + arc.CenterPoint.X, + arc.CenterPoint.Y, + arc.Rotation, + arc.Layer, + arc.Suppressed + ), + LinearMove line => new PartGeometryMotion( + line.Type, + line.EndPoint.X, + line.EndPoint.Y, + 0, + 0, + default, + line.Layer, + line.Suppressed + ), + RapidMove rapid => new PartGeometryMotion( + rapid.Type, + rapid.EndPoint.X, + rapid.EndPoint.Y, + 0, + 0, + default, + default, + rapid.Suppressed + ), + _ => throw new NotSupportedException( + "Geometry snapshots currently support only flat rapid/linear/arc programs." + ), + } + ); return new PartGeometrySnapshot(program.Mode, motions); } } diff --git a/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs b/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs index e184090..857d506 100644 --- a/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs +++ b/OpenNest.Engine/Jobs/Placement/DefaultPlateNester.cs @@ -21,20 +21,25 @@ public sealed class DefaultPlateNester : IPlateNester { private readonly Func engineFactory; private readonly Dictionary drawingsById = new(StringComparer.Ordinal); - private readonly Dictionary idByDrawing = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary idByDrawing = new( + ReferenceEqualityComparer.Instance + ); - public DefaultPlateNester() : this(static plate => new DefaultNestEngine(plate)) - { - } + public DefaultPlateNester() + : this(static plate => new DefaultNestEngine(plate)) { } /// Injectable for tests; defaults to . public DefaultPlateNester(Func engineFactory) { - this.engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory)); + this.engineFactory = + engineFactory ?? throw new ArgumentNullException(nameof(engineFactory)); } - public PlateCandidate Place(PlatePlacementRequest request, IProgress progress = null, - CancellationToken token = default) + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress progress = null, + CancellationToken token = default + ) { ArgumentNullException.ThrowIfNull(request); token.ThrowIfCancellationRequested(); @@ -52,29 +57,38 @@ public sealed class DefaultPlateNester : IPlateNester // Quantity is the request's remaining demand; the engine may mutate this per-trial item, // and that mutation is deliberately discarded — placement counts come from the result. - items.Add(new NestItem - { - Drawing = drawing, - Quantity = requirement.Quantity, - Priority = requirement.Priority, - StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation), - RotationStart = requirement.Rotation.Start, - RotationEnd = requirement.Rotation.End - }); + items.Add( + new NestItem + { + Drawing = drawing, + Quantity = requirement.Quantity, + Priority = requirement.Priority, + StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation), + RotationStart = requirement.Rotation.Start, + RotationEnd = requirement.Rotation.End, + } + ); } - var engine = engineFactory(plate) ?? throw new InvalidOperationException("Engine factory returned null."); + var engine = + engineFactory(plate) + ?? throw new InvalidOperationException("Engine factory returned null."); var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id); var parts = engine.Nest(items, legacyProgress, token); token.ThrowIfCancellationRequested(); - if (parts == null) throw new InvalidOperationException("Engine returned null placements."); + if (parts == null) + throw new InvalidOperationException("Engine returned null placements."); var placements = new List(parts.Count); foreach (var part in parts) { if (part?.BaseDrawing == null || !idByDrawing.TryGetValue(part.BaseDrawing, out var id)) - throw new InvalidOperationException("Placement does not reference a known requirement drawing."); - placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation)); + throw new InvalidOperationException( + "Placement does not reference a known requirement drawing." + ); + placements.Add( + new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation) + ); } return new PlateCandidate(placements); diff --git a/OpenNest.Engine/Jobs/Placement/OrderedPlateNester.cs b/OpenNest.Engine/Jobs/Placement/OrderedPlateNester.cs index c8476c7..37b3738 100644 --- a/OpenNest.Engine/Jobs/Placement/OrderedPlateNester.cs +++ b/OpenNest.Engine/Jobs/Placement/OrderedPlateNester.cs @@ -13,8 +13,11 @@ internal sealed class OrderedPlateNester : IPlateNester { private readonly Dictionary drawings = new(StringComparer.Ordinal); - public PlateCandidate Place(PlatePlacementRequest request, IProgress progress = null, - CancellationToken token = default) + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress progress = null, + CancellationToken token = default + ) { var work = DrawingJobMapper.CreatePlate(request.Stock).WorkArea(); var poses = new List(); @@ -38,27 +41,55 @@ internal sealed class OrderedPlateNester : IPlateNester token.ThrowIfCancellationRequested(); // FillLinear uses actual line/arc geometry for copy distances. var parts = new FillLinear(region, request.Stock.PartSpacing) - .Fill(drawing, angle, NestDirection.Horizontal).Take(left).ToList(); - if (parts.Count == 0 || (best != null && parts.Count <= best.Count)) continue; - var trial = poses.Concat(parts.Select(p => new NestJobPlacement(requirement.Id, 0, - p.Location.X, p.Location.Y, p.Rotation))).ToList(); + .Fill(drawing, angle, NestDirection.Horizontal) + .Take(left) + .ToList(); + if (parts.Count == 0 || (best != null && parts.Count <= best.Count)) + continue; + var trial = poses + .Concat( + parts.Select(p => new NestJobPlacement( + requirement.Id, + 0, + p.Location.X, + p.Location.Y, + p.Rotation + )) + ) + .ToList(); try { - NestJobValidator.ValidateCandidate(new PlateCandidate(trial), request.Stock, demand, requirements); + NestJobValidator.ValidateCandidate( + new PlateCandidate(trial), + request.Stock, + demand, + requirements + ); best = parts; } catch (InvalidOperationException) { // Geometry kernels are proposal generators, never the acceptance gate. } - if (best?.Count == left) break; + if (best?.Count == left) + break; } - if (best?.Count == left) break; + if (best?.Count == left) + break; } - if (best == null) break; + if (best == null) + break; foreach (var part in best) { - poses.Add(new NestJobPlacement(requirement.Id, 0, part.Location.X, part.Location.Y, part.Rotation)); + poses.Add( + new NestJobPlacement( + requirement.Id, + 0, + part.Location.X, + part.Location.Y, + part.Rotation + ) + ); obstacles.Add(part.BoundingBox.Offset(request.Stock.PartSpacing)); } left -= best.Count; @@ -83,13 +114,15 @@ internal sealed class OrderedPlateNester : IPlateNester yield return System.Math.PI; yield return 3 * System.Math.PI / 2; for (var degrees = 5; degrees < 180; degrees += 5) - if (degrees != 90) yield return degrees * System.Math.PI / 180; + if (degrees != 90) + yield return degrees * System.Math.PI / 180; yield break; } for (var index = 0L; ; index++) { var angle = policy.Start + index * policy.Step; - if (angle > policy.End + 1e-9) yield break; + if (angle > policy.End + 1e-9) + yield break; yield return angle; } } diff --git a/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs b/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs index 8c6060c..6e46b80 100644 --- a/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs +++ b/OpenNest.Engine/Jobs/Placement/StripPlateNester.cs @@ -18,20 +18,25 @@ public sealed class StripPlateNester : IPlateNester { private readonly Func engineFactory; private readonly Dictionary drawingsById = new(StringComparer.Ordinal); - private readonly Dictionary idByDrawing = new(ReferenceEqualityComparer.Instance); + private readonly Dictionary idByDrawing = new( + ReferenceEqualityComparer.Instance + ); - public StripPlateNester() : this(static plate => new StripNestEngine(plate)) - { - } + public StripPlateNester() + : this(static plate => new StripNestEngine(plate)) { } /// Injectable for tests; defaults to . public StripPlateNester(Func engineFactory) { - this.engineFactory = engineFactory ?? throw new ArgumentNullException(nameof(engineFactory)); + this.engineFactory = + engineFactory ?? throw new ArgumentNullException(nameof(engineFactory)); } - public PlateCandidate Place(PlatePlacementRequest request, IProgress progress = null, - CancellationToken token = default) + public PlateCandidate Place( + PlatePlacementRequest request, + IProgress progress = null, + CancellationToken token = default + ) { ArgumentNullException.ThrowIfNull(request); token.ThrowIfCancellationRequested(); @@ -47,29 +52,38 @@ public sealed class StripPlateNester : IPlateNester idByDrawing.Add(drawing, requirement.Id); } - items.Add(new NestItem - { - Drawing = drawing, - Quantity = requirement.Quantity, - Priority = requirement.Priority, - StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation), - RotationStart = requirement.Rotation.Start, - RotationEnd = requirement.Rotation.End - }); + items.Add( + new NestItem + { + Drawing = drawing, + Quantity = requirement.Quantity, + Priority = requirement.Priority, + StepAngle = DrawingJobMapper.LegacyStep(requirement.Rotation), + RotationStart = requirement.Rotation.Start, + RotationEnd = requirement.Rotation.End, + } + ); } - var engine = engineFactory(plate) ?? throw new InvalidOperationException("Engine factory returned null."); + var engine = + engineFactory(plate) + ?? throw new InvalidOperationException("Engine factory returned null."); var legacyProgress = CandidateProgressBridge.Create(progress, request.Stock.Id); var parts = engine.Nest(items, legacyProgress, token); token.ThrowIfCancellationRequested(); - if (parts == null) throw new InvalidOperationException("Engine returned null placements."); + if (parts == null) + throw new InvalidOperationException("Engine returned null placements."); var placements = new List(parts.Count); foreach (var part in parts) { if (part?.BaseDrawing == null || !idByDrawing.TryGetValue(part.BaseDrawing, out var id)) - throw new InvalidOperationException("Placement does not reference a known requirement drawing."); - placements.Add(new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation)); + throw new InvalidOperationException( + "Placement does not reference a known requirement drawing." + ); + placements.Add( + new NestJobPlacement(id, 0, part.Location.X, part.Location.Y, part.Rotation) + ); } return new PlateCandidate(placements); diff --git a/OpenNest.Engine/Jobs/PlateCandidate.cs b/OpenNest.Engine/Jobs/PlateCandidate.cs index a09512c..58a9f0c 100644 --- a/OpenNest.Engine/Jobs/PlateCandidate.cs +++ b/OpenNest.Engine/Jobs/PlateCandidate.cs @@ -5,6 +5,8 @@ namespace OpenNest; /// Owned candidate poses only; not committed fulfillment or inventory accounting. public sealed class PlateCandidate { - public PlateCandidate(IEnumerable placements) => Placements = NestJob.Own(placements); + public PlateCandidate(IEnumerable placements) => + Placements = NestJob.Own(placements); + public IReadOnlyList Placements { get; } } diff --git a/OpenNest.Engine/Jobs/PlateNesterFactory.cs b/OpenNest.Engine/Jobs/PlateNesterFactory.cs index 495bfee..66dc6f3 100644 --- a/OpenNest.Engine/Jobs/PlateNesterFactory.cs +++ b/OpenNest.Engine/Jobs/PlateNesterFactory.cs @@ -1,4 +1,5 @@ using System; + namespace OpenNest; /// @@ -16,9 +17,13 @@ public static class PlateNesterFactory { "Default" => new DefaultPlateNester(), "Strip" => new StripPlateNester(), - "Vertical Remnant" => new LegacyPlateNesterAdapter(plate => new VerticalRemnantEngine(plate)), - "Horizontal Remnant" => new LegacyPlateNesterAdapter(plate => new HorizontalRemnantEngine(plate)), - _ => throw new NotSupportedException($"Unknown placement strategy: {strategy}.") + "Vertical Remnant" => new LegacyPlateNesterAdapter(plate => new VerticalRemnantEngine( + plate + )), + "Horizontal Remnant" => new LegacyPlateNesterAdapter( + plate => new HorizontalRemnantEngine(plate) + ), + _ => throw new NotSupportedException($"Unknown placement strategy: {strategy}."), }; } } diff --git a/OpenNest.Engine/Jobs/RotationPolicy.cs b/OpenNest.Engine/Jobs/RotationPolicy.cs index 13cdddd..3b4844c 100644 --- a/OpenNest.Engine/Jobs/RotationPolicy.cs +++ b/OpenNest.Engine/Jobs/RotationPolicy.cs @@ -2,7 +2,12 @@ using System; namespace OpenNest; -public enum RotationPolicyKind { Fixed, BoundedSweep, Automatic } +public enum RotationPolicyKind +{ + Fixed, + BoundedSweep, + Automatic, +} /// Immutable rotation constraints, in radians about the geometry origin. public sealed class RotationPolicy @@ -22,14 +27,21 @@ public sealed class RotationPolicy public double End { get; } public double Step { get; } public static RotationPolicy Automatic { get; } = new(RotationPolicyKind.Automatic, 0, 0, 0); - public static RotationPolicy Fixed(double angle) => new(RotationPolicyKind.Fixed, angle, angle, 0); + + public static RotationPolicy Fixed(double angle) => + new(RotationPolicyKind.Fixed, angle, angle, 0); + public static RotationPolicy BoundedSweep(double start, double end, double step) { - if (step <= 0 || end < start) throw new ArgumentException("Sweep needs a positive step and ordered bounds."); + if (step <= 0 || end < start) + throw new ArgumentException("Sweep needs a positive step and ordered bounds."); return new RotationPolicy(RotationPolicyKind.BoundedSweep, start, end, step); } /// Preserves the legacy zero-step automatic sentinel; zero never means locked rotation. - public static RotationPolicy FromLegacy(double stepAngle, double rotationStart, double rotationEnd) => - stepAngle == 0 ? Automatic : BoundedSweep(rotationStart, rotationEnd, stepAngle); + public static RotationPolicy FromLegacy( + double stepAngle, + double rotationStart, + double rotationEnd + ) => stepAngle == 0 ? Automatic : BoundedSweep(rotationStart, rotationEnd, stepAngle); } diff --git a/OpenNest.Engine/Jobs/StockLadderNestingEngine.cs b/OpenNest.Engine/Jobs/StockLadderNestingEngine.cs index 2adcae4..2d6e5c7 100644 --- a/OpenNest.Engine/Jobs/StockLadderNestingEngine.cs +++ b/OpenNest.Engine/Jobs/StockLadderNestingEngine.cs @@ -13,12 +13,18 @@ namespace OpenNest; public sealed class StockLadderNestingEngine : INestingEngine { private readonly Func factory; - public StockLadderNestingEngine() : this(() => new OrderedPlateNester()) { } + + public StockLadderNestingEngine() + : this(() => new OrderedPlateNester()) { } + public StockLadderNestingEngine(Func factory) => this.factory = factory ?? throw new ArgumentNullException(nameof(factory)); - public NestJobResult Solve(NestJob job, IProgress progress = null, - CancellationToken token = default) + public NestJobResult Solve( + NestJob job, + IProgress progress = null, + CancellationToken token = default + ) { ArgumentNullException.ThrowIfNull(job); token.ThrowIfCancellationRequested(); @@ -33,32 +39,41 @@ public sealed class StockLadderNestingEngine : INestingEngine // Probe actual validated single-part placements, not bounding-box fit assertions. foreach (var part in job.Parts) - foreach (var stock in job.Plates.Where(s => s.Quantity != 0)) - { - var probe = Trial(stock, new[] { WithQuantity(part, 1) }); - if (probe.Placements.Count != 0) feasible[part.Id].Add(stock.Id); - } - var ordered = job.Parts.OrderBy(p => p.Priority) - .ThenBy(p => feasible[p.Id].Count).ThenByDescending(p => areas[p.Id]).ToList(); + foreach (var stock in job.Plates.Where(s => s.Quantity != 0)) + { + var probe = Trial(stock, new[] { WithQuantity(part, 1) }); + if (probe.Placements.Count != 0) + feasible[part.Id].Add(stock.Id); + } + var ordered = job + .Parts.OrderBy(p => p.Priority) + .ThenBy(p => feasible[p.Id].Count) + .ThenByDescending(p => areas[p.Id]) + .ToList(); var reason = NestJobStopReason.Completed; while (remaining.Values.Any(n => n > 0)) { token.ThrowIfCancellationRequested(); if (job.Options.MaxPlates <= sheets.Count) { - if (Consolidate()) continue; + if (Consolidate()) + continue; reason = NestJobStopReason.PlateLimitReached; break; } - var available = job.Plates.Where(s => s.Quantity == null || used[s.Id] < s.Quantity).ToList(); + var available = job + .Plates.Where(s => s.Quantity == null || used[s.Id] < s.Quantity) + .ToList(); if (available.Count == 0) { - if (Consolidate()) continue; + if (Consolidate()) + continue; reason = NestJobStopReason.StockExhausted; break; } - var anchor = ordered.FirstOrDefault(p => remaining[p.Id] > 0 && - available.Any(s => feasible[p.Id].Contains(s.Id))); + var anchor = ordered.FirstOrDefault(p => + remaining[p.Id] > 0 && available.Any(s => feasible[p.Id].Contains(s.Id)) + ); if (anchor == null) { reason = NestJobStopReason.NoPlacementFound; @@ -69,14 +84,18 @@ public sealed class StockLadderNestingEngine : INestingEngine foreach (var stock in available.Where(s => feasible[anchor.Id].Contains(s.Id))) { // Pin the constrained anchor before fillers, including quantity-one requirements. - var requests = new[] { anchor }.Concat(ordered.Where(p => p.Id != anchor.Id)) - .Where(p => remaining[p.Id] > 0).Select(p => WithQuantity(p, remaining[p.Id])); + var requests = new[] { anchor } + .Concat(ordered.Where(p => p.Id != anchor.Id)) + .Where(p => remaining[p.Id] > 0) + .Select(p => WithQuantity(p, remaining[p.Id])); var candidate = Trial(stock, requests); - if (!candidate.Placements.Any(p => p.PartId == anchor.Id)) continue; + if (!candidate.Placements.Any(p => p.PartId == anchor.Id)) + continue; var sheet = new NestJobPlateResult(sheets.Count, stock, candidate.Placements); // Initial construction only: material area, never raw part counts. Repacking below // compares EXACTLY equivalent demand, and never replaces a sheet by a partial fill. - var value = EstimateNetArea(job, sheet) / candidate.Placements.Sum(p => areas[p.PartId]); + var value = + EstimateNetArea(job, sheet) / candidate.Placements.Sum(p => areas[p.PartId]); if (value < score - 1e-9) { winner = sheet; @@ -90,28 +109,69 @@ public sealed class StockLadderNestingEngine : INestingEngine } sheets.Add(winner); used[winner.StockId]++; - foreach (var pose in winner.Placements) remaining[pose.PartId]--; - progress?.Report(new NestJobProgress(NestJobStage.PlateCommitted, winner.StockId, - sheets.Count - 1, sheets.Count, sheets.Sum(s => s.Placements.Count))); + foreach (var pose in winner.Placements) + remaining[pose.PartId]--; + progress?.Report( + new NestJobProgress( + NestJobStage.PlateCommitted, + winner.StockId, + sheets.Count - 1, + sheets.Count, + sheets.Sum(s => s.Placements.Count) + ) + ); } Consolidate(); token.ThrowIfCancellationRequested(); var placed = job.Parts.ToDictionary(p => p.Id, _ => 0); - var final = sheets.Select((sheet, index) => new NestJobPlateResult(index, sheet.Stock, - sheet.Placements.Select(p => p with { InstanceIndex = placed[p.PartId]++ }).ToList())).ToList(); - return new NestJobResult(reason == NestJobStopReason.Completed ? NestJobStatus.Complete : NestJobStatus.Incomplete, - reason, final, job.Parts.Select(p => new PartFulfillment(p.Id, p.Quantity, placed[p.Id], remaining[p.Id])), - job.Plates.Select(s => new StockUsage(s.Id, used[s.Id], s.Quantity - used[s.Id]))); + var final = sheets + .Select( + (sheet, index) => + new NestJobPlateResult( + index, + sheet.Stock, + sheet + .Placements.Select(p => p with { InstanceIndex = placed[p.PartId]++ }) + .ToList() + ) + ) + .ToList(); + return new NestJobResult( + reason == NestJobStopReason.Completed + ? NestJobStatus.Complete + : NestJobStatus.Incomplete, + reason, + final, + job.Parts.Select(p => new PartFulfillment( + p.Id, + p.Quantity, + placed[p.Id], + remaining[p.Id] + )), + job.Plates.Select(s => new StockUsage(s.Id, used[s.Id], s.Quantity - used[s.Id])) + ); PlateCandidate Trial(NestPlateStock stock, IEnumerable requirements) { token.ThrowIfCancellationRequested(); var request = new PlatePlacementRequest(stock, requirements); - progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id, - sheets.Count, sheets.Count, sheets.Sum(s => s.Placements.Count))); + progress?.Report( + new NestJobProgress( + NestJobStage.EvaluatingCandidate, + stock.Id, + sheets.Count, + sheets.Count, + sheets.Sum(s => s.Placements.Count) + ) + ); var candidate = nester.Place(request, null, token); token.ThrowIfCancellationRequested(); - NestJobValidator.ValidateCandidate(candidate, stock, request.Parts.ToDictionary(p => p.Id, p => p.Quantity), parts); + NestJobValidator.ValidateCandidate( + candidate, + stock, + request.Parts.ToDictionary(p => p.Id, p => p.Quantity), + parts + ); return candidate; } @@ -120,40 +180,55 @@ public sealed class StockLadderNestingEngine : INestingEngine var changed = false; // Single downgrade and adjacent pair merge only: bounded local search, no combinatorial tree. for (var index = 0; index < sheets.Count; index++) - for (var count = System.Math.Min(2, sheets.Count - index); count >= 1; count--) + for (var count = System.Math.Min(2, sheets.Count - index); count >= 1; count--) + { + var old = sheets.Skip(index).Take(count).ToList(); + var demand = old.SelectMany(s => s.Placements) + .GroupBy(p => p.PartId) + .ToDictionary(g => g.Key, g => g.Count()); + var baseline = old.Sum(s => EstimateNetArea(job, s)); + NestJobPlateResult replacement = null; + foreach (var stock in job.Plates) { - var old = sheets.Skip(index).Take(count).ToList(); - var demand = old.SelectMany(s => s.Placements).GroupBy(p => p.PartId) + token.ThrowIfCancellationRequested(); + var returned = old.Count(s => s.StockId == stock.Id); + if (stock.Quantity is int limit && used[stock.Id] - returned >= limit) + continue; + // Even the maximum possible salvage credit cannot beat the incumbent. + var lowerBound = + stock.Size.Width * stock.Size.Length * (1 - job.Options.SalvageRate); + if (lowerBound >= baseline - 1e-9) + continue; + if (demand.Keys.Any(id => !feasible[id].Contains(stock.Id))) + continue; + var candidate = Trial( + stock, + ordered + .Where(p => demand.ContainsKey(p.Id)) + .Select(p => WithQuantity(p, demand[p.Id])) + ); + var actual = candidate + .Placements.GroupBy(p => p.PartId) .ToDictionary(g => g.Key, g => g.Count()); - var baseline = old.Sum(s => EstimateNetArea(job, s)); - NestJobPlateResult replacement = null; - foreach (var stock in job.Plates) - { - token.ThrowIfCancellationRequested(); - var returned = old.Count(s => s.StockId == stock.Id); - if (stock.Quantity is int limit && used[stock.Id] - returned >= limit) continue; - // Even the maximum possible salvage credit cannot beat the incumbent. - var lowerBound = stock.Size.Width * stock.Size.Length * (1 - job.Options.SalvageRate); - if (lowerBound >= baseline - 1e-9) continue; - if (demand.Keys.Any(id => !feasible[id].Contains(stock.Id))) continue; - var candidate = Trial(stock, ordered.Where(p => demand.ContainsKey(p.Id)) - .Select(p => WithQuantity(p, demand[p.Id]))); - var actual = candidate.Placements.GroupBy(p => p.PartId).ToDictionary(g => g.Key, g => g.Count()); - if (demand.Any(kv => !actual.TryGetValue(kv.Key, out var n) || n != kv.Value)) continue; - var trial = new NestJobPlateResult(index, stock, candidate.Placements); - var cost = EstimateNetArea(job, trial); - if (cost >= baseline - 1e-9) continue; - baseline = cost; - replacement = trial; - } - if (replacement == null) continue; - // No accounting changes until the entire equivalent-demand candidate is valid. - foreach (var sheet in old) used[sheet.StockId]--; - used[replacement.StockId]++; - sheets.RemoveRange(index, count); - sheets.Insert(index, replacement); - changed = true; + if (demand.Any(kv => !actual.TryGetValue(kv.Key, out var n) || n != kv.Value)) + continue; + var trial = new NestJobPlateResult(index, stock, candidate.Placements); + var cost = EstimateNetArea(job, trial); + if (cost >= baseline - 1e-9) + continue; + baseline = cost; + replacement = trial; } + if (replacement == null) + continue; + // No accounting changes until the entire equivalent-demand candidate is valid. + foreach (var sheet in old) + used[sheet.StockId]--; + used[replacement.StockId]++; + sheets.RemoveRange(index, count); + sheets.Insert(index, replacement); + changed = true; + } return changed; } } @@ -169,27 +244,33 @@ public sealed class StockLadderNestingEngine : INestingEngine { var area = sheet.Stock.Size.Width * sheet.Stock.Size.Length; var minimum = job.Options.MinimumSalvageDimension; - if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0) return area; + if (job.Options.SalvageRate == 0 || minimum <= 0 || sheet.Placements.Count == 0) + return area; var work = DrawingJobMapper.CreatePlate(sheet.Stock).WorkArea(); var parts = job.Parts.ToDictionary(p => p.Id); - var boxes = sheet.Placements.Select(p => - { - var part = new Part(DrawingJobMapper.CreateDrawing(parts[p.PartId])); - part.Rotate(p.Rotation); - part.Location = new OpenNest.Geometry.Vector(p.X, p.Y); - part.UpdateBounds(); - return part.BoundingBox; - }).ToList(); + var boxes = sheet + .Placements.Select(p => + { + var part = new Part(DrawingJobMapper.CreateDrawing(parts[p.PartId])); + part.Rotate(p.Rotation); + part.Location = new OpenNest.Geometry.Vector(p.X, p.Y); + part.UpdateBounds(); + return part.BoundingBox; + }) + .ToList(); var gap = sheet.Stock.PartSpacing; var candidates = new[] { (work.Length, boxes.Min(b => b.Bottom) - work.Bottom - gap), (work.Length, work.Top - boxes.Max(b => b.Top) - gap), (boxes.Min(b => b.Left) - work.Left - gap, work.Width), - (work.Right - boxes.Max(b => b.Right) - gap, work.Width) + (work.Right - boxes.Max(b => b.Right) - gap, work.Width), }; - var salvage = candidates.Where(c => c.Item1 >= minimum && c.Item2 >= minimum) - .Select(c => c.Item1 * c.Item2).DefaultIfEmpty(0).Max(); + var salvage = candidates + .Where(c => c.Item1 >= minimum && c.Item2 >= minimum) + .Select(c => c.Item1 * c.Item2) + .DefaultIfEmpty(0) + .Max(); return area - job.Options.SalvageRate * salvage; } } diff --git a/OpenNest.Engine/LeadInAssigner.cs b/OpenNest.Engine/LeadInAssigner.cs index 4150f1a..ba765ac 100644 --- a/OpenNest.Engine/LeadInAssigner.cs +++ b/OpenNest.Engine/LeadInAssigner.cs @@ -1,8 +1,8 @@ +using System.Collections.Generic; +using System.Linq; using OpenNest.CNC.CuttingStrategy; using OpenNest.Engine.Sequencing; using OpenNest.Geometry; -using System.Collections.Generic; -using System.Linq; namespace OpenNest.Engine { @@ -26,8 +26,12 @@ namespace OpenNest.Engine AssignPass(sequenced, parameters, exitPoint, nextPiercePoints: piercePoints); } - private Vector[] AssignPass(List sequenced, CuttingParameters parameters, - Vector exitPoint, Vector[] nextPiercePoints) + private Vector[] AssignPass( + List sequenced, + CuttingParameters parameters, + Vector exitPoint, + Vector[] nextPiercePoints + ) { var piercePoints = new Vector[sequenced.Count]; var currentPoint = exitPoint; diff --git a/OpenNest.Engine/ML/AnglePredictor.cs b/OpenNest.Engine/ML/AnglePredictor.cs index 37cb749..5b18724 100644 --- a/OpenNest.Engine/ML/AnglePredictor.cs +++ b/OpenNest.Engine/ML/AnglePredictor.cs @@ -1,11 +1,11 @@ -using Microsoft.ML.OnnxRuntime; -using Microsoft.ML.OnnxRuntime.Tensors; -using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using OpenNest.Math; namespace OpenNest.Engine.ML { @@ -16,8 +16,11 @@ namespace OpenNest.Engine.ML private static readonly object _lock = new(); public static List PredictAngles( - PartFeatures features, double sheetWidth, double sheetHeight, - double threshold = 0.3) + PartFeatures features, + double sheetWidth, + double sheetHeight, + double threshold = 0.3 + ) { var session = GetSession(); if (session == null) @@ -41,7 +44,7 @@ namespace OpenNest.Engine.ML var tensor = new DenseTensor(input, new[] { 1, 11 }); var inputs = new List { - NamedOnnxValue.CreateFromTensor("features", tensor) + NamedOnnxValue.CreateFromTensor("features", tensor), }; using var results = session.Run(inputs); diff --git a/OpenNest.Engine/ML/BruteForceRunner.cs b/OpenNest.Engine/ML/BruteForceRunner.cs index 7aaaa32..7db8a08 100644 --- a/OpenNest.Engine/ML/BruteForceRunner.cs +++ b/OpenNest.Engine/ML/BruteForceRunner.cs @@ -24,24 +24,32 @@ namespace OpenNest.Engine.ML public static class BruteForceRunner { - public static BruteForceResult Run(Drawing drawing, Plate plate, bool forceFullAngleSweep = false) + public static BruteForceResult Run( + Drawing drawing, + Plate plate, + bool forceFullAngleSweep = false + ) { var engine = new DefaultNestEngine(plate); engine.ForceFullAngleSweep = forceFullAngleSweep; var item = new NestItem { Drawing = drawing }; var sw = Stopwatch.StartNew(); - var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var parts = engine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); sw.Stop(); if (parts == null || parts.Count == 0) return null; // Rank phase results — winner is explicit, runners-up sorted by count. - var winner = engine.PhaseResults - .FirstOrDefault(r => r.Phase == engine.WinnerPhase); - var runnerUps = engine.PhaseResults - .Where(r => r.PartCount > 0 && r.Phase != engine.WinnerPhase) + var winner = engine.PhaseResults.FirstOrDefault(r => r.Phase == engine.WinnerPhase); + var runnerUps = engine + .PhaseResults.Where(r => r.PartCount > 0 && r.Phase != engine.WinnerPhase) .OrderByDescending(r => r.PartCount) .ToList(); @@ -60,19 +68,27 @@ namespace OpenNest.Engine.ML ThirdPlaceEngine = runnerUps.Count > 1 ? runnerUps[1].Phase.ToString() : "", ThirdPlacePartCount = runnerUps.Count > 1 ? runnerUps[1].PartCount : 0, ThirdPlaceTimeMs = runnerUps.Count > 1 ? runnerUps[1].TimeMs : 0, - AngleResults = engine.AngleResults.ToList() + AngleResults = engine.AngleResults.ToList(), }; } private static string SerializeLayout(List parts) { - var data = parts.Select(p => new { X = p.Location.X, Y = p.Location.Y, R = p.Rotation }).ToList(); + var data = parts + .Select(p => new + { + X = p.Location.X, + Y = p.Location.Y, + R = p.Rotation, + }) + .ToList(); return System.Text.Json.JsonSerializer.Serialize(data); } private static double CalculateUtilization(List parts, double plateArea) { - if (plateArea <= 0) return 0; + if (plateArea <= 0) + return 0; return parts.Sum(p => p.BaseDrawing.Area) / plateArea; } } diff --git a/OpenNest.Engine/ML/FeatureExtractor.cs b/OpenNest.Engine/ML/FeatureExtractor.cs index 3920533..1127802 100644 --- a/OpenNest.Engine/ML/FeatureExtractor.cs +++ b/OpenNest.Engine/ML/FeatureExtractor.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Linq; +using OpenNest.Geometry; namespace OpenNest.Engine.ML { @@ -7,10 +7,10 @@ namespace OpenNest.Engine.ML { // --- Geometric Features --- public double Area { get; set; } - public double Convexity { get; set; } // Area / Convex Hull Area - public double AspectRatio { get; set; } // Width / Length - public double BoundingBoxFill { get; set; } // Area / (Width * Length) - public double Circularity { get; set; } // 4 * PI * Area / Perimeter^2 + public double Convexity { get; set; } // Area / Convex Hull Area + public double AspectRatio { get; set; } // Width / Length + public double BoundingBoxFill { get; set; } // Area / (Width * Length) + public double Circularity { get; set; } // 4 * PI * Area / Perimeter^2 public double PerimeterToAreaRatio { get; set; } // Perimeter / Area — spacing sensitivity public int VertexCount { get; set; } @@ -30,14 +30,16 @@ namespace OpenNest.Engine.ML // Normalize to canonical frame so features are invariant to import orientation. var canonical = CanonicalFrame.AsCanonicalCopy(drawing); - var entities = OpenNest.Converters.ConvertProgram.ToGeometry(canonical.Program) + var entities = OpenNest + .Converters.ConvertProgram.ToGeometry(canonical.Program) .Where(e => e.Layer != SpecialLayers.Rapid) .ToList(); var profile = new ShapeProfile(entities); var perimeter = profile.Perimeter; - if (perimeter == null) return null; + if (perimeter == null) + return null; var polygon = perimeter.ToPolygonWithTolerance(0.01); polygon.UpdateBounds(); @@ -53,12 +55,13 @@ namespace OpenNest.Engine.ML AspectRatio = bb.Length / (bb.Width > 0 ? bb.Width : 1.0), BoundingBoxFill = canonical.Area / (bb.Area() > 0 ? bb.Area() : 1.0), VertexCount = polygon.Vertices.Count, - Bitmask = GenerateBitmask(polygon, 32) + Bitmask = GenerateBitmask(polygon, 32), }; // Circularity = 4 * PI * Area / Perimeter^2 var perimeterLen = polygon.Perimeter(); - features.Circularity = (4 * System.Math.PI * canonical.Area) / (perimeterLen * perimeterLen); + features.Circularity = + (4 * System.Math.PI * canonical.Area) / (perimeterLen * perimeterLen); features.PerimeterToAreaRatio = canonical.Area > 0 ? perimeterLen / canonical.Area : 0; return features; diff --git a/OpenNest.Engine/MultiPlateNester.cs b/OpenNest.Engine/MultiPlateNester.cs index 67c221f..48b78d1 100644 --- a/OpenNest.Engine/MultiPlateNester.cs +++ b/OpenNest.Engine/MultiPlateNester.cs @@ -32,7 +32,9 @@ namespace OpenNest private MultiPlateNester( MultiPlateNestOptions options, List existingPlates, - IProgress progress, CancellationToken token) + IProgress progress, + CancellationToken token + ) { _options = options; _template = options.Template; @@ -49,16 +51,20 @@ namespace OpenNest public static bool FitsBounds(Box container, Box part) { - var fitsNormal = container.Width >= part.Width - Tolerance.Epsilon - && container.Length >= part.Length - Tolerance.Epsilon; - var fitsRotated = container.Width >= part.Length - Tolerance.Epsilon - && container.Length >= part.Width - Tolerance.Epsilon; + var fitsNormal = + container.Width >= part.Width - Tolerance.Epsilon + && container.Length >= part.Length - Tolerance.Epsilon; + var fitsRotated = + container.Width >= part.Length - Tolerance.Epsilon + && container.Length >= part.Width - Tolerance.Epsilon; return fitsNormal || fitsRotated; } public static List SortItems(List items, PartSortOrder sortOrder) { - var withBounds = items.Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox())).ToList(); + var withBounds = items + .Select(i => (Item: i, Bounds: i.Drawing.Program.BoundingBox())) + .ToList(); switch (sortOrder) { @@ -151,7 +157,8 @@ namespace OpenNest PlateOption upgradeSize, PlateOption newPlateSize, double salvageRate, - double estimatedNewPlateUtilization) + double estimatedNewPlateUtilization + ) { var upgradeCost = upgradeSize.Cost - currentSize.Cost; @@ -175,7 +182,8 @@ namespace OpenNest MultiPlateNestOptions options, List existingPlates = null, IProgress progress = null, - CancellationToken token = default) + CancellationToken token = default + ) { var nester = new MultiPlateNester(options, existingPlates, progress, token); return nester.Run(items, options.SortOrder, options.AllowPlateCreation); @@ -205,7 +213,8 @@ namespace OpenNest var zoneAspect = zone.Width / zone.Length; var partAspect = partBounds.Width / partBounds.Length; - var aspectMatch = System.Math.Min(zoneAspect, partAspect) / System.Math.Max(zoneAspect, partAspect); + var aspectMatch = + System.Math.Min(zoneAspect, partAspect) / System.Math.Max(zoneAspect, partAspect); return utilization * 0.7 + aspectMatch * 0.3; } @@ -237,7 +246,8 @@ namespace OpenNest if (HasPlateOptions) { pr.ChosenSize = _plateOptions.FirstOrDefault(o => - o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length)); + o.Width.IsEqualTo(plate.Size.Width) && o.Length.IsEqualTo(plate.Size.Length) + ); } return pr; @@ -269,7 +279,11 @@ namespace OpenNest return pool; } - private bool TryWithUpgradedSize(PlateResult pr, PlateOption upgradeOption, Func, bool> tryFill) + private bool TryWithUpgradedSize( + PlateResult pr, + PlateOption upgradeOption, + Func, bool> tryFill + ) { var oldSize = pr.Plate.Size; var oldChosenSize = pr.ChosenSize; @@ -289,12 +303,18 @@ namespace OpenNest private PlateOption FindSmallestFittingOption(Box partBounds) { - return _sortedOptions?.FirstOrDefault(o => FitsBounds(OptionWorkArea(o, _template), partBounds)); + return _sortedOptions?.FirstOrDefault(o => + FitsBounds(OptionWorkArea(o, _template), partBounds) + ); } // --- Orchestration --- - private MultiPlateResult Run(List items, PartSortOrder sortOrder, bool allowPlateCreation) + private MultiPlateResult Run( + List items, + PartSortOrder sortOrder, + bool allowPlateCreation + ) { var result = new MultiPlateResult(); @@ -459,9 +479,10 @@ namespace OpenNest var workArea = pr.Plate.WorkArea(); var classification = Classify(partBounds, workArea); - remnantCache[pr] = classification == PartClass.Small - ? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true) - : FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false); + remnantCache[pr] = + classification == PartClass.Small + ? FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: true) + : FindRemnants(pr.Plate, _minRemnantSize, scrapOnly: false); } foreach (var zone in remnantCache[pr]) @@ -522,7 +543,9 @@ namespace OpenNest { var currentOption = pr.ChosenSize; var currentIdx = _sortedOptions.FindIndex(o => - o.Width.IsEqualTo(currentOption.Width) && o.Length.IsEqualTo(currentOption.Length)); + o.Width.IsEqualTo(currentOption.Width) + && o.Length.IsEqualTo(currentOption.Length) + ); if (currentIdx < 0 || currentIdx >= _sortedOptions.Count - 1) continue; @@ -531,8 +554,10 @@ namespace OpenNest { var upgradeOption = _sortedOptions[i]; - if (upgradeOption.Width < currentOption.Width - Tolerance.Epsilon - || upgradeOption.Length < currentOption.Length - Tolerance.Epsilon) + if ( + upgradeOption.Width < currentOption.Width - Tolerance.Epsilon + || upgradeOption.Length < currentOption.Length - Tolerance.Epsilon + ) continue; var smallestNew = FindSmallestFittingOption(partBounds); @@ -541,20 +566,29 @@ namespace OpenNest continue; var utilEst = pr.Plate.Utilization(); - var decision = EvaluateUpgradeVsNew(currentOption, upgradeOption, smallestNew, - _salvageRate, utilEst); + var decision = EvaluateUpgradeVsNew( + currentOption, + upgradeOption, + smallestNew, + _salvageRate, + utilEst + ); if (decision.ShouldUpgrade) { - var placed = TryWithUpgradedSize(pr, upgradeOption, remnants => - { - foreach (var remnant in remnants) + var placed = TryWithUpgradedSize( + pr, + upgradeOption, + remnants => { - if (FillAndPlace(pr, remnant, item) > 0) - return true; + foreach (var remnant in remnants) + { + if (FillAndPlace(pr, remnant, item) > 0) + return true; + } + return false; } - return false; - }); + ); if (placed) return true; @@ -593,53 +627,69 @@ namespace OpenNest var currentOption = target.ChosenSize; - foreach (var upgradeOption in _sortedOptions.Where(o => - o.Width >= currentOption.Width - Tolerance.Epsilon - && o.Length >= currentOption.Length - Tolerance.Epsilon - && (o.Width > currentOption.Width + Tolerance.Epsilon - || o.Length > currentOption.Length + Tolerance.Epsilon))) + foreach ( + var upgradeOption in _sortedOptions.Where(o => + o.Width >= currentOption.Width - Tolerance.Epsilon + && o.Length >= currentOption.Length - Tolerance.Epsilon + && ( + o.Width > currentOption.Width + Tolerance.Epsilon + || o.Length > currentOption.Length + Tolerance.Epsilon + ) + ) + ) { - absorbed = TryWithUpgradedSize(target, upgradeOption, remnants => - { - var engine = NestEngineRegistry.Create(target.Plate); - var tempItems = donorParts - .GroupBy(p => p.BaseDrawing) - .Select(g => new NestItem - { - Drawing = g.Key, - Quantity = g.Count(), - }) - .ToList(); - - var totalPlaced = new List(); - foreach (var remnant in remnants) + absorbed = TryWithUpgradedSize( + target, + upgradeOption, + remnants => { - var placed = engine.PackArea(remnant, tempItems, _progress, _token); - totalPlaced.AddRange(placed); + var engine = NestEngineRegistry.Create(target.Plate); + var tempItems = donorParts + .GroupBy(p => p.BaseDrawing) + .Select(g => new NestItem + { + Drawing = g.Key, + Quantity = g.Count(), + }) + .ToList(); - foreach (var ti in tempItems) + var totalPlaced = new List(); + foreach (var remnant in remnants) { - var count = placed.Count(p => p.BaseDrawing == ti.Drawing); - ti.Quantity = System.Math.Max(0, ti.Quantity - count); + var placed = engine.PackArea( + remnant, + tempItems, + _progress, + _token + ); + totalPlaced.AddRange(placed); + + foreach (var ti in tempItems) + { + var count = placed.Count(p => + p.BaseDrawing == ti.Drawing + ); + ti.Quantity = System.Math.Max(0, ti.Quantity - count); + } + + if (tempItems.All(ti => ti.Quantity <= 0)) + break; } - if (tempItems.All(ti => ti.Quantity <= 0)) - break; + if (totalPlaced.Count >= donorParts.Count) + { + target.AddParts(totalPlaced); + + foreach (var p in donorParts) + donor.Plate.Parts.Remove(p); + donor.Parts.Clear(); + _platePool.Remove(donor); + return true; + } + + return false; } - - if (totalPlaced.Count >= donorParts.Count) - { - target.AddParts(totalPlaced); - - foreach (var p in donorParts) - donor.Plate.Parts.Remove(p); - donor.Parts.Clear(); - _platePool.Remove(donor); - return true; - } - - return false; - }); + ); if (absorbed) break; diff --git a/OpenNest.Engine/NestDirection.cs b/OpenNest.Engine/NestDirection.cs index 9dc801d..17ea779 100644 --- a/OpenNest.Engine/NestDirection.cs +++ b/OpenNest.Engine/NestDirection.cs @@ -1,9 +1,8 @@ - -namespace OpenNest +namespace OpenNest { public enum NestDirection { Vertical, - Horizontal + Horizontal, } } diff --git a/OpenNest.Engine/NestEngineBase.cs b/OpenNest.Engine/NestEngineBase.cs index a79503e..c5cbb12 100644 --- a/OpenNest.Engine/NestEngineBase.cs +++ b/OpenNest.Engine/NestEngineBase.cs @@ -1,14 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; using OpenNest.Engine; using OpenNest.Engine.BestFit; using OpenNest.Engine.Fill; using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; namespace OpenNest { @@ -47,9 +47,17 @@ namespace OpenNest public virtual ShrinkAxis TrimAxis => ShrinkAxis.Width; - public virtual List BuildAngles(NestItem item, ClassificationResult classification, Box workArea) + public virtual List BuildAngles( + NestItem item, + ClassificationResult classification, + Box workArea + ) { - return new List { classification.PrimaryAngle, classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI }; + return new List + { + classification.PrimaryAngle, + classification.PrimaryAngle + OpenNest.Math.Angle.HalfPI, + }; } protected virtual void RecordProductiveAngles(List angleResults) { } @@ -58,28 +66,43 @@ namespace OpenNest // --- Virtual methods (side-effect-free, return parts) --- - public virtual List Fill(NestItem item, Box workArea, - IProgress progress, CancellationToken token) + public virtual List Fill( + NestItem item, + Box workArea, + IProgress progress, + CancellationToken token + ) { return new List(); } - public virtual List Fill(List groupParts, Box workArea, - IProgress progress, CancellationToken token) + public virtual List Fill( + List groupParts, + Box workArea, + IProgress progress, + CancellationToken token + ) { return new List(); } - public virtual List PackArea(Box box, List items, - IProgress progress, CancellationToken token) + public virtual List PackArea( + Box box, + List items, + IProgress progress, + CancellationToken token + ) { return new List(); } // --- Nest: multi-item strategy (virtual, side-effect-free) --- - public virtual List Nest(List items, - IProgress progress, CancellationToken token) + public virtual List Nest( + List items, + IProgress progress, + CancellationToken token + ) { if (items == null || items.Count == 0) return new List(); @@ -95,9 +118,7 @@ namespace OpenNest .ThenByDescending(i => i.Drawing.Area) .ToList(); - var packItems = items - .Where(i => !ShouldFill(i, plateArea)) - .ToList(); + var packItems = items.Where(i => !ShouldFill(i, plateArea)).ToList(); // Phase 1: Fill multi-quantity drawings using RemnantFiller. if (fillItems.Count > 0) @@ -117,12 +138,15 @@ namespace OpenNest foreach (var item in fillItems) { var placed = fillParts.Count(p => - ReferenceEquals(p.BaseDrawing, item.Drawing)); + ReferenceEquals(p.BaseDrawing, item.Drawing) + ); item.Quantity = System.Math.Max(0, item.Quantity - placed); } // Update workArea for pack phase - var placedObstacles = fillParts.Select(p => p.BoundingBox.Offset(Plate.PartSpacing)).ToList(); + var placedObstacles = fillParts + .Select(p => p.BoundingBox.Offset(Plate.PartSpacing)) + .ToList(); var finder = new RemnantFinder(workArea, placedObstacles); var remnants = finder.FindRemnants(); if (remnants.Count > 0) @@ -138,8 +162,12 @@ namespace OpenNest var pairItems = packItems.Where(i => i.Quantity == 2).ToList(); var regularPackItems = packItems.Where(i => i.Quantity != 2).ToList(); - if (regularPackItems.Count > 0 && workArea.Width > 0 && workArea.Length > 0 - && !token.IsCancellationRequested) + if ( + regularPackItems.Count > 0 + && workArea.Width > 0 + && workArea.Length > 0 + && !token.IsCancellationRequested + ) { var packParts = PackArea(workArea, regularPackItems, progress, token); @@ -151,7 +179,8 @@ namespace OpenNest foreach (var item in regularPackItems) { var placed = packParts.Count(p => - ReferenceEquals(p.BaseDrawing, item.Drawing)); + ReferenceEquals(p.BaseDrawing, item.Drawing) + ); item.Quantity = System.Math.Max(0, item.Quantity - placed); } } @@ -172,8 +201,12 @@ namespace OpenNest // --- FillExact (non-virtual, delegates to virtual Fill) --- - public List FillExact(NestItem item, Box workArea, - IProgress progress, CancellationToken token) + public List FillExact( + NestItem item, + Box workArea, + IProgress progress, + CancellationToken token + ) { return Fill(item, workArea, progress, token); } @@ -226,8 +259,7 @@ namespace OpenNest // --- Protected utilities --- - internal static void ReportProgress( - IProgress progress, ProgressReport report) + internal static void ReportProgress(IProgress progress, ProgressReport report) { if (progress == null || report.Parts == null || report.Parts.Count == 0) return; @@ -236,18 +268,22 @@ namespace OpenNest foreach (var part in report.Parts) clonedParts.Add((Part)part.Clone()); - Debug.WriteLine($"[Progress] Phase={report.Phase}, Plate={report.PlateNumber}, " + - $"Parts={clonedParts.Count} | {report.Description}"); + Debug.WriteLine( + $"[Progress] Phase={report.Phase}, Plate={report.PlateNumber}, " + + $"Parts={clonedParts.Count} | {report.Description}" + ); - progress.Report(new NestProgress - { - Phase = report.Phase, - PlateNumber = report.PlateNumber, - BestParts = clonedParts, - Description = report.Description, - ActiveWorkArea = report.WorkArea, - IsOverallBest = report.IsOverallBest, - }); + progress.Report( + new NestProgress + { + Phase = report.Phase, + PlateNumber = report.PlateNumber, + BestParts = clonedParts, + Description = report.Description, + ActiveWorkArea = report.WorkArea, + IsOverallBest = report.IsOverallBest, + } + ); } protected string BuildProgressSummary() @@ -263,14 +299,20 @@ namespace OpenNest return string.Join(" | ", parts); } - protected bool IsBetterFill(List candidate, List current, Box workArea) - => Comparer.IsBetter(candidate, current, workArea); + protected bool IsBetterFill(List candidate, List current, Box workArea) => + Comparer.IsBetter(candidate, current, workArea); protected bool IsBetterValidFill(List candidate, List current, Box workArea) { - if (candidate != null && candidate.Count > 0 && HasOverlaps(candidate, Plate.PartSpacing)) + if ( + candidate != null + && candidate.Count > 0 + && HasOverlaps(candidate, Plate.PartSpacing) + ) { - Debug.WriteLine($"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})"); + Debug.WriteLine( + $"[IsBetterValidFill] REJECTED {candidate.Count} parts due to overlaps (current best: {current?.Count ?? 0})" + ); return false; } @@ -290,10 +332,12 @@ namespace OpenNest { var box2 = parts[j].BoundingBox; - var overlapX = System.Math.Min(box1.Right, box2.Right) - - System.Math.Max(box1.Left, box2.Left); - var overlapY = System.Math.Min(box1.Top, box2.Top) - - System.Math.Max(box1.Bottom, box2.Bottom); + var overlapX = + System.Math.Min(box1.Right, box2.Right) + - System.Math.Max(box1.Left, box2.Left); + var overlapY = + System.Math.Min(box1.Top, box2.Top) + - System.Math.Max(box1.Bottom, box2.Bottom); if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon) continue; @@ -304,9 +348,11 @@ namespace OpenNest { var b1 = parts[i].BoundingBox; var b2 = parts[j].BoundingBox; - Debug.WriteLine($"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" + - $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" + - $" intersections={pts?.Count ?? 0}"); + Debug.WriteLine( + $"[HasOverlaps] Overlap: part[{i}] ({parts[i].BaseDrawing?.Name}) @ ({b1.Left:F2},{b1.Bottom:F2})-({b1.Right:F2},{b1.Top:F2}) rot={parts[i].Rotation:F2}" + + $" vs part[{j}] ({parts[j].BaseDrawing?.Name}) @ ({b2.Left:F2},{b2.Bottom:F2})-({b2.Right:F2},{b2.Top:F2}) rot={parts[j].Rotation:F2}" + + $" intersections={pts?.Count ?? 0}" + ); return true; } } @@ -319,8 +365,11 @@ namespace OpenNest /// Places best-fit pairs for qty=2 items into remnant spaces around /// already-placed parts. Returns all placed pair parts. /// - private List PlaceBestFitPairs(List pairItems, - List existingParts, Box fullWorkArea) + private List PlaceBestFitPairs( + List pairItems, + List existingParts, + Box fullWorkArea + ) { var result = new List(); var obstacles = existingParts @@ -330,10 +379,15 @@ namespace OpenNest foreach (var item in pairItems) { - if (item.Quantity < 2) continue; + if (item.Quantity < 2) + continue; var bestFits = BestFitCache.GetOrCompute( - item.Drawing, Plate.Size.Length, Plate.Size.Width, Plate.PartSpacing); + item.Drawing, + Plate.Size.Length, + Plate.Size.Width, + Plate.PartSpacing + ); // BestFitCache stores pair coordinates in canonical frame. Build candidates // from a canonical drawing copy so geometry and coords share a frame; rebind @@ -359,8 +413,10 @@ namespace OpenNest foreach (var r in remnants) { - if (pairW <= r.Width + Tolerance.Epsilon && - pairL <= r.Length + Tolerance.Epsilon) + if ( + pairW <= r.Width + Tolerance.Epsilon + && pairL <= r.Length + Tolerance.Epsilon + ) { var offset = r.Location - pairBbox.Location; foreach (var p in parts) @@ -379,7 +435,8 @@ namespace OpenNest } } - if (bestPlacement == null) continue; + if (bestPlacement == null) + continue; // Rebind to the original drawing and compose sourceAngle onto rotation so the // final placed parts sit in the user's visible frame. @@ -391,9 +448,11 @@ namespace OpenNest var envelope = ((IEnumerable)bestPlacement).GetBoundingBox(); finder.AddObstacle(envelope.Offset(Plate.PartSpacing)); - Debug.WriteLine($"[Nest] Placed best-fit pair for {item.Drawing.Name} " + - $"at ({bestTarget.X:F1},{bestTarget.Y:F1}), " + - $"size {envelope.Width:F1}x{envelope.Length:F1}"); + Debug.WriteLine( + $"[Nest] Placed best-fit pair for {item.Drawing.Name} " + + $"at ({bestTarget.X:F1},{bestTarget.Y:F1}), " + + $"size {envelope.Width:F1}x{envelope.Length:F1}" + ); } return result; @@ -405,7 +464,11 @@ namespace OpenNest /// the returned list is in the original drawing's visible frame. Mirrors /// DefaultNestEngine.RebindAndUnCanonicalize. /// - private static List RebindPairToOriginal(List parts, Drawing original, double sourceAngle) + private static List RebindPairToOriginal( + List parts, + Drawing original, + double sourceAngle + ) { if (parts == null || parts.Count == 0) return parts; @@ -444,6 +507,5 @@ namespace OpenNest // packing produces better results than grid-filling. return totalArea >= plateArea * 0.1; } - } } diff --git a/OpenNest.Engine/NestEngineRegistry.cs b/OpenNest.Engine/NestEngineRegistry.cs index 67ca728..b6b1b40 100644 --- a/OpenNest.Engine/NestEngineRegistry.cs +++ b/OpenNest.Engine/NestEngineRegistry.cs @@ -13,21 +13,29 @@ namespace OpenNest static NestEngineRegistry() { - Register("Default", + Register( + "Default", "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)", - plate => new DefaultNestEngine(plate)); + plate => new DefaultNestEngine(plate) + ); - Register("Strip", + Register( + "Strip", "Strip-based nesting for mixed-drawing layouts", - plate => new StripNestEngine(plate)); + plate => new StripNestEngine(plate) + ); - Register("Vertical Remnant", + Register( + "Vertical Remnant", "Optimizes for largest right-side vertical drop", - plate => new VerticalRemnantEngine(plate)); + plate => new VerticalRemnantEngine(plate) + ); - Register("Horizontal Remnant", + Register( + "Horizontal Remnant", "Optimizes for largest top-side horizontal drop", - plate => new HorizontalRemnantEngine(plate)); + plate => new HorizontalRemnantEngine(plate) + ); } public static IReadOnlyList AvailableEngines => engines; @@ -37,18 +45,25 @@ namespace OpenNest public static NestEngineBase Create(Plate plate) { var info = engines.FirstOrDefault(e => - e.Name.Equals(ActiveEngineName, StringComparison.OrdinalIgnoreCase)); + e.Name.Equals(ActiveEngineName, StringComparison.OrdinalIgnoreCase) + ); if (info == null) { - Debug.WriteLine($"[NestEngineRegistry] Engine '{ActiveEngineName}' not found, falling back to Default"); + Debug.WriteLine( + $"[NestEngineRegistry] Engine '{ActiveEngineName}' not found, falling back to Default" + ); info = engines[0]; } return info.Factory(plate); } - public static void Register(string name, string description, Func factory) + public static void Register( + string name, + string description, + Func factory + ) { if (engines.Any(e => e.Name.Equals(name, StringComparison.OrdinalIgnoreCase))) { @@ -79,7 +94,9 @@ namespace OpenNest if (ctor == null) { - Debug.WriteLine($"[NestEngineRegistry] Skipping {type.Name}: no Plate constructor"); + Debug.WriteLine( + $"[NestEngineRegistry] Skipping {type.Name}: no Plate constructor" + ); continue; } @@ -88,19 +105,28 @@ namespace OpenNest { var tempPlate = new Plate(); var instance = (NestEngineBase)ctor.Invoke(new object[] { tempPlate }); - Register(instance.Name, instance.Description, - plate => (NestEngineBase)ctor.Invoke(new object[] { plate })); - Debug.WriteLine($"[NestEngineRegistry] Loaded plugin engine: {instance.Name}"); + Register( + instance.Name, + instance.Description, + plate => (NestEngineBase)ctor.Invoke(new object[] { plate }) + ); + Debug.WriteLine( + $"[NestEngineRegistry] Loaded plugin engine: {instance.Name}" + ); } catch (Exception ex) { - Debug.WriteLine($"[NestEngineRegistry] Failed to instantiate {type.Name}: {ex.Message}"); + Debug.WriteLine( + $"[NestEngineRegistry] Failed to instantiate {type.Name}: {ex.Message}" + ); } } } catch (Exception ex) { - Debug.WriteLine($"[NestEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}"); + Debug.WriteLine( + $"[NestEngineRegistry] Failed to load assembly {Path.GetFileName(dll)}: {ex.Message}" + ); } } } diff --git a/OpenNest.Engine/NestProgress.cs b/OpenNest.Engine/NestProgress.cs index ce68b83..64eced2 100644 --- a/OpenNest.Engine/NestProgress.cs +++ b/OpenNest.Engine/NestProgress.cs @@ -1,9 +1,9 @@ -using OpenNest.Geometry; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; +using OpenNest.Geometry; namespace OpenNest { @@ -15,12 +15,23 @@ namespace OpenNest public enum NestPhase { - [Description("Trying rotations..."), ShortName("Linear")] Linear, - [Description("Trying best fit..."), ShortName("BestFit")] RectBestFit, - [Description("Trying pairs..."), ShortName("Pairs")] Pairs, - [Description("Trying NFP..."), ShortName("NFP")] Nfp, - [Description("Trying extents..."), ShortName("Extents")] Extents, - [Description("Custom"), ShortName("Custom")] Custom + [Description("Trying rotations..."), ShortName("Linear")] + Linear, + + [Description("Trying best fit..."), ShortName("BestFit")] + RectBestFit, + + [Description("Trying pairs..."), ShortName("Pairs")] + Pairs, + + [Description("Trying NFP..."), ShortName("NFP")] + Nfp, + + [Description("Trying extents..."), ShortName("Extents")] + Extents, + + [Description("Custom"), ShortName("Custom")] + Custom, } public static class NestPhaseExtensions @@ -30,22 +41,28 @@ namespace OpenNest public static string DisplayName(this NestPhase phase) { - return DisplayNames.GetOrAdd(phase, p => - { - var field = typeof(NestPhase).GetField(p.ToString()); - var attr = field?.GetCustomAttribute(); - return attr?.Description ?? p.ToString(); - }); + return DisplayNames.GetOrAdd( + phase, + p => + { + var field = typeof(NestPhase).GetField(p.ToString()); + var attr = field?.GetCustomAttribute(); + return attr?.Description ?? p.ToString(); + } + ); } public static string ShortName(this NestPhase phase) { - return ShortNames.GetOrAdd(phase, p => - { - var field = typeof(NestPhase).GetField(p.ToString()); - var attr = field?.GetCustomAttribute(); - return attr?.Name ?? p.ToString(); - }); + return ShortNames.GetOrAdd( + phase, + p => + { + var field = typeof(NestPhase).GetField(p.ToString()); + var attr = field?.GetCustomAttribute(); + return attr?.Name ?? p.ToString(); + } + ); } } @@ -89,7 +106,11 @@ namespace OpenNest public List BestParts { get => bestParts; - set { bestParts = value; cachedParts = null; } + set + { + bestParts = value; + cachedParts = null; + } } public string Description { get; set; } @@ -104,7 +125,8 @@ namespace OpenNest private void EnsureCache() { - if (cachedParts == bestParts) return; + if (cachedParts == bestParts) + return; cachedParts = bestParts; if (bestParts == null || bestParts.Count == 0) { @@ -122,7 +144,8 @@ namespace OpenNest { get { - if (BestParts == null || BestParts.Count == 0) return 0; + if (BestParts == null || BestParts.Count == 0) + return 0; EnsureCache(); var bboxArea = cachedBounds.Width * cachedBounds.Length; return bboxArea > 0 ? cachedPartArea / bboxArea : 0; @@ -133,7 +156,8 @@ namespace OpenNest { get { - if (BestParts == null || BestParts.Count == 0) return 0; + if (BestParts == null || BestParts.Count == 0) + return 0; EnsureCache(); return cachedBounds.Width; } @@ -143,7 +167,8 @@ namespace OpenNest { get { - if (BestParts == null || BestParts.Count == 0) return 0; + if (BestParts == null || BestParts.Count == 0) + return 0; EnsureCache(); return cachedBounds.Length; } @@ -153,7 +178,8 @@ namespace OpenNest { get { - if (BestParts == null || BestParts.Count == 0) return 0; + if (BestParts == null || BestParts.Count == 0) + return 0; EnsureCache(); return cachedPartArea; } diff --git a/OpenNest.Engine/Nfp/AutoNester.cs b/OpenNest.Engine/Nfp/AutoNester.cs index af371a1..554403b 100644 --- a/OpenNest.Engine/Nfp/AutoNester.cs +++ b/OpenNest.Engine/Nfp/AutoNester.cs @@ -1,11 +1,11 @@ -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.Nfp { @@ -15,9 +15,12 @@ namespace OpenNest.Engine.Nfp /// public static class AutoNester { - public static List Nest(List items, Plate plate, + public static List Nest( + List items, + Plate plate, IProgress progress = null, - CancellationToken cancellation = default) + CancellationToken cancellation = default + ) { var workArea = plate.WorkArea(); var halfSpacing = plate.PartSpacing / 2.0; @@ -36,7 +39,9 @@ namespace OpenNest.Engine.Nfp if (perimeterPolygon == null) { - Debug.WriteLine($"[AutoNest] Skipping drawing '{drawing.Name}': no valid perimeter"); + Debug.WriteLine( + $"[AutoNest] Skipping drawing '{drawing.Name}': no valid perimeter" + ); continue; } @@ -58,11 +63,20 @@ namespace OpenNest.Engine.Nfp // Pre-compute all NFPs. nfpCache.PreComputeAll(); - Debug.WriteLine($"[AutoNest] NFP cache: {nfpCache.Count} entries for {candidateRotations.Count} drawings"); + Debug.WriteLine( + $"[AutoNest] NFP cache: {nfpCache.Count} entries for {candidateRotations.Count} drawings" + ); // Run simulated annealing optimizer. var optimizer = new SimulatedAnnealing(); - var result = optimizer.Optimize(items, workArea, nfpCache, candidateRotations, progress, cancellation); + var result = optimizer.Optimize( + items, + workArea, + nfpCache, + candidateRotations, + progress, + cancellation + ); if (result.Sequence == null || result.Sequence.Count == 0) return new List(); @@ -72,17 +86,22 @@ namespace OpenNest.Engine.Nfp var placedParts = blf.Fill(result.Sequence); var parts = BottomLeftFill.ToNestParts(placedParts); - Debug.WriteLine($"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations"); + Debug.WriteLine( + $"[AutoNest] Result: {parts.Count} parts placed, {result.Iterations} SA iterations" + ); - NestEngineBase.ReportProgress(progress, new ProgressReport - { - Phase = NestPhase.Nfp, - PlateNumber = 0, - Parts = parts, - WorkArea = workArea, - Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations", - IsOverallBest = true, - }); + NestEngineBase.ReportProgress( + progress, + new ProgressReport + { + Phase = NestPhase.Nfp, + PlateNumber = 0, + Parts = parts, + WorkArea = workArea, + Description = $"NFP: {parts.Count} parts, {result.Iterations} iterations", + IsOverallBest = true, + } + ); return parts; } @@ -147,7 +166,9 @@ namespace OpenNest.Engine.Nfp // Only use the NFP result if it kept all parts and improved density. if (optimized.Count < parts.Count) { - Debug.WriteLine($"[AutoNest.Optimize] Rejected: placed {optimized.Count}/{parts.Count} parts"); + Debug.WriteLine( + $"[AutoNest.Optimize] Rejected: placed {optimized.Count}/{parts.Count} parts" + ); return parts; } @@ -163,24 +184,32 @@ namespace OpenNest.Engine.Nfp if (optimizedScore > originalScore) { - Debug.WriteLine($"[AutoNest.Optimize] Improved: density {originalScore.Density:P1} -> {optimizedScore.Density:P1}"); + Debug.WriteLine( + $"[AutoNest.Optimize] Improved: density {originalScore.Density:P1} -> {optimizedScore.Density:P1}" + ); return optimized; } - Debug.WriteLine($"[AutoNest.Optimize] No improvement: {originalScore.Density:P1} >= {optimizedScore.Density:P1}"); + Debug.WriteLine( + $"[AutoNest.Optimize] No improvement: {originalScore.Density:P1} >= {optimizedScore.Density:P1}" + ); return parts; } private static bool AllPartsInBounds(List parts, Box workArea) { var logPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"); + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "nest-debug.log" + ); var allInBounds = true; // Append to the log that BLF already started using var log = new StreamWriter(logPath, true); - log.WriteLine($"\n[Bounds] workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}"); + log.WriteLine( + $"\n[Bounds] workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}" + ); foreach (var part in parts) { @@ -193,7 +222,9 @@ namespace OpenNest.Engine.Nfp if (oob) { - log.WriteLine($"[Bounds] OOB DrawingId={part.BaseDrawing.Id} \"{part.BaseDrawing.Name}\" loc=({part.Location.X:F4},{part.Location.Y:F4}) rot={part.Rotation:F3} bb=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4}) violations: {(outLeft ? "LEFT " : "")}{(outBottom ? "BOTTOM " : "")}{(outRight ? "RIGHT " : "")}{(outTop ? "TOP " : "")}"); + log.WriteLine( + $"[Bounds] OOB DrawingId={part.BaseDrawing.Id} \"{part.BaseDrawing.Name}\" loc=({part.Location.X:F4},{part.Location.Y:F4}) rot={part.Rotation:F3} bb=({bb.Left:F4},{bb.Bottom:F4})-({bb.Right:F4},{bb.Top:F4}) violations: {(outLeft ? "LEFT " : "")}{(outBottom ? "BOTTOM " : "")}{(outRight ? "RIGHT " : "")}{(outTop ? "TOP " : "")}" + ); allInBounds = false; } } @@ -215,8 +246,11 @@ namespace OpenNest.Engine.Nfp /// /// Computes candidate rotation angles for a drawing. /// - private static List ComputeCandidateRotations(NestItem item, - Polygon perimeterPolygon, Box workArea) + private static List ComputeCandidateRotations( + NestItem item, + Polygon perimeterPolygon, + Box workArea + ) { var rotations = new List { 0 }; diff --git a/OpenNest.Engine/Nfp/BottomLeftFill.cs b/OpenNest.Engine/Nfp/BottomLeftFill.cs index 92d4b58..3f55fd5 100644 --- a/OpenNest.Engine/Nfp/BottomLeftFill.cs +++ b/OpenNest.Engine/Nfp/BottomLeftFill.cs @@ -1,8 +1,8 @@ -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.IO; using Clipper2Lib; +using OpenNest.Geometry; namespace OpenNest.Engine.Nfp { @@ -14,7 +14,9 @@ namespace OpenNest.Engine.Nfp public class BottomLeftFill { private static readonly string DebugLogPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "nest-debug.log"); + Environment.GetFolderPath(Environment.SpecialFolder.Desktop), + "nest-debug.log" + ); private readonly Box workArea; private readonly NfpCache nfpCache; @@ -34,7 +36,9 @@ namespace OpenNest.Engine.Nfp var placedParts = new List(); using var log = new StreamWriter(DebugLogPath, false); - log.WriteLine($"[BLF] {DateTime.Now:HH:mm:ss.fff} workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}"); + log.WriteLine( + $"[BLF] {DateTime.Now:HH:mm:ss.fff} workArea: X={workArea.X} Y={workArea.Y} W={workArea.Width} H={workArea.Length} Right={workArea.Right} Top={workArea.Top}" + ); log.WriteLine($"[BLF] Sequence count: {sequence.Count}"); foreach (var entry in sequence) @@ -43,13 +47,22 @@ namespace OpenNest.Engine.Nfp if (ifp.Vertices.Count < 3) { - log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)"); + log.WriteLine( + $"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} SKIPPED (IFP has {ifp.Vertices.Count} verts)" + ); continue; } - log.WriteLine($"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} IFP verts={ifp.Vertices.Count} bounds=({ifp.BoundingBox.X:F2},{ifp.BoundingBox.Y:F2},{ifp.BoundingBox.Width:F2},{ifp.BoundingBox.Length:F2})"); + log.WriteLine( + $"[BLF] DrawingId={entry.DrawingId} rot={entry.Rotation:F3} IFP verts={ifp.Vertices.Count} bounds=({ifp.BoundingBox.X:F2},{ifp.BoundingBox.Y:F2},{ifp.BoundingBox.Width:F2},{ifp.BoundingBox.Length:F2})" + ); - var nfpPaths = ComputeNfpPaths(placedParts, entry.DrawingId, entry.Rotation, ifp.BoundingBox); + var nfpPaths = ComputeNfpPaths( + placedParts, + entry.DrawingId, + entry.Rotation, + ifp.BoundingBox + ); var feasible = InnerFitPolygon.ComputeFeasibleRegion(ifp, nfpPaths); var point = InnerFitPolygon.FindBottomLeftPoint(feasible); @@ -63,17 +76,22 @@ namespace OpenNest.Engine.Nfp var ifpBb = ifp.BoundingBox; point = new Vector( System.Math.Max(ifpBb.X, System.Math.Min(ifpBb.Right, point.X)), - System.Math.Max(ifpBb.Y, System.Math.Min(ifpBb.Top, point.Y))); + System.Math.Max(ifpBb.Y, System.Math.Min(ifpBb.Top, point.Y)) + ); - log.WriteLine($"[BLF] -> placed at ({point.X:F4}, {point.Y:F4}) nfpPaths={nfpPaths.Count} feasibleVerts={feasible.Vertices.Count}"); + log.WriteLine( + $"[BLF] -> placed at ({point.X:F4}, {point.Y:F4}) nfpPaths={nfpPaths.Count} feasibleVerts={feasible.Vertices.Count}" + ); - placedParts.Add(new PlacedPart - { - DrawingId = entry.DrawingId, - Rotation = entry.Rotation, - Position = point, - Drawing = entry.Drawing - }); + placedParts.Add( + new PlacedPart + { + DrawingId = entry.DrawingId, + Rotation = entry.Rotation, + Position = point, + Drawing = entry.Drawing, + } + ); } log.WriteLine($"[BLF] Total placed: {placedParts.Count}/{sequence.Count}"); @@ -106,7 +124,12 @@ namespace OpenNest.Engine.Nfp /// returned as Clipper paths with translations applied. /// Filters NFPs that don't intersect the target IFP. /// - private PathsD ComputeNfpPaths(List placedParts, int drawingId, double rotation, Box ifpBounds) + private PathsD ComputeNfpPaths( + List placedParts, + int drawingId, + double rotation, + Box ifpBounds + ) { var nfpPaths = new PathsD(placedParts.Count); diff --git a/OpenNest.Engine/Nfp/INestOptimizer.cs b/OpenNest.Engine/Nfp/INestOptimizer.cs index 2664d98..ec9c75d 100644 --- a/OpenNest.Engine/Nfp/INestOptimizer.cs +++ b/OpenNest.Engine/Nfp/INestOptimizer.cs @@ -1,8 +1,8 @@ -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Threading; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Engine.Nfp { @@ -33,9 +33,13 @@ namespace OpenNest.Engine.Nfp /// public interface INestOptimizer { - OptimizationResult Optimize(List items, Box workArea, NfpCache cache, + OptimizationResult Optimize( + List items, + Box workArea, + NfpCache cache, Dictionary> candidateRotations, IProgress progress = null, - CancellationToken cancellation = default); + CancellationToken cancellation = default + ); } } diff --git a/OpenNest.Engine/Nfp/NfpCache.cs b/OpenNest.Engine/Nfp/NfpCache.cs index 8f6a553..8038473 100644 --- a/OpenNest.Engine/Nfp/NfpCache.cs +++ b/OpenNest.Engine/Nfp/NfpCache.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; using System; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.Nfp { @@ -12,10 +12,10 @@ namespace OpenNest.Engine.Nfp public class NfpCache { private readonly Dictionary cache = new Dictionary(); - private readonly Dictionary> polygonCache - = new Dictionary>(); - private readonly Dictionary<(int drawingId, double rotation), Polygon> ifpCache - = new Dictionary<(int drawingId, double rotation), Polygon>(); + private readonly Dictionary> polygonCache = + new Dictionary>(); + private readonly Dictionary<(int drawingId, double rotation), Polygon> ifpCache = + new Dictionary<(int drawingId, double rotation), Polygon>(); /// /// Registers a pre-computed polygon for a drawing at a specific rotation. @@ -107,8 +107,12 @@ namespace OpenNest.Engine.Nfp { for (var j = 0; j < entries.Count; j++) { - Get(entries[i].drawingId, entries[i].rotation, - entries[j].drawingId, entries[j].rotation); + Get( + entries[i].drawingId, + entries[i].rotation, + entries[j].drawingId, + entries[j].rotation + ); } } } @@ -136,9 +140,9 @@ namespace OpenNest.Engine.Nfp public bool Equals(NfpKey other) { return DrawingIdA == other.DrawingIdA - && RotationA == other.RotationA - && DrawingIdB == other.DrawingIdB - && RotationB == other.RotationB; + && RotationA == other.RotationA + && DrawingIdB == other.DrawingIdB + && RotationB == other.RotationB; } public override bool Equals(object obj) => obj is NfpKey key && Equals(key); diff --git a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs index 8f93721..f28576c 100644 --- a/OpenNest.Engine/Nfp/SimulatedAnnealing.cs +++ b/OpenNest.Engine/Nfp/SimulatedAnnealing.cs @@ -1,10 +1,10 @@ -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Engine.Nfp { @@ -18,10 +18,14 @@ namespace OpenNest.Engine.Nfp private const double DefaultMinTemperature = 0.1; private const int DefaultMaxNoImprovement = 500; - public OptimizationResult Optimize(List items, Box workArea, NfpCache cache, + public OptimizationResult Optimize( + List items, + Box workArea, + NfpCache cache, Dictionary> candidateRotations, IProgress progress = null, - CancellationToken cancellation = default) + CancellationToken cancellation = default + ) { var random = new Random(); @@ -30,7 +34,12 @@ namespace OpenNest.Engine.Nfp var sequence = BuildInitialSequence(items, candidateRotations); if (sequence.Count == 0) - return new OptimizationResult { Sequence = sequence, Score = default, Iterations = 0 }; + return new OptimizationResult + { + Sequence = sequence, + Score = default, + Iterations = 0, + }; // Evaluate initial solution. var blf = new BottomLeftFill(workArea, cache); @@ -42,20 +51,33 @@ namespace OpenNest.Engine.Nfp var currentScore = bestScore; // Calibrate initial temperature so ~80% of worse moves are accepted. - var initialTemp = CalibrateTemperature(currentSequence, workArea, cache, - candidateRotations, random); + var initialTemp = CalibrateTemperature( + currentSequence, + workArea, + cache, + candidateRotations, + random + ); var temperature = initialTemp; var noImprovement = 0; var iteration = 0; - Debug.WriteLine($"[SA] Initial: {bestScore.Count} parts, density={bestScore.Density:P1}, temp={initialTemp:F2}"); + Debug.WriteLine( + $"[SA] Initial: {bestScore.Count} parts, density={bestScore.Density:P1}, temp={initialTemp:F2}" + ); - ReportBest(progress, BottomLeftFill.ToNestParts(bestPlaced), workArea, - $"NFP: initial {bestScore.Count} parts, density={bestScore.Density:P1}"); + ReportBest( + progress, + BottomLeftFill.ToNestParts(bestPlaced), + workArea, + $"NFP: initial {bestScore.Count} parts, density={bestScore.Density:P1}" + ); - while (temperature > DefaultMinTemperature - && noImprovement < DefaultMaxNoImprovement - && !cancellation.IsCancellationRequested) + while ( + temperature > DefaultMinTemperature + && noImprovement < DefaultMaxNoImprovement + && !cancellation.IsCancellationRequested + ) { iteration++; @@ -63,7 +85,10 @@ namespace OpenNest.Engine.Nfp Mutate(candidate, candidateRotations, random); var candidatePlaced = blf.Fill(candidate); - var candidateScore = FillScore.Compute(BottomLeftFill.ToNestParts(candidatePlaced), workArea); + var candidateScore = FillScore.Compute( + BottomLeftFill.ToNestParts(candidatePlaced), + workArea + ); var delta = candidateScore.CompareTo(currentScore); @@ -79,10 +104,16 @@ namespace OpenNest.Engine.Nfp bestSequence = new List(currentSequence); noImprovement = 0; - Debug.WriteLine($"[SA] New best at iter {iteration}: {bestScore.Count} parts, density={bestScore.Density:P1}"); + Debug.WriteLine( + $"[SA] New best at iter {iteration}: {bestScore.Count} parts, density={bestScore.Density:P1}" + ); - ReportBest(progress, BottomLeftFill.ToNestParts(candidatePlaced), workArea, - $"NFP: iter {iteration}, {bestScore.Count} parts, density={bestScore.Density:P1}"); + ReportBest( + progress, + BottomLeftFill.ToNestParts(candidatePlaced), + workArea, + $"NFP: iter {iteration}, {bestScore.Count} parts, density={bestScore.Density:P1}" + ); } else { @@ -111,13 +142,15 @@ namespace OpenNest.Engine.Nfp temperature *= DefaultCoolingRate; } - Debug.WriteLine($"[SA] Done: {iteration} iters, best={bestScore.Count} parts, density={bestScore.Density:P1}"); + Debug.WriteLine( + $"[SA] Done: {iteration} iters, best={bestScore.Count} parts, density={bestScore.Density:P1}" + ); return new OptimizationResult { Sequence = bestSequence, Score = bestScore, - Iterations = iteration + Iterations = iteration, }; } @@ -126,7 +159,9 @@ namespace OpenNest.Engine.Nfp /// Each NestItem is expanded by its quantity. /// private static List BuildInitialSequence( - List items, Dictionary> candidateRotations) + List items, + Dictionary> candidateRotations + ) { var sequence = new List(); @@ -138,7 +173,10 @@ namespace OpenNest.Engine.Nfp var qty = item.Quantity > 0 ? item.Quantity : 1; var rotation = 0.0; - if (candidateRotations.TryGetValue(item.Drawing.Id, out var rotations) && rotations.Count > 0) + if ( + candidateRotations.TryGetValue(item.Drawing.Id, out var rotations) + && rotations.Count > 0 + ) rotation = rotations[0]; for (var i = 0; i < qty; i++) @@ -151,8 +189,11 @@ namespace OpenNest.Engine.Nfp /// /// Applies a random mutation to the sequence. /// - private static void Mutate(List sequence, - Dictionary> candidateRotations, Random random) + private static void Mutate( + List sequence, + Dictionary> candidateRotations, + Random random + ) { if (sequence.Count < 2) return; @@ -190,13 +231,19 @@ namespace OpenNest.Engine.Nfp /// /// Changes a random part's rotation to another candidate angle. /// - private static void MutateRotate(List sequence, - Dictionary> candidateRotations, Random random) + private static void MutateRotate( + List sequence, + Dictionary> candidateRotations, + Random random + ) { var idx = random.Next(sequence.Count); var entry = sequence[idx]; - if (!candidateRotations.TryGetValue(entry.DrawingId, out var rotations) || rotations.Count <= 1) + if ( + !candidateRotations.TryGetValue(entry.DrawingId, out var rotations) + || rotations.Count <= 1 + ) return; var newRotation = rotations[random.Next(rotations.Count)]; @@ -229,8 +276,11 @@ namespace OpenNest.Engine.Nfp /// private static double CalibrateTemperature( List sequence, - Box workArea, NfpCache cache, - Dictionary> candidateRotations, Random random) + Box workArea, + NfpCache cache, + Dictionary> candidateRotations, + Random random + ) { const int samples = 20; var deltas = new List(); @@ -274,18 +324,25 @@ namespace OpenNest.Engine.Nfp return countDiff * 10.0 + densityDiff; } - private static void ReportBest(IProgress progress, List parts, - Box workArea, string description) + private static void ReportBest( + IProgress progress, + List parts, + Box workArea, + string description + ) { - NestEngineBase.ReportProgress(progress, new ProgressReport - { - Phase = NestPhase.Nfp, - PlateNumber = 0, - Parts = parts, - WorkArea = workArea, - Description = description, - IsOverallBest = true, - }); + NestEngineBase.ReportProgress( + progress, + new ProgressReport + { + Phase = NestPhase.Nfp, + PlateNumber = 0, + Parts = parts, + WorkArea = workArea, + Description = description, + IsOverallBest = true, + } + ); } } } diff --git a/OpenNest.Engine/PartClassifier.cs b/OpenNest.Engine/PartClassifier.cs index e0ceef7..9ae9cec 100644 --- a/OpenNest.Engine/PartClassifier.cs +++ b/OpenNest.Engine/PartClassifier.cs @@ -1,12 +1,17 @@ +using System.Collections.Generic; +using System.Linq; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; -using System.Linq; namespace OpenNest.Engine { - public enum PartType { Rectangle, Circle, Irregular } + public enum PartType + { + Rectangle, + Circle, + Irregular, + } public struct ClassificationResult { @@ -27,7 +32,8 @@ namespace OpenNest.Engine { var result = new ClassificationResult { Type = PartType.Irregular }; - var entities = ConvertProgram.ToGeometry(drawing.Program) + var entities = ConvertProgram + .ToGeometry(drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); @@ -72,7 +78,8 @@ namespace OpenNest.Engine // Circularity: 4*PI*area / perimeter^2. Circles ~ 1.0. if (drawingPerimeter > Tolerance.Epsilon) - result.Circularity = 4 * System.Math.PI * perimeterArea / (drawingPerimeter * drawingPerimeter); + result.Circularity = + 4 * System.Math.PI * perimeterArea / (drawingPerimeter * drawingPerimeter); // Check circle first (rotationally invariant). if (result.Circularity >= CircularityThreshold) @@ -90,8 +97,10 @@ namespace OpenNest.Engine result.PerimeterRatio = mbrPerimeter / drawingPerimeter; // Rectangle: both metrics pass thresholds. - if (result.Rectangularity >= RectangularityThreshold - && result.PerimeterRatio >= PerimeterRatioThreshold) + if ( + result.Rectangularity >= RectangularityThreshold + && result.PerimeterRatio >= PerimeterRatioThreshold + ) { result.Type = PartType.Rectangle; return result; diff --git a/OpenNest.Engine/PlateOptimizer.cs b/OpenNest.Engine/PlateOptimizer.cs index fd7be1c..5983818 100644 --- a/OpenNest.Engine/PlateOptimizer.cs +++ b/OpenNest.Engine/PlateOptimizer.cs @@ -1,12 +1,12 @@ -using OpenNest.Engine; -using OpenNest.Engine.BestFit; -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; +using OpenNest.Engine; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest { @@ -18,9 +18,15 @@ namespace OpenNest double salvageRate, Plate templatePlate, IProgress progress = null, - CancellationToken token = default) + CancellationToken token = default + ) { - if (items == null || items.Count == 0 || plateOptions == null || plateOptions.Count == 0) + if ( + items == null + || items.Count == 0 + || plateOptions == null + || plateOptions.Count == 0 + ) return null; // Find the minimum dimension needed to fit the largest part, @@ -29,20 +35,29 @@ namespace OpenNest var minPartLength = 0.0; foreach (var item in items) { - if (item.Quantity <= 0) continue; + if (item.Quantity <= 0) + continue; var bb = item.Drawing.Program.BoundingBox(); var shortSide = System.Math.Min(bb.Width, bb.Length); var longSide = System.Math.Max(bb.Width, bb.Length); - if (!plateOptions.Any(o => FitsPart(o, shortSide, longSide, templatePlate.EdgeSpacing))) + if ( + !plateOptions.Any(o => + FitsPart(o, shortSide, longSide, templatePlate.EdgeSpacing) + ) + ) { - Debug.WriteLine($"[PlateOptimizer] Skipping oversized item '{item.Drawing.Name}' " + - $"({shortSide:F1}x{longSide:F1}) — does not fit any plate option"); + Debug.WriteLine( + $"[PlateOptimizer] Skipping oversized item '{item.Drawing.Name}' " + + $"({shortSide:F1}x{longSide:F1}) — does not fit any plate option" + ); continue; } - if (shortSide > minPartWidth) minPartWidth = shortSide; - if (longSide > minPartLength) minPartLength = longSide; + if (shortSide > minPartWidth) + minPartWidth = shortSide; + if (longSide > minPartLength) + minPartLength = longSide; } // Sort candidates by cost ascending — try cheapest first. @@ -57,13 +72,12 @@ namespace OpenNest // Pre-compute best fits for all candidate plate sizes at once. // This runs the expensive GPU evaluation once on the largest plate // and filters the results for each smaller size. - var plateSizes = candidates - .Select(o => (Width: o.Length, Height: o.Width)) - .ToList(); + var plateSizes = candidates.Select(o => (Width: o.Length, Height: o.Width)).ToList(); foreach (var item in items) { - if (item.Quantity <= 0) continue; + if (item.Quantity <= 0) + continue; BestFitCache.ComputeForSizes(item.Drawing, templatePlate.PartSpacing, plateSizes); } @@ -74,7 +88,14 @@ namespace OpenNest if (token.IsCancellationRequested) break; - var result = TryPlateSize(option, items, salvageRate, templatePlate, progress, token); + var result = TryPlateSize( + option, + items, + salvageRate, + templatePlate, + progress, + token + ); if (result == null) continue; @@ -86,11 +107,16 @@ namespace OpenNest // remnant credit never offsets the extra plate cost, so skip. if (salvageRate < 1.0) { - var allPlaced = items.All(i => i.Quantity <= 0 || - result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) >= i.Quantity); + var allPlaced = items.All(i => + i.Quantity <= 0 + || result.Parts.Count(p => p.BaseDrawing.Name == i.Drawing.Name) + >= i.Quantity + ); if (allPlaced) { - Debug.WriteLine($"[PlateOptimizer] Early exit: {option.Width}x{option.Length} placed all items"); + Debug.WriteLine( + $"[PlateOptimizer] Early exit: {option.Width}x{option.Length} placed all items" + ); break; } } @@ -99,14 +125,21 @@ namespace OpenNest return best; } - private static bool FitsPart(PlateOption option, double minWidth, double minLength, Spacing edgeSpacing) + private static bool FitsPart( + PlateOption option, + double minWidth, + double minLength, + Spacing edgeSpacing + ) { var workW = option.Width - edgeSpacing.Left - edgeSpacing.Right; var workL = option.Length - edgeSpacing.Top - edgeSpacing.Bottom; // Part fits in either orientation. - var fitsNormal = workW >= minWidth - Tolerance.Epsilon && workL >= minLength - Tolerance.Epsilon; - var fitsRotated = workW >= minLength - Tolerance.Epsilon && workL >= minWidth - Tolerance.Epsilon; + var fitsNormal = + workW >= minWidth - Tolerance.Epsilon && workL >= minLength - Tolerance.Epsilon; + var fitsRotated = + workW >= minLength - Tolerance.Epsilon && workL >= minWidth - Tolerance.Epsilon; return fitsNormal || fitsRotated; } @@ -116,7 +149,8 @@ namespace OpenNest double salvageRate, Plate templatePlate, IProgress progress, - CancellationToken token) + CancellationToken token + ) { // Create a temporary plate with candidate size + settings from template. var tempPlate = new Plate(option.Width, option.Length) @@ -132,15 +166,17 @@ namespace OpenNest }; // Clone items so the dry run doesn't mutate originals. - var clonedItems = items.Select(i => new NestItem - { - Drawing = i.Drawing, // share Drawing reference for BestFitCache compatibility - Priority = i.Priority, - Quantity = i.Quantity, - StepAngle = i.StepAngle, - RotationStart = i.RotationStart, - RotationEnd = i.RotationEnd, - }).ToList(); + var clonedItems = items + .Select(i => new NestItem + { + Drawing = i.Drawing, // share Drawing reference for BestFitCache compatibility + Priority = i.Priority, + Quantity = i.Quantity, + StepAngle = i.StepAngle, + RotationStart = i.RotationStart, + RotationEnd = i.RotationEnd, + }) + .ToList(); var engine = NestEngineRegistry.Create(tempPlate); var parts = engine.Nest(clonedItems, progress, token); @@ -158,8 +194,10 @@ namespace OpenNest var costPerSqUnit = option.Cost / option.Area; var netCost = option.Cost - (remnantArea * costPerSqUnit * salvageRate); - Debug.WriteLine($"[PlateOptimizer] {option.Width}x{option.Length} ${option.Cost}: " + - $"{parts.Count} parts, util={partsArea / plateArea:P1}, net=${netCost:F2}"); + Debug.WriteLine( + $"[PlateOptimizer] {option.Width}x{option.Length} ${option.Cost}: " + + $"{parts.Count} parts, util={partsArea / plateArea:P1}, net=${netCost:F2}" + ); return new PlateOptimizerResult { @@ -172,7 +210,8 @@ namespace OpenNest private static bool IsBetter(PlateOptimizerResult candidate, PlateOptimizerResult current) { - if (current == null) return true; + if (current == null) + return true; // 1. More parts placed is always better. if (candidate.Parts.Count != current.Parts.Count) diff --git a/OpenNest.Engine/PlateProcessingResult.cs b/OpenNest.Engine/PlateProcessingResult.cs index 778001a..836e5ea 100644 --- a/OpenNest.Engine/PlateProcessingResult.cs +++ b/OpenNest.Engine/PlateProcessingResult.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Engine.RapidPlanning; -using System.Collections.Generic; namespace OpenNest.Engine { diff --git a/OpenNest.Engine/PlateProcessor.cs b/OpenNest.Engine/PlateProcessor.cs index 325968f..a32a6fb 100644 --- a/OpenNest.Engine/PlateProcessor.cs +++ b/OpenNest.Engine/PlateProcessor.cs @@ -1,10 +1,10 @@ +using System.Collections.Generic; +using System.Linq; using OpenNest.CNC; using OpenNest.CNC.CuttingStrategy; using OpenNest.Engine.RapidPlanning; using OpenNest.Engine.Sequencing; using OpenNest.Geometry; -using System.Collections.Generic; -using System.Linq; namespace OpenNest.Engine { @@ -61,7 +61,11 @@ namespace OpenNest.Engine if (i + 1 < sequenced.Count) { var nextStart = ToPartLocal(piercePoints[i + 1], part); - cuttingResult = CuttingStrategy.Apply(part.Program, localApproach, nextStart); + cuttingResult = CuttingStrategy.Apply( + part.Program, + localApproach, + nextStart + ); } else { @@ -82,12 +86,14 @@ namespace OpenNest.Engine var rapidPath = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas); - results.Add(new ProcessedPart - { - Part = part, - ProcessedProgram = processedProgram, - RapidPath = rapidPath - }); + results.Add( + new ProcessedPart + { + Part = part, + ProcessedProgram = processedProgram, + RapidPath = rapidPath, + } + ); var perimeter = GetPartPerimeter(part); if (perimeter != null) diff --git a/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs index 7784c48..a02f65c 100644 --- a/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs +++ b/OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.RapidPlanning { @@ -13,19 +13,11 @@ namespace OpenNest.Engine.RapidPlanning { if (TravelLineIntersectsShape(travelLine, cutArea)) { - return new RapidPath - { - HeadUp = true, - Waypoints = new List() - }; + return new RapidPath { HeadUp = true, Waypoints = new List() }; } } - return new RapidPath - { - HeadUp = false, - Waypoints = new List() - }; + return new RapidPath { HeadUp = false, Waypoints = new List() }; } private static bool TravelLineIntersectsShape(Line travelLine, Shape shape) diff --git a/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs index c33f36e..edae37c 100644 --- a/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs +++ b/OpenNest.Engine/RapidPlanning/IRapidPlanner.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.RapidPlanning { diff --git a/OpenNest.Engine/RapidPlanning/RapidPath.cs b/OpenNest.Engine/RapidPlanning/RapidPath.cs index f62b8d7..8ff6eb4 100644 --- a/OpenNest.Engine/RapidPlanning/RapidPath.cs +++ b/OpenNest.Engine/RapidPlanning/RapidPath.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.RapidPlanning { diff --git a/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs b/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs index c090224..2032c25 100644 --- a/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs +++ b/OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.Engine.RapidPlanning { @@ -7,11 +7,7 @@ namespace OpenNest.Engine.RapidPlanning { public RapidPath Plan(Vector from, Vector to, IReadOnlyList cutAreas) { - return new RapidPath - { - HeadUp = true, - Waypoints = new List() - }; + return new RapidPath { HeadUp = true, Waypoints = new List() }; } } } diff --git a/OpenNest.Engine/RectanglePacking/Bin.cs b/OpenNest.Engine/RectanglePacking/Bin.cs index 815913d..8688277 100644 --- a/OpenNest.Engine/RectanglePacking/Bin.cs +++ b/OpenNest.Engine/RectanglePacking/Bin.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; namespace OpenNest.RectanglePacking { @@ -24,7 +24,7 @@ namespace OpenNest.RectanglePacking { Location = this.Location, Size = this.Size, - Items = new List(Items) + Items = new List(Items), }; } } diff --git a/OpenNest.Engine/RectanglePacking/BinConverter.cs b/OpenNest.Engine/RectanglePacking/BinConverter.cs index 1b5c7d9..cf2c6fc 100644 --- a/OpenNest.Engine/RectanglePacking/BinConverter.cs +++ b/OpenNest.Engine/RectanglePacking/BinConverter.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.RectanglePacking { @@ -8,11 +8,7 @@ namespace OpenNest.RectanglePacking { public static Bin CreateBin(Box area, double partSpacing) { - var bin = new Bin - { - Location = area.Location, - Size = area.Size - }; + var bin = new Bin { Location = area.Location, Size = area.Size }; bin.Width += partSpacing; bin.Length += partSpacing; @@ -31,7 +27,7 @@ namespace OpenNest.RectanglePacking { Id = id, Location = box.Location, - Size = box.Size + Size = box.Size, }; } diff --git a/OpenNest.Engine/RectanglePacking/FillBestFit.cs b/OpenNest.Engine/RectanglePacking/FillBestFit.cs index 1917b85..fc851e2 100644 --- a/OpenNest.Engine/RectanglePacking/FillBestFit.cs +++ b/OpenNest.Engine/RectanglePacking/FillBestFit.cs @@ -1,14 +1,12 @@ -using OpenNest.Math; -using System; +using System; +using OpenNest.Math; namespace OpenNest.RectanglePacking { internal class FillBestFit : FillEngine { public FillBestFit(Bin bin) - : base(bin) - { - } + : base(bin) { } public override void Fill(Item item) { @@ -29,8 +27,7 @@ namespace OpenNest.RectanglePacking Bin.Items.AddRange(bin1.Items); else Bin.Items.AddRange(bin2.Items); - } - + } public override void Fill(Item item, int maxCount) { @@ -60,8 +57,10 @@ namespace OpenNest.RectanglePacking var normalPrimary = combo.Count1; var rotatePrimary = combo.Count2; - var normalSecondary = (int)System.Math.Floor((binSecondary + Tolerance.Epsilon) / secondarySize); - var rotateSecondary = (int)System.Math.Floor((binSecondary + Tolerance.Epsilon) / primarySize); + var normalSecondary = (int) + System.Math.Floor((binSecondary + Tolerance.Epsilon) / secondarySize); + var rotateSecondary = (int) + System.Math.Floor((binSecondary + Tolerance.Epsilon) / primarySize); var (normalRows, normalCols) = horizontal ? (normalSecondary, normalPrimary) diff --git a/OpenNest.Engine/RectanglePacking/FillEngine.cs b/OpenNest.Engine/RectanglePacking/FillEngine.cs index 857c44b..27900dd 100644 --- a/OpenNest.Engine/RectanglePacking/FillEngine.cs +++ b/OpenNest.Engine/RectanglePacking/FillEngine.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; +using OpenNest.Geometry; namespace OpenNest.RectanglePacking { @@ -16,7 +16,13 @@ namespace OpenNest.RectanglePacking public abstract void Fill(Item item, int maxCount); - protected List FillGrid(Item item, int rows, int columns, int maxCount, bool columnMajor = true) + protected List FillGrid( + Item item, + int rows, + int columns, + int maxCount, + bool columnMajor = true + ) { var items = new List(); diff --git a/OpenNest.Engine/RectanglePacking/FillNoRotation.cs b/OpenNest.Engine/RectanglePacking/FillNoRotation.cs index 1870be8..c76d60a 100644 --- a/OpenNest.Engine/RectanglePacking/FillNoRotation.cs +++ b/OpenNest.Engine/RectanglePacking/FillNoRotation.cs @@ -6,9 +6,7 @@ namespace OpenNest.RectanglePacking internal class FillNoRotation : FillEngine { public FillNoRotation(Bin bin) - : base(bin) - { - } + : base(bin) { } public NestDirection NestDirection { get; set; } @@ -59,7 +57,9 @@ namespace OpenNest.RectanglePacking columns = (int)System.Math.Ceiling((double)maxCount / rows); } - Bin.Items.AddRange(FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Length)); + Bin.Items.AddRange( + FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Length) + ); } } } diff --git a/OpenNest.Engine/RectanglePacking/FillSameRotation.cs b/OpenNest.Engine/RectanglePacking/FillSameRotation.cs index a7b5a64..c933b94 100644 --- a/OpenNest.Engine/RectanglePacking/FillSameRotation.cs +++ b/OpenNest.Engine/RectanglePacking/FillSameRotation.cs @@ -5,9 +5,7 @@ namespace OpenNest.RectanglePacking internal class FillSameRotation : FillEngine { public FillSameRotation(Bin bin) - : base(bin) - { - } + : base(bin) { } public override void Fill(Item item) { diff --git a/OpenNest.Engine/RectanglePacking/FillSpiral.cs b/OpenNest.Engine/RectanglePacking/FillSpiral.cs index 855295c..7f13b34 100644 --- a/OpenNest.Engine/RectanglePacking/FillSpiral.cs +++ b/OpenNest.Engine/RectanglePacking/FillSpiral.cs @@ -8,9 +8,7 @@ namespace OpenNest.RectanglePacking public Box CenterRemnant { get; private set; } public FillSpiral(Bin bin) - : base(bin) - { - } + : base(bin) { } public override void Fill(Item item) { @@ -19,7 +17,8 @@ namespace OpenNest.RectanglePacking public override void Fill(Item item, int maxCount) { - if (item == null) return; + if (item == null) + return; // Width = Y axis, Length = X axis var comboY = BestCombination.FindFrom2(item.Width, item.Length, Bin.Width); @@ -28,15 +27,13 @@ namespace OpenNest.RectanglePacking if (!comboY.Found || !comboX.Found) return; - var q14size = new Size( - item.Width * comboY.Count1, - item.Length * comboX.Count1); - var q23size = new Size( - item.Length * comboY.Count2, - item.Width * comboX.Count2); + var q14size = new Size(item.Width * comboY.Count1, item.Length * comboX.Count1); + var q23size = new Size(item.Length * comboY.Count2, item.Width * comboX.Count2); - if ((q14size.Width > q23size.Width && q14size.Length > q23size.Length) || - (q23size.Width > q14size.Width && q23size.Length > q14size.Length)) + if ( + (q14size.Width > q23size.Width && q14size.Length > q23size.Length) + || (q23size.Width > q14size.Width && q23size.Length > q14size.Length) + ) return; // cant do an efficient spiral fill // Q1: normal orientation at bin origin @@ -57,9 +54,7 @@ namespace OpenNest.RectanglePacking // Q4: normal orientation, diagonal from Q1 item.Rotate(); - item.Location = new Vector( - Bin.X + q23size.Length, - Bin.Y + q23size.Width); + item.Location = new Vector(Bin.X + q23size.Length, Bin.Y + q23size.Width); var q4 = FillGrid(item, comboY.Count1, comboX.Count1, maxCount); Bin.Items.AddRange(q4); @@ -69,14 +64,21 @@ namespace OpenNest.RectanglePacking var centerW = System.Math.Abs(q14size.Length - q23size.Length); var centerH = System.Math.Abs(q14size.Width - q23size.Width); - if (comboY.Count1 > 0 && comboY.Count2 > 0 && comboX.Count1 > 0 && comboX.Count2 > 0 - && centerW > Tolerance.Epsilon && centerH > Tolerance.Epsilon) + if ( + comboY.Count1 > 0 + && comboY.Count2 > 0 + && comboX.Count1 > 0 + && comboX.Count2 > 0 + && centerW > Tolerance.Epsilon + && centerH > Tolerance.Epsilon + ) { CenterRemnant = new Box( Bin.X + System.Math.Min(q14size.Length, q23size.Length), Bin.Y + System.Math.Min(q14size.Width, q23size.Width), centerW, - centerH); + centerH + ); } } } diff --git a/OpenNest.Engine/RectanglePacking/Item.cs b/OpenNest.Engine/RectanglePacking/Item.cs index d5ff03d..4d1a94e 100644 --- a/OpenNest.Engine/RectanglePacking/Item.cs +++ b/OpenNest.Engine/RectanglePacking/Item.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; +using System.Collections.Generic; +using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.RectanglePacking { @@ -23,7 +23,7 @@ namespace OpenNest.RectanglePacking IsRotated = this.IsRotated, Location = this.Location, Size = this.Size, - Id = this.Id + Id = this.Id, }; } } @@ -42,10 +42,14 @@ namespace OpenNest.RectanglePacking foreach (var box in items) { - if (box.Left < minX) minX = box.Left; - if (box.Right > maxX) maxX = box.Right; - if (box.Bottom < minY) minY = box.Bottom; - if (box.Top > maxY) maxY = box.Top; + if (box.Left < minX) + minX = box.Left; + if (box.Right > maxX) + maxX = box.Right; + if (box.Bottom < minY) + minY = box.Bottom; + if (box.Top > maxY) + maxY = box.Top; } return new Box(minX, minY, maxX - minX, maxY - minY); diff --git a/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs b/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs index 6017b59..bb05614 100644 --- a/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs +++ b/OpenNest.Engine/RectanglePacking/PackBottomLeft.cs @@ -1,21 +1,25 @@ -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.RectanglePacking { internal class PackBottomLeft : PackEngine { public PackBottomLeft(Bin bin) - : base(bin) - { - } + : base(bin) { } public override void Pack(List items) { - var byArea = items.Select(i => i.Clone() as Item).OrderByDescending(i => i.Area()).ToList(); - var byLength = items.Select(i => i.Clone() as Item).OrderByDescending(i => System.Math.Max(i.Width, i.Length)).ToList(); + var byArea = items + .Select(i => i.Clone() as Item) + .OrderByDescending(i => i.Area()) + .ToList(); + var byLength = items + .Select(i => i.Clone() as Item) + .OrderByDescending(i => System.Math.Max(i.Width, i.Length)) + .ToList(); var resultA = PackWithOrder(byArea); var resultB = PackWithOrder(byLength); diff --git a/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs b/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs index 62cdbed..25fe8ab 100644 --- a/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs +++ b/OpenNest.Engine/RectanglePacking/PackFirstFitDecreasing.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; namespace OpenNest.RectanglePacking { diff --git a/OpenNest.Engine/Sequencing/AdvancedSequencer.cs b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs index 862d50b..29d2941 100644 --- a/OpenNest.Engine/Sequencing/AdvancedSequencer.cs +++ b/OpenNest.Engine/Sequencing/AdvancedSequencer.cs @@ -1,7 +1,7 @@ -using OpenNest.CNC.CuttingStrategy; -using OpenNest.Math; using System.Collections.Generic; using System.Linq; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Math; namespace OpenNest.Engine.Sequencing { @@ -50,9 +50,7 @@ namespace OpenNest.Engine.Sequencing private static List GroupIntoRows(IReadOnlyList parts, double minDistance) { // Sort parts by Y center - var sorted = parts - .OrderBy(p => p.BoundingBox.Center.Y) - .ToList(); + var sorted = parts.OrderBy(p => p.BoundingBox.Center.Y).ToList(); var rows = new List(); diff --git a/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs b/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs index 3ce47a9..9245ae9 100644 --- a/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs +++ b/OpenNest.Engine/Sequencing/EdgeStartSequencer.cs @@ -12,9 +12,11 @@ namespace OpenNest.Engine.Sequencing // corrected extents: Size.Length = X-extent, Size.Width = Y-extent. var origin = plate.BoundingBox(false); var plateBox = new OpenNest.Geometry.Box( - origin.X, origin.Y, + origin.X, + origin.Y, plate.Size.Length, - plate.Size.Width); + plate.Size.Width + ); return parts .OrderBy(p => MinEdgeDistance(p.BoundingBox.Center, plateBox)) @@ -23,14 +25,20 @@ namespace OpenNest.Engine.Sequencing .ToList(); } - private static double MinEdgeDistance(OpenNest.Geometry.Vector center, OpenNest.Geometry.Box plateBox) + private static double MinEdgeDistance( + OpenNest.Geometry.Vector center, + OpenNest.Geometry.Box plateBox + ) { var distLeft = center.X - plateBox.Left; var distRight = plateBox.Right - center.X; var distBottom = center.Y - plateBox.Bottom; var distTop = plateBox.Top - center.Y; - return System.Math.Min(System.Math.Min(distLeft, distRight), System.Math.Min(distBottom, distTop)); + return System.Math.Min( + System.Math.Min(distLeft, distRight), + System.Math.Min(distBottom, distTop) + ); } } } diff --git a/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs b/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs index 045cb1e..67e9e4c 100644 --- a/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs +++ b/OpenNest.Engine/Sequencing/LeastCodeSequencer.cs @@ -1,5 +1,5 @@ -using OpenNest.Math; using System.Collections.Generic; +using OpenNest.Math; namespace OpenNest.Engine.Sequencing { @@ -27,7 +27,10 @@ namespace OpenNest.Engine.Sequencing return result; } - private static List NearestNeighbor(IReadOnlyList parts, OpenNest.Geometry.Vector exit) + private static List NearestNeighbor( + IReadOnlyList parts, + OpenNest.Geometry.Vector exit + ) { var remaining = new List(parts); var ordered = new List(parts.Count); @@ -97,7 +100,12 @@ namespace OpenNest.Engine.Sequencing /// Only the segment around the reversed segment [i..j] needs to be checked, /// but here we compute the full route cost for correctness. /// - private static double RouteDistance(List ordered, OpenNest.Geometry.Vector exit, int i, int j) + private static double RouteDistance( + List ordered, + OpenNest.Geometry.Vector exit, + int i, + int j + ) { // Full route distance: exit -> ordered[0] -> ... -> ordered[n-1] var total = 0.0; diff --git a/OpenNest.Engine/Sequencing/PartSequencerFactory.cs b/OpenNest.Engine/Sequencing/PartSequencerFactory.cs index 57444eb..85a837a 100644 --- a/OpenNest.Engine/Sequencing/PartSequencerFactory.cs +++ b/OpenNest.Engine/Sequencing/PartSequencerFactory.cs @@ -1,5 +1,5 @@ -using OpenNest.CNC.CuttingStrategy; using System; +using OpenNest.CNC.CuttingStrategy; namespace OpenNest.Engine.Sequencing { @@ -16,7 +16,8 @@ namespace OpenNest.Engine.Sequencing SequenceMethod.LeastCode => new LeastCodeSequencer(), SequenceMethod.Advanced => new AdvancedSequencer(parameters), _ => throw new NotSupportedException( - $"Sequence method '{parameters.Method}' is not supported.") + $"Sequence method '{parameters.Method}' is not supported." + ), }; } } diff --git a/OpenNest.Engine/Sequencing/PlateHelper.cs b/OpenNest.Engine/Sequencing/PlateHelper.cs index 04e3282..0308530 100644 --- a/OpenNest.Engine/Sequencing/PlateHelper.cs +++ b/OpenNest.Engine/Sequencing/PlateHelper.cs @@ -15,7 +15,7 @@ namespace OpenNest.Engine.Sequencing 2 => new Vector(0, yExtent), 3 => new Vector(0, 0), 4 => new Vector(xExtent, 0), - _ => new Vector(xExtent, yExtent) + _ => new Vector(xExtent, yExtent), }; } } diff --git a/OpenNest.Engine/Strategies/ColumnFillStrategy.cs b/OpenNest.Engine/Strategies/ColumnFillStrategy.cs index c300171..9df0918 100644 --- a/OpenNest.Engine/Strategies/ColumnFillStrategy.cs +++ b/OpenNest.Engine/Strategies/ColumnFillStrategy.cs @@ -14,7 +14,10 @@ public class ColumnFillStrategy : IFillStrategy if (context.PartType == PartType.Rectangle) return null; - var filler = new StripeFiller(context, NestDirection.Vertical) { CompleteStripesOnly = true }; + var filler = new StripeFiller(context, NestDirection.Vertical) + { + CompleteStripesOnly = true, + }; return filler.Fill(); } } diff --git a/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs b/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs index e47f4b9..957156e 100644 --- a/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/ExtentsFillStrategy.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Engine.Fill; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Engine.Strategies { @@ -22,10 +22,13 @@ namespace OpenNest.Engine.Strategies var angles = new[] { bestRotation, bestRotation + Angle.HalfPI }; - return FillHelpers.BestOverAngles(context, angles, - angle => filler.Fill(context.Item.Drawing, angle, - context.Token, context.ReportProgress), - "Extents"); + return FillHelpers.BestOverAngles( + context, + angles, + angle => + filler.Fill(context.Item.Drawing, angle, context.Token, context.ReportProgress), + "Extents" + ); } } } diff --git a/OpenNest.Engine/Strategies/FillContext.cs b/OpenNest.Engine/Strategies/FillContext.cs index 3b8262c..ab56dac 100644 --- a/OpenNest.Engine/Strategies/FillContext.cs +++ b/OpenNest.Engine/Strategies/FillContext.cs @@ -1,9 +1,9 @@ -using OpenNest.Engine; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Threading; +using OpenNest.Engine; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Engine.Strategies { @@ -20,6 +20,7 @@ namespace OpenNest.Engine.Strategies public PartType PartType { get; set; } public List CurrentBest { get; set; } + /// For progress reporting only; comparisons use Policy.Comparer. public FillScore CurrentBestScore { get; set; } public NestPhase WinnerPhase { get; set; } @@ -37,7 +38,9 @@ namespace OpenNest.Engine.Strategies /// public void ReportProgress(List parts, string description) { - var isNewBest = parts != null && parts.Count > 0 + var isNewBest = + parts != null + && parts.Count > 0 && Policy.Comparer.IsBetter(parts, CurrentBest, WorkArea); if (isNewBest) @@ -47,15 +50,18 @@ namespace OpenNest.Engine.Strategies WinnerPhase = ActivePhase; } - NestEngineBase.ReportProgress(Progress, new ProgressReport - { - Phase = ActivePhase, - PlateNumber = PlateNumber, - Parts = isNewBest ? parts : CurrentBest, - WorkArea = WorkArea, - Description = description, - IsOverallBest = isNewBest, - }); + NestEngineBase.ReportProgress( + Progress, + new ProgressReport + { + Phase = ActivePhase, + PlateNumber = PlateNumber, + Parts = isNewBest ? parts : CurrentBest, + WorkArea = WorkArea, + Description = description, + IsOverallBest = isNewBest, + } + ); } } } diff --git a/OpenNest.Engine/Strategies/FillHelpers.cs b/OpenNest.Engine/Strategies/FillHelpers.cs index 024122e..2ac300b 100644 --- a/OpenNest.Engine/Strategies/FillHelpers.cs +++ b/OpenNest.Engine/Strategies/FillHelpers.cs @@ -1,10 +1,10 @@ -using OpenNest.Engine.Fill; -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Engine.Strategies { @@ -30,25 +30,34 @@ namespace OpenNest.Engine.Strategies return pattern; } - public static List FillPattern(FillLinear engine, List groupParts, List angles, Box workArea, IFillComparer comparer = null) + public static List FillPattern( + FillLinear engine, + List groupParts, + List angles, + Box workArea, + IFillComparer comparer = null + ) { var results = new ConcurrentBag<(List Parts, FillScore Score)>(); - Parallel.ForEach(angles, angle => - { - var pattern = BuildRotatedPattern(groupParts, angle); + Parallel.ForEach( + angles, + angle => + { + var pattern = BuildRotatedPattern(groupParts, angle); - if (pattern.Parts.Count == 0) - return; + if (pattern.Parts.Count == 0) + return; - var h = engine.Fill(pattern, NestDirection.Horizontal); - if (h != null && h.Count > 0) - results.Add((h, FillScore.Compute(h, workArea))); + var h = engine.Fill(pattern, NestDirection.Horizontal); + if (h != null && h.Count > 0) + results.Add((h, FillScore.Compute(h, workArea))); - var v = engine.Fill(pattern, NestDirection.Vertical); - if (v != null && v.Count > 0) - results.Add((v, FillScore.Compute(v, workArea))); - }); + var v = engine.Fill(pattern, NestDirection.Vertical); + if (v != null && v.Count > 0) + results.Add((v, FillScore.Compute(v, workArea))); + } + ); List best = null; var bestScore = default(FillScore); @@ -82,7 +91,8 @@ namespace OpenNest.Engine.Strategies Func> fillFunc, NestDirection? preferred, IFillComparer comparer, - Box workArea) + Box workArea + ) { if (preferred == null) { @@ -92,15 +102,18 @@ namespace OpenNest.Engine.Strategies if ((h == null || h.Count == 0) && (v == null || v.Count == 0)) return new List(); - if (h == null || h.Count == 0) return v; - if (v == null || v.Count == 0) return h; + if (h == null || h.Count == 0) + return v; + if (v == null || v.Count == 0) + return h; return comparer.IsBetter(h, v, workArea) ? h : v; } - var other = preferred == NestDirection.Horizontal - ? NestDirection.Vertical - : NestDirection.Horizontal; + var other = + preferred == NestDirection.Horizontal + ? NestDirection.Vertical + : NestDirection.Horizontal; var pref = fillFunc(preferred.Value); if (pref != null && pref.Count > 0) @@ -119,7 +132,8 @@ namespace OpenNest.Engine.Strategies FillContext context, IReadOnlyList angles, Func> fillAtAngle, - string phaseLabel) + string phaseLabel + ) { var workArea = context.WorkArea; var comparer = context.Policy?.Comparer ?? new DefaultFillComparer(); @@ -139,8 +153,10 @@ namespace OpenNest.Engine.Strategies best = result; } - context.ReportProgress(best, - $"{phaseLabel}: {i + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts"); + context.ReportProgress( + best, + $"{phaseLabel}: {i + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts" + ); } return best ?? new List(); @@ -160,10 +176,10 @@ namespace OpenNest.Engine.Strategies { var b2 = parts[j].BoundingBox; - var overlapX = System.Math.Min(b1.Right, b2.Right) - - System.Math.Max(b1.Left, b2.Left); - var overlapY = System.Math.Min(b1.Top, b2.Top) - - System.Math.Max(b1.Bottom, b2.Bottom); + var overlapX = + System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left); + var overlapY = + System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom); if (overlapX <= Tolerance.Epsilon || overlapY <= Tolerance.Epsilon) continue; diff --git a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs index 1d86771..abb1bb1 100644 --- a/OpenNest.Engine/Strategies/FillStrategyRegistry.cs +++ b/OpenNest.Engine/Strategies/FillStrategyRegistry.cs @@ -19,8 +19,7 @@ namespace OpenNest.Engine.Strategies LoadFrom(typeof(FillStrategyRegistry).Assembly); } - public static IReadOnlyList Strategies => - sorted ??= FilterStrategies(); + public static IReadOnlyList Strategies => sorted ??= FilterStrategies(); /// /// Returns all registered strategies regardless of enabled/disabled state. @@ -35,9 +34,10 @@ namespace OpenNest.Engine.Strategies private static List FilterStrategies() { - var source = enabledFilter != null - ? strategies.Where(s => enabledFilter.Contains(s.Name)) - : strategies.Where(s => !disabled.Contains(s.Name)); + var source = + enabledFilter != null + ? strategies.Where(s => enabledFilter.Contains(s.Name)) + : strategies.Where(s => !disabled.Contains(s.Name)); return source.OrderBy(s => s.Order).ToList(); } @@ -68,9 +68,10 @@ namespace OpenNest.Engine.Strategies /// public static void SetEnabled(params string[] names) { - enabledFilter = names != null && names.Length > 0 - ? new HashSet(names, StringComparer.OrdinalIgnoreCase) - : null; + enabledFilter = + names != null && names.Length > 0 + ? new HashSet(names, StringComparer.OrdinalIgnoreCase) + : null; sorted = null; } @@ -78,13 +79,19 @@ namespace OpenNest.Engine.Strategies { foreach (var type in assembly.GetTypes()) { - if (type.IsAbstract || type.IsInterface || !typeof(IFillStrategy).IsAssignableFrom(type)) + if ( + type.IsAbstract + || type.IsInterface + || !typeof(IFillStrategy).IsAssignableFrom(type) + ) continue; var ctor = type.GetConstructor(Type.EmptyTypes); if (ctor == null) { - Debug.WriteLine($"[FillStrategyRegistry] Skipping {type.Name}: no parameterless constructor"); + Debug.WriteLine( + $"[FillStrategyRegistry] Skipping {type.Name}: no parameterless constructor" + ); continue; } @@ -92,18 +99,28 @@ namespace OpenNest.Engine.Strategies { var instance = (IFillStrategy)ctor.Invoke(null); - if (strategies.Any(s => s.Name.Equals(instance.Name, StringComparison.OrdinalIgnoreCase))) + if ( + strategies.Any(s => + s.Name.Equals(instance.Name, StringComparison.OrdinalIgnoreCase) + ) + ) { - Debug.WriteLine($"[FillStrategyRegistry] Duplicate strategy '{instance.Name}' skipped"); + Debug.WriteLine( + $"[FillStrategyRegistry] Duplicate strategy '{instance.Name}' skipped" + ); continue; } strategies.Add(instance); - Debug.WriteLine($"[FillStrategyRegistry] Registered: {instance.Name} (Order={instance.Order})"); + Debug.WriteLine( + $"[FillStrategyRegistry] Registered: {instance.Name} (Order={instance.Order})" + ); } catch (Exception ex) { - Debug.WriteLine($"[FillStrategyRegistry] Failed to instantiate {type.Name}: {ex.Message}"); + Debug.WriteLine( + $"[FillStrategyRegistry] Failed to instantiate {type.Name}: {ex.Message}" + ); } } @@ -121,11 +138,15 @@ namespace OpenNest.Engine.Strategies { var assembly = Assembly.LoadFrom(dll); LoadFrom(assembly); - Debug.WriteLine($"[FillStrategyRegistry] Loaded plugin assembly: {Path.GetFileName(dll)}"); + Debug.WriteLine( + $"[FillStrategyRegistry] Loaded plugin assembly: {Path.GetFileName(dll)}" + ); } catch (Exception ex) { - Debug.WriteLine($"[FillStrategyRegistry] Failed to load {Path.GetFileName(dll)}: {ex.Message}"); + Debug.WriteLine( + $"[FillStrategyRegistry] Failed to load {Path.GetFileName(dll)}: {ex.Message}" + ); } } } diff --git a/OpenNest.Engine/Strategies/LinearFillStrategy.cs b/OpenNest.Engine/Strategies/LinearFillStrategy.cs index 8c1c278..d3b25d1 100644 --- a/OpenNest.Engine/Strategies/LinearFillStrategy.cs +++ b/OpenNest.Engine/Strategies/LinearFillStrategy.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Engine.Fill; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Engine.Strategies { @@ -20,27 +20,38 @@ namespace OpenNest.Engine.Strategies var comparer = context.Policy?.Comparer ?? new DefaultFillComparer(); var preferred = context.Policy?.PreferredDirection; - return FillHelpers.BestOverAngles(context, angles, + return FillHelpers.BestOverAngles( + context, + angles, angle => { - var engine = new FillLinear(workArea, context.Plate.PartSpacing) { Label = "Linear" }; + var engine = new FillLinear(workArea, context.Plate.PartSpacing) + { + Label = "Linear", + }; var result = FillHelpers.FillWithDirectionPreference( dir => engine.Fill(context.Item.Drawing, angle, dir), - preferred, comparer, workArea); + preferred, + comparer, + workArea + ); if (result != null && result.Count > 0) { - context.AngleResults.Add(new AngleResult - { - AngleDeg = Angle.ToDegrees(angle), - Direction = preferred ?? NestDirection.Horizontal, - PartCount = result.Count - }); + context.AngleResults.Add( + new AngleResult + { + AngleDeg = Angle.ToDegrees(angle), + Direction = preferred ?? NestDirection.Horizontal, + PartCount = result.Count, + } + ); } return result; }, - "Linear"); + "Linear" + ); } } } diff --git a/OpenNest.Engine/Strategies/PairsFillStrategy.cs b/OpenNest.Engine/Strategies/PairsFillStrategy.cs index 76a9d44..e0867fe 100644 --- a/OpenNest.Engine/Strategies/PairsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/PairsFillStrategy.cs @@ -1,6 +1,6 @@ -using OpenNest.Engine.Fill; using System.Collections.Generic; using System.Threading; +using OpenNest.Engine.Fill; namespace OpenNest.Engine.Strategies { @@ -29,8 +29,12 @@ namespace OpenNest.Engine.Strategies var comparer = context.Policy?.Comparer; var dedup = GridDedup.GetOrCreate(context.SharedState); var filler = new PairFiller(context.Plate, comparer, dedup); - var result = filler.Fill(context.Item, context.WorkArea, - context.Token, context.ReportProgress); + var result = filler.Fill( + context.Item, + context.WorkArea, + context.Token, + context.ReportProgress + ); context.SharedState["BestFits"] = result.BestFits; diff --git a/OpenNest.Engine/Strategies/RectBestFitStrategy.cs b/OpenNest.Engine/Strategies/RectBestFitStrategy.cs index b0856a1..36de1b0 100644 --- a/OpenNest.Engine/Strategies/RectBestFitStrategy.cs +++ b/OpenNest.Engine/Strategies/RectBestFitStrategy.cs @@ -1,5 +1,5 @@ -using OpenNest.RectanglePacking; using System.Collections.Generic; +using OpenNest.RectanglePacking; namespace OpenNest.Engine.Strategies { diff --git a/OpenNest.Engine/Strategies/RowFillStrategy.cs b/OpenNest.Engine/Strategies/RowFillStrategy.cs index 29cce72..31db0f4 100644 --- a/OpenNest.Engine/Strategies/RowFillStrategy.cs +++ b/OpenNest.Engine/Strategies/RowFillStrategy.cs @@ -14,7 +14,10 @@ public class RowFillStrategy : IFillStrategy if (context.PartType == PartType.Rectangle) return null; - var filler = new StripeFiller(context, NestDirection.Horizontal) { CompleteStripesOnly = true }; + var filler = new StripeFiller(context, NestDirection.Horizontal) + { + CompleteStripesOnly = true, + }; return filler.Fill(); } } diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index d25cb57..e4bc097 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -1,27 +1,31 @@ -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest { public class StripNestEngine : NestEngineBase { - public StripNestEngine(Plate plate) : base(plate) - { - } + public StripNestEngine(Plate plate) + : base(plate) { } public override string Name => "Strip"; - public override string Description => "Iterative shrink-fill nesting for mixed-drawing layouts"; + public override string Description => + "Iterative shrink-fill nesting for mixed-drawing layouts"; /// /// Single-item fill delegates to DefaultNestEngine. /// - public override List Fill(NestItem item, Box workArea, - IProgress progress, CancellationToken token) + public override List Fill( + NestItem item, + Box workArea, + IProgress progress, + CancellationToken token + ) { var inner = new DefaultNestEngine(Plate); return inner.Fill(item, workArea, progress, token); @@ -30,8 +34,12 @@ namespace OpenNest /// /// Group-parts fill delegates to DefaultNestEngine. /// - public override List Fill(List groupParts, Box workArea, - IProgress progress, CancellationToken token) + public override List Fill( + List groupParts, + Box workArea, + IProgress progress, + CancellationToken token + ) { var inner = new DefaultNestEngine(Plate); return inner.Fill(groupParts, workArea, progress, token); @@ -40,8 +48,12 @@ namespace OpenNest /// /// Pack delegates to DefaultNestEngine. /// - public override List PackArea(Box box, List items, - IProgress progress, CancellationToken token) + public override List PackArea( + Box box, + List items, + IProgress progress, + CancellationToken token + ) { var inner = new DefaultNestEngine(Plate); return inner.PackArea(box, items, progress, token); @@ -53,8 +65,11 @@ namespace OpenNest /// sub-region using dual-direction selection. Singles and leftovers /// are packed at the end. /// - public override List Nest(List items, - IProgress progress, CancellationToken token) + public override List Nest( + List items, + IProgress progress, + CancellationToken token + ) { if (items == null || items.Count == 0) return new List(); @@ -68,9 +83,7 @@ namespace OpenNest .ThenByDescending(i => i.Drawing.Area) .ToList(); - var packItems = items - .Where(i => i.Quantity == 1) - .ToList(); + var packItems = items.Where(i => i.Quantity == 1).ToList(); var allParts = new List(); @@ -92,8 +105,15 @@ namespace OpenNest }; var shrinkResult = IterativeShrinkFiller.Fill( - fillItems, workArea, heightFillFunc, Plate.PartSpacing, token, - progress, PlateNumber, widthFillFunc); + fillItems, + workArea, + heightFillFunc, + Plate.PartSpacing, + token, + progress, + PlateNumber, + widthFillFunc + ); allParts.AddRange(shrinkResult.Parts); @@ -134,8 +154,7 @@ namespace OpenNest if (item.Quantity <= 0) continue; - var placed = allParts.Count(p => - ReferenceEquals(p.BaseDrawing, item.Drawing)); + var placed = allParts.Count(p => ReferenceEquals(p.BaseDrawing, item.Drawing)); item.Quantity = System.Math.Max(0, item.Quantity - placed); } diff --git a/OpenNest.Engine/VerticalRemnantEngine.cs b/OpenNest.Engine/VerticalRemnantEngine.cs index 510f1bb..c50c550 100644 --- a/OpenNest.Engine/VerticalRemnantEngine.cs +++ b/OpenNest.Engine/VerticalRemnantEngine.cs @@ -14,7 +14,8 @@ namespace OpenNest /// public class VerticalRemnantEngine : DefaultNestEngine { - public VerticalRemnantEngine(Plate plate) : base(plate) { } + public VerticalRemnantEngine(Plate plate) + : base(plate) { } public override string Name => "Vertical Remnant"; @@ -24,9 +25,17 @@ namespace OpenNest public override NestDirection? PreferredDirection => NestDirection.Horizontal; - public override List BuildAngles(NestItem item, ClassificationResult classification, Box workArea) + public override List BuildAngles( + NestItem item, + ClassificationResult classification, + Box workArea + ) { - var baseAngles = new List { classification.PrimaryAngle, classification.PrimaryAngle + Angle.HalfPI }; + var baseAngles = new List + { + classification.PrimaryAngle, + classification.PrimaryAngle + Angle.HalfPI, + }; baseAngles.Sort((a, b) => RotatedWidth(item, a).CompareTo(RotatedWidth(item, b))); return baseAngles; } diff --git a/OpenNest.Gpu/GpuEvaluatorFactory.cs b/OpenNest.Gpu/GpuEvaluatorFactory.cs index 22c2e5e..aebd0c2 100644 --- a/OpenNest.Gpu/GpuEvaluatorFactory.cs +++ b/OpenNest.Gpu/GpuEvaluatorFactory.cs @@ -1,8 +1,8 @@ +using System; +using System.Diagnostics; using ILGPU; using ILGPU.Runtime; using OpenNest.Engine.BestFit; -using System; -using System.Diagnostics; namespace OpenNest.Gpu { @@ -18,7 +18,8 @@ namespace OpenNest.Gpu { get { - if (!_probed) Probe(); + if (!_probed) + Probe(); return _gpuAvailable; } } @@ -27,7 +28,8 @@ namespace OpenNest.Gpu { get { - if (!_probed) Probe(); + if (!_probed) + Probe(); return _deviceName ?? "None"; } } @@ -65,7 +67,9 @@ namespace OpenNest.Gpu } catch (Exception ex) { - Debug.WriteLine($"[GpuEvaluatorFactory] GPU slide computer failed: {ex.Message}"); + Debug.WriteLine( + $"[GpuEvaluatorFactory] GPU slide computer failed: {ex.Message}" + ); return null; } } @@ -80,12 +84,16 @@ namespace OpenNest.Gpu using var context = Context.CreateDefault(); foreach (var device in context.Devices) { - if (device.AcceleratorType == AcceleratorType.Cuda || - device.AcceleratorType == AcceleratorType.OpenCL) + if ( + device.AcceleratorType == AcceleratorType.Cuda + || device.AcceleratorType == AcceleratorType.OpenCL + ) { _gpuAvailable = true; _deviceName = device.Name; - Debug.WriteLine($"[GpuEvaluatorFactory] GPU found: {device.Name} ({device.AcceleratorType})"); + Debug.WriteLine( + $"[GpuEvaluatorFactory] GPU found: {device.Name} ({device.AcceleratorType})" + ); return; } } diff --git a/OpenNest.Gpu/GpuPairEvaluator.cs b/OpenNest.Gpu/GpuPairEvaluator.cs index 7ff20f7..cd79632 100644 --- a/OpenNest.Gpu/GpuPairEvaluator.cs +++ b/OpenNest.Gpu/GpuPairEvaluator.cs @@ -1,12 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using ILGPU; using ILGPU.Runtime; using OpenNest.Converters; using OpenNest.Engine.BestFit; using OpenNest.Geometry; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; namespace OpenNest.Gpu { @@ -18,13 +18,18 @@ namespace OpenNest.Gpu private readonly double _spacing; private readonly double _cellSize; - public GpuPairEvaluator(Drawing drawing, double spacing, double cellSize = PartBitmap.DefaultCellSize) + public GpuPairEvaluator( + Drawing drawing, + double spacing, + double cellSize = PartBitmap.DefaultCellSize + ) { _drawing = drawing; _spacing = spacing; _cellSize = cellSize; _context = Context.CreateDefault(); - _accelerator = _context.GetPreferredDevice(preferCPU: false) + _accelerator = _context + .GetPreferredDevice(preferCPU: false) .CreateAccelerator(_context); } @@ -91,8 +96,10 @@ namespace OpenNest.Gpu var c = groupItems[i].Candidate; var shiftX = c.Part2Offset.X - locationB.X; var shiftY = c.Part2Offset.Y - locationB.Y; - offsets[i * 2 + 0] = (int)System.Math.Round((shiftX + bitmapB.OriginX - bitmapA.OriginX) / _cellSize); - offsets[i * 2 + 1] = (int)System.Math.Round((shiftY + bitmapB.OriginY - bitmapA.OriginY) / _cellSize); + offsets[i * 2 + 0] = (int) + System.Math.Round((shiftX + bitmapB.OriginX - bitmapA.OriginX) / _cellSize); + offsets[i * 2 + 1] = (int) + System.Math.Round((shiftY + bitmapB.OriginY - bitmapA.OriginY) / _cellSize); } var resultScores = new int[candidateCount]; @@ -108,10 +115,19 @@ namespace OpenNest.Gpu ArrayView1D, ArrayView1D, ArrayView1D, - int, int>(OverlapKernel); + int, + int + >(OverlapKernel); - kernel(candidateCount, gpuPaddedA.View, gpuPaddedB.View, - gpuOffsets.View, gpuResults.View, gridWidth, gridHeight); + kernel( + candidateCount, + gpuPaddedA.View, + gpuPaddedB.View, + gpuOffsets.View, + gpuResults.View, + gridWidth, + gridHeight + ); _accelerator.Synchronize(); gpuResults.CopyToCPU(resultScores); @@ -119,31 +135,40 @@ namespace OpenNest.Gpu // Process results in parallel — pre-computed vertices avoid // per-candidate Part creation, and Parallel.For matches the // CPU evaluator's Parallel.ForEach concurrency. - Parallel.For(0, candidateCount, i => - { - var item = groupItems[i]; - var hasOverlap = resultScores[i] > 0; + Parallel.For( + 0, + candidateCount, + i => + { + var item = groupItems[i]; + var hasOverlap = resultScores[i] > 0; - if (hasOverlap) - { - allResults[item.OriginalIndex] = new BestFitResult + if (hasOverlap) { - Candidate = item.Candidate, - RotatedArea = 0, - BoundingWidth = 0, - BoundingHeight = 0, - OptimalRotation = 0, - TrueArea = trueArea, - Keep = false, - Reason = "Overlap detected" - }; + allResults[item.OriginalIndex] = new BestFitResult + { + Candidate = item.Candidate, + RotatedArea = 0, + BoundingWidth = 0, + BoundingHeight = 0, + OptimalRotation = 0, + TrueArea = trueArea, + Keep = false, + Reason = "Overlap detected", + }; + } + else + { + allResults[item.OriginalIndex] = ComputeBoundingResult( + item.Candidate, + trueArea, + verticesA, + verticesB, + locationB + ); + } } - else - { - allResults[item.OriginalIndex] = ComputeBoundingResult( - item.Candidate, trueArea, verticesA, verticesB, locationB); - } - }); + ); } return allResults.ToList(); @@ -161,7 +186,8 @@ namespace OpenNest.Gpu ArrayView1D candidateOffsets, ArrayView1D results, int gridWidth, - int gridHeight) + int gridHeight + ) { var offsetX = candidateOffsets[index * 2]; var offsetY = candidateOffsets[index * 2 + 1]; @@ -173,7 +199,8 @@ namespace OpenNest.Gpu for (var x = 0; x < gridWidth; x++) { var cellA = partBitmapA[y * gridWidth + x]; - if (cellA != 1) continue; + if (cellA != 1) + continue; var bx = x - offsetX; var by = y - offsetY; @@ -210,8 +237,12 @@ namespace OpenNest.Gpu private const double ChordTolerance = 0.01; private static BestFitResult ComputeBoundingResult( - PairCandidate candidate, double trueArea, - List verticesA, List verticesB, Vector locationB) + PairCandidate candidate, + double trueArea, + List verticesA, + List verticesB, + Vector locationB + ) { var shift = candidate.Part2Offset - locationB; @@ -221,7 +252,10 @@ namespace OpenNest.Gpu foreach (var v in verticesB) allPoints.Add(v + shift); - double bestArea, bestWidth, bestHeight, bestRotation; + double bestArea, + bestWidth, + bestHeight, + bestRotation; if (allPoints.Count >= 3) { @@ -249,13 +283,14 @@ namespace OpenNest.Gpu OptimalRotation = bestRotation, TrueArea = trueArea, Keep = true, - Reason = "Valid" + Reason = "Valid", }; } private static List GetPartVertices(Part part) { - var entities = ConvertProgram.ToGeometry(part.Program) + var entities = ConvertProgram + .ToGeometry(part.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); var points = new List(); @@ -283,7 +318,7 @@ namespace OpenNest.Gpu OptimalRotation = 0, TrueArea = 0, Keep = false, - Reason = "No geometry" + Reason = "No geometry", }; } diff --git a/OpenNest.Gpu/GpuSlideComputer.cs b/OpenNest.Gpu/GpuSlideComputer.cs index 4e00511..21876ad 100644 --- a/OpenNest.Gpu/GpuSlideComputer.cs +++ b/OpenNest.Gpu/GpuSlideComputer.cs @@ -1,8 +1,8 @@ +using System; using ILGPU; using ILGPU.Algorithms; using ILGPU.Runtime; using OpenNest.Engine.BestFit; -using System; namespace OpenNest.Gpu { @@ -14,25 +14,34 @@ namespace OpenNest.Gpu // ── Kernels ────────────────────────────────────────────────── - private readonly Action, // stationaryPrep - ArrayView1D, // movingPrep - ArrayView1D, // offsets - ArrayView1D, // results - int, int, int> _kernel; + private readonly Action< + Index1D, + ArrayView1D, // stationaryPrep + ArrayView1D, // movingPrep + ArrayView1D, // offsets + ArrayView1D, // results + int, + int, + int + > _kernel; - private readonly Action, // stationaryPrep - ArrayView1D, // movingPrep - ArrayView1D, // offsets - ArrayView1D, // results - ArrayView1D, // directions - int, int> _kernelMultiDir; + private readonly Action< + Index1D, + ArrayView1D, // stationaryPrep + ArrayView1D, // movingPrep + ArrayView1D, // offsets + ArrayView1D, // results + ArrayView1D, // directions + int, + int + > _kernelMultiDir; - private readonly Action, // raw - ArrayView1D, // prepared - int> _prepareKernel; + private readonly Action< + Index1D, + ArrayView1D, // raw + ArrayView1D, // prepared + int + > _prepareKernel; // ── Buffers ────────────────────────────────────────────────── @@ -52,7 +61,8 @@ namespace OpenNest.Gpu public GpuSlideComputer() { _context = Context.CreateDefault(); - _accelerator = _context.GetPreferredDevice(preferCPU: false) + _accelerator = _context + .GetPreferredDevice(preferCPU: false) .CreateAccelerator(_context); _kernel = _accelerator.LoadAutoGroupedStreamKernel< @@ -61,7 +71,10 @@ namespace OpenNest.Gpu ArrayView1D, ArrayView1D, ArrayView1D, - int, int, int>(SlideKernel); + int, + int, + int + >(SlideKernel); _kernelMultiDir = _accelerator.LoadAutoGroupedStreamKernel< Index1D, @@ -70,20 +83,27 @@ namespace OpenNest.Gpu ArrayView1D, ArrayView1D, ArrayView1D, - int, int>(SlideKernelMultiDir); + int, + int + >(SlideKernelMultiDir); _prepareKernel = _accelerator.LoadAutoGroupedStreamKernel< Index1D, ArrayView1D, ArrayView1D, - int>(PrepareKernel); + int + >(PrepareKernel); } public double[] ComputeBatch( - double[] stationarySegments, int stationaryCount, - double[] movingTemplateSegments, int movingCount, - double[] offsets, int offsetCount, - PushDirection direction) + double[] stationarySegments, + int stationaryCount, + double[] movingTemplateSegments, + int movingCount, + double[] offsets, + int offsetCount, + PushDirection direction + ) { var results = new double[offsetCount]; if (offsetCount == 0 || stationaryCount == 0 || movingCount == 0) @@ -100,10 +120,16 @@ namespace OpenNest.Gpu _gpuOffsets!.View.SubView(0, offsetCount * 2).CopyFromCPU(offsets); - _kernel(offsetCount, - _gpuStationaryPrep!.View, _gpuMovingPrep!.View, - _gpuOffsets.View, _gpuResults!.View, - stationaryCount, movingCount, (int)direction); + _kernel( + offsetCount, + _gpuStationaryPrep!.View, + _gpuMovingPrep!.View, + _gpuOffsets.View, + _gpuResults!.View, + stationaryCount, + movingCount, + (int)direction + ); _accelerator.Synchronize(); _gpuResults.View.SubView(0, offsetCount).CopyToCPU(results); @@ -113,10 +139,14 @@ namespace OpenNest.Gpu } public double[] ComputeBatchMultiDir( - double[] stationarySegments, int stationaryCount, - double[] movingTemplateSegments, int movingCount, - double[] offsets, int offsetCount, - int[] directions) + double[] stationarySegments, + int stationaryCount, + double[] movingTemplateSegments, + int movingCount, + double[] offsets, + int offsetCount, + int[] directions + ) { var results = new double[offsetCount]; if (offsetCount == 0 || stationaryCount == 0 || movingCount == 0) @@ -134,10 +164,16 @@ namespace OpenNest.Gpu _gpuOffsets!.View.SubView(0, offsetCount * 2).CopyFromCPU(offsets); _gpuDirs!.View.SubView(0, offsetCount).CopyFromCPU(directions); - _kernelMultiDir(offsetCount, - _gpuStationaryPrep!.View, _gpuMovingPrep!.View, - _gpuOffsets.View, _gpuResults!.View, _gpuDirs.View, - stationaryCount, movingCount); + _kernelMultiDir( + offsetCount, + _gpuStationaryPrep!.View, + _gpuMovingPrep!.View, + _gpuOffsets.View, + _gpuResults!.View, + _gpuDirs.View, + stationaryCount, + movingCount + ); _accelerator.Synchronize(); _gpuResults.View.SubView(0, offsetCount).CopyToCPU(results); @@ -147,18 +183,25 @@ namespace OpenNest.Gpu } public void InvalidateStationary() => _lastStationaryData = null; + public void InvalidateMoving() => _lastMovingData = null; private void EnsureStationary(double[] data, int count) { // Fast check: if same object or content is identical, skip upload - if (_gpuStationaryPrep != null && - _lastStationaryData != null && - _lastStationaryData.Length == data.Length) + if ( + _gpuStationaryPrep != null + && _lastStationaryData != null + && _lastStationaryData.Length == data.Length + ) { // Reference equality or content equality - if (_lastStationaryData == data || - new ReadOnlySpan(_lastStationaryData).SequenceEqual(new ReadOnlySpan(data))) + if ( + _lastStationaryData == data + || new ReadOnlySpan(_lastStationaryData).SequenceEqual( + new ReadOnlySpan(data) + ) + ) { return; } @@ -178,12 +221,18 @@ namespace OpenNest.Gpu private void EnsureMoving(double[] data, int count) { - if (_gpuMovingPrep != null && - _lastMovingData != null && - _lastMovingData.Length == data.Length) + if ( + _gpuMovingPrep != null + && _lastMovingData != null + && _lastMovingData.Length == data.Length + ) { - if (_lastMovingData == data || - new ReadOnlySpan(_lastMovingData).SequenceEqual(new ReadOnlySpan(data))) + if ( + _lastMovingData == data + || new ReadOnlySpan(_lastMovingData).SequenceEqual( + new ReadOnlySpan(data) + ) + ) { return; } @@ -225,9 +274,11 @@ namespace OpenNest.Gpu Index1D index, ArrayView1D raw, ArrayView1D prepared, - int count) + int count + ) { - if (index >= count) return; + if (index >= count) + return; var x1 = raw[index * 4 + 0]; var y1 = raw[index * 4 + 1]; var x2 = raw[index * 4 + 2]; @@ -259,15 +310,26 @@ namespace OpenNest.Gpu ArrayView1D movingPrep, ArrayView1D offsets, ArrayView1D results, - int sCount, int mCount, int direction) + int sCount, + int mCount, + int direction + ) { - if (index >= results.Length) return; + if (index >= results.Length) + return; var dx = offsets[index * 2]; var dy = offsets[index * 2 + 1]; results[index] = ComputeSlideLean( - stationaryPrep, movingPrep, dx, dy, sCount, mCount, direction); + stationaryPrep, + movingPrep, + dx, + dy, + sCount, + mCount, + direction + ); } private static void SlideKernelMultiDir( @@ -277,22 +339,37 @@ namespace OpenNest.Gpu ArrayView1D offsets, ArrayView1D results, ArrayView1D directions, - int sCount, int mCount) + int sCount, + int mCount + ) { - if (index >= results.Length) return; + if (index >= results.Length) + return; var dx = offsets[index * 2]; var dy = offsets[index * 2 + 1]; var dir = directions[index]; results[index] = ComputeSlideLean( - stationaryPrep, movingPrep, dx, dy, sCount, mCount, dir); + stationaryPrep, + movingPrep, + dx, + dy, + sCount, + mCount, + dir + ); } private static double ComputeSlideLean( ArrayView1D sPrep, ArrayView1D mPrep, - double dx, double dy, int sCount, int mCount, int direction) + double dx, + double dy, + int sCount, + int mCount, + int direction + ) { const double eps = 0.00001; var minDist = double.MaxValue; @@ -317,7 +394,8 @@ namespace OpenNest.Gpu if (mv1 >= sMin - eps && mv1 <= sMax + eps) { var d = RayEdgeLean(m1x, m1y, sPrep, j, direction, eps); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } // Test moving vertex 2 against stationary edge j @@ -325,7 +403,8 @@ namespace OpenNest.Gpu if (mv2 >= sMin - eps && mv2 <= sMax + eps) { var d = RayEdgeLean(m2x, m2y, sPrep, j, direction, eps); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } } } @@ -348,7 +427,8 @@ namespace OpenNest.Gpu if (sv1 >= mMin - eps && sv1 <= mMax + eps) { var d = RayEdgeLeanMoving(s1x, s1y, mPrep, j, dx, dy, oppDir, eps); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } // Test stationary vertex 2 against moving edge j @@ -356,7 +436,8 @@ namespace OpenNest.Gpu if (sv2 >= mMin - eps && sv2 <= mMax + eps) { var d = RayEdgeLeanMoving(s2x, s2y, mPrep, j, dx, dy, oppDir, eps); - if (d < minDist) minDist = d; + if (d < minDist) + minDist = d; } } } @@ -365,9 +446,13 @@ namespace OpenNest.Gpu } private static double RayEdgeLean( - double vx, double vy, - ArrayView1D sPrep, int j, - int direction, double eps) + double vx, + double vy, + ArrayView1D sPrep, + int j, + int direction, + double eps + ) { var p1x = sPrep[j * 10 + 0]; var p1y = sPrep[j * 10 + 1]; @@ -377,37 +462,49 @@ namespace OpenNest.Gpu if (direction >= 2) // Horizontal (Left=2, Right=3) { var invDy = sPrep[j * 10 + 5]; - if (invDy == 0) return double.MaxValue; + if (invDy == 0) + return double.MaxValue; var t = (vy - p1y) * invDy; - if (t < -eps || t > 1.0 + eps) return double.MaxValue; + if (t < -eps || t > 1.0 + eps) + return double.MaxValue; var ix = p1x + t * (p2x - p1x); var dist = (direction == 2) ? (vx - ix) : (ix - vx); - if (dist > eps) return dist; + if (dist > eps) + return dist; return (dist >= -eps) ? 0.0 : double.MaxValue; } else // Vertical (Up=0, Down=1) { var invDx = sPrep[j * 10 + 4]; - if (invDx == 0) return double.MaxValue; + if (invDx == 0) + return double.MaxValue; var t = (vx - p1x) * invDx; - if (t < -eps || t > 1.0 + eps) return double.MaxValue; + if (t < -eps || t > 1.0 + eps) + return double.MaxValue; var iy = p1y + t * (p2y - p1y); var dist = (direction == 1) ? (vy - iy) : (iy - vy); - if (dist > eps) return dist; + if (dist > eps) + return dist; return (dist >= -eps) ? 0.0 : double.MaxValue; } } private static double RayEdgeLeanMoving( - double vx, double vy, - ArrayView1D mPrep, int j, - double dx, double dy, int direction, double eps) + double vx, + double vy, + ArrayView1D mPrep, + int j, + double dx, + double dy, + int direction, + double eps + ) { var p1x = mPrep[j * 10 + 0] + dx; var p1y = mPrep[j * 10 + 1] + dy; @@ -417,29 +514,35 @@ namespace OpenNest.Gpu if (direction >= 2) // Horizontal { var invDy = mPrep[j * 10 + 5]; - if (invDy == 0) return double.MaxValue; + if (invDy == 0) + return double.MaxValue; var t = (vy - p1y) * invDy; - if (t < -eps || t > 1.0 + eps) return double.MaxValue; + if (t < -eps || t > 1.0 + eps) + return double.MaxValue; var ix = p1x + t * (p2x - p1x); var dist = (direction == 2) ? (vx - ix) : (ix - vx); - if (dist > eps) return dist; + if (dist > eps) + return dist; return (dist >= -eps) ? 0.0 : double.MaxValue; } else // Vertical { var invDx = mPrep[j * 10 + 4]; - if (invDx == 0) return double.MaxValue; + if (invDx == 0) + return double.MaxValue; var t = (vx - p1x) * invDx; - if (t < -eps || t > 1.0 + eps) return double.MaxValue; + if (t < -eps || t > 1.0 + eps) + return double.MaxValue; var iy = p1y + t * (p2y - p1y); var dist = (direction == 1) ? (vy - iy) : (iy - vy); - if (dist > eps) return dist; + if (dist > eps) + return dist; return (dist >= -eps) ? 0.0 : double.MaxValue; } } diff --git a/OpenNest.Gpu/PartBitmap.cs b/OpenNest.Gpu/PartBitmap.cs index f75c2b3..ac89eb9 100644 --- a/OpenNest.Gpu/PartBitmap.cs +++ b/OpenNest.Gpu/PartBitmap.cs @@ -1,9 +1,9 @@ -using OpenNest.Converters; -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Generic; using System.Linq; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Gpu { @@ -18,13 +18,22 @@ namespace OpenNest.Gpu public const double DefaultCellSize = 0.05; - public static PartBitmap FromDrawing(Drawing drawing, double cellSize = DefaultCellSize, double spacingDilation = 0) + public static PartBitmap FromDrawing( + Drawing drawing, + double cellSize = DefaultCellSize, + double spacingDilation = 0 + ) { var polygons = GetClosedPolygons(drawing); return Rasterize(polygons, cellSize, spacingDilation); } - public static PartBitmap FromDrawingRotated(Drawing drawing, double rotation, double cellSize = DefaultCellSize, double spacingDilation = 0) + public static PartBitmap FromDrawingRotated( + Drawing drawing, + double rotation, + double cellSize = DefaultCellSize, + double spacingDilation = 0 + ) { var polygons = GetClosedPolygons(drawing); @@ -45,7 +54,8 @@ namespace OpenNest.Gpu /// public static PartBitmap FromPart(Part part, double cellSize = DefaultCellSize) { - var entities = ConvertProgram.ToGeometry(part.Program) + var entities = ConvertProgram + .ToGeometry(part.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); @@ -65,10 +75,20 @@ namespace OpenNest.Gpu return Rasterize(polygons, cellSize, 0); } - private static PartBitmap Rasterize(List polygons, double cellSize, double spacingDilation) + private static PartBitmap Rasterize( + List polygons, + double cellSize, + double spacingDilation + ) { if (polygons.Count == 0) - return new PartBitmap { Cells = Array.Empty(), Width = 0, Height = 0, CellSize = cellSize }; + return new PartBitmap + { + Cells = Array.Empty(), + Width = 0, + Height = 0, + CellSize = cellSize, + }; var minX = double.MaxValue; var minY = double.MaxValue; @@ -79,10 +99,14 @@ namespace OpenNest.Gpu { poly.UpdateBounds(); var bb = poly.BoundingBox; - if (bb.Left < minX) minX = bb.Left; - if (bb.Bottom < minY) minY = bb.Bottom; - if (bb.Right > maxX) maxX = bb.Right; - if (bb.Top > maxY) maxY = bb.Top; + if (bb.Left < minX) + minX = bb.Left; + if (bb.Bottom < minY) + minY = bb.Bottom; + if (bb.Right > maxX) + maxX = bb.Right; + if (bb.Top > maxY) + maxY = bb.Top; } minX -= spacingDilation; @@ -94,7 +118,13 @@ namespace OpenNest.Gpu var height = (int)System.Math.Ceiling((maxY - minY) / cellSize); if (width <= 0 || height <= 0) - return new PartBitmap { Cells = Array.Empty(), Width = 0, Height = 0, CellSize = cellSize }; + return new PartBitmap + { + Cells = Array.Empty(), + Width = 0, + Height = 0, + CellSize = cellSize, + }; var cells = new int[width * height]; @@ -129,13 +159,14 @@ namespace OpenNest.Gpu Height = height, CellSize = cellSize, OriginX = minX, - OriginY = minY + OriginY = minY, }; } private static List GetClosedPolygons(Drawing drawing) { - var entities = ConvertProgram.ToGeometry(drawing.Program) + var entities = ConvertProgram + .ToGeometry(drawing.Program) .Where(e => e.Layer != SpecialLayers.Rapid); var shapes = ShapeBuilder.GetShapes(entities); @@ -160,7 +191,11 @@ namespace OpenNest.Gpu /// arrays of identical dimensions with no fractional offset math. /// public static (int[] cellsA, int[] cellsB, int width, int height) BlitPair( - PartBitmap bitmapA, PartBitmap bitmapB, double offsetX, double offsetY) + PartBitmap bitmapA, + PartBitmap bitmapB, + double offsetX, + double offsetY + ) { var cellSize = bitmapA.CellSize; @@ -173,10 +208,12 @@ namespace OpenNest.Gpu var combinedMinY = System.Math.Min(bitmapA.OriginY, bWorldOriginY); var combinedMaxX = System.Math.Max( bitmapA.OriginX + bitmapA.Width * cellSize, - bWorldOriginX + bitmapB.Width * cellSize); + bWorldOriginX + bitmapB.Width * cellSize + ); var combinedMaxY = System.Math.Max( bitmapA.OriginY + bitmapA.Height * cellSize, - bWorldOriginY + bitmapB.Height * cellSize); + bWorldOriginY + bitmapB.Height * cellSize + ); var sharedWidth = (int)System.Math.Ceiling((combinedMaxX - combinedMinX) / cellSize); var sharedHeight = (int)System.Math.Ceiling((combinedMaxY - combinedMinY) / cellSize); diff --git a/OpenNest.IO.Tests/BendRepairImportTests.cs b/OpenNest.IO.Tests/BendRepairImportTests.cs index 4ac260a..0c0b985 100644 --- a/OpenNest.IO.Tests/BendRepairImportTests.cs +++ b/OpenNest.IO.Tests/BendRepairImportTests.cs @@ -3,8 +3,8 @@ using ACadSharp.IO; using CSMath; using OpenNest.Geometry; using OpenNest.IO.Bending; -using CadLine = ACadSharp.Entities.Line; using CadLayer = ACadSharp.Tables.Layer; +using CadLine = ACadSharp.Entities.Line; namespace OpenNest.IO.Tests; @@ -15,31 +15,64 @@ public class BendRepairImportTests [InlineData("SCRIBE", false, false, "Repaired")] [InlineData("ETCH", true, false, "Skipped")] [InlineData("ETCH", false, true, "Skipped")] - public void ImportPreservesMarksAndHonorsAmbiguityAndHeader(string layer, bool duplicate, bool conflict, string status) + public void ImportPreservesMarksAndHonorsAmbiguityAndHeader( + string layer, + bool duplicate, + bool conflict, + string status + ) { var doc = Fixture(layer); - if (duplicate) doc.Entities.Add(new CadLine(new XYZ(0.05, 5, 0), new XYZ(0.55, 5, 0)) { Layer = new CadLayer(layer) }); - if (conflict) doc.Header.InsUnits = ACadSharp.Types.Units.UnitsType.Millimeters; - WithFile(doc, path => - { - var raw = Dxf.Import(path, preserveRepairMarks: true); - var result = CadImporter.Import(path, new CadImportOptions { BendRepair = Options() }); - Assert.Equal(status, Assert.Single(result.BendRepairReports).Status); - Assert.Equal(raw.Entities.Count, result.Entities.Count); - Assert.Equal(Signatures(raw.Entities.Where(e => e.Layer.Name == "0")), Signatures(result.Entities.Where(e => e.Layer.Name == "0"))); - Assert.Contains(result.Entities.OfType(), l => l.StartPoint == new Vector(4, 2) && l.EndPoint == new Vector(4, 3)); - if (status == "Skipped") Assert.Equal(Signatures(raw.Entities), Signatures(result.Entities)); - else + if (duplicate) + doc.Entities.Add( + new CadLine(new XYZ(0.05, 5, 0), new XYZ(0.55, 5, 0)) + { + Layer = new CadLayer(layer), + } + ); + if (conflict) + doc.Header.InsUnits = ACadSharp.Types.Units.UnitsType.Millimeters; + WithFile( + doc, + path => { - Assert.Equal(new Vector(0, 5), result.Bends[0].StartPoint); - Assert.Equal(new Vector(10, 5), result.Bends[0].EndPoint); - Assert.Equal("Unchanged", Assert.Single(BendRepair.Apply(result.Entities, result.Bends, Options())).Status); + var raw = Dxf.Import(path, preserveRepairMarks: true); + var result = CadImporter.Import( + path, + new CadImportOptions { BendRepair = Options() } + ); + Assert.Equal(status, Assert.Single(result.BendRepairReports).Status); + Assert.Equal(raw.Entities.Count, result.Entities.Count); + Assert.Equal( + Signatures(raw.Entities.Where(e => e.Layer.Name == "0")), + Signatures(result.Entities.Where(e => e.Layer.Name == "0")) + ); + Assert.Contains( + result.Entities.OfType(), + l => l.StartPoint == new Vector(4, 2) && l.EndPoint == new Vector(4, 3) + ); + if (status == "Skipped") + Assert.Equal(Signatures(raw.Entities), Signatures(result.Entities)); + else + { + Assert.Equal(new Vector(0, 5), result.Bends[0].StartPoint); + Assert.Equal(new Vector(10, 5), result.Bends[0].EndPoint); + Assert.Equal( + "Unchanged", + Assert + .Single(BendRepair.Apply(result.Entities, result.Bends, Options())) + .Status + ); + } + Assert.Empty(CadImporter.Import(path).BendRepairReports); + var disabled = CadImporter.Import( + path, + new CadImportOptions { DetectBends = false, BendRepair = Options() } + ); + Assert.Empty(disabled.Bends); + Assert.Equal(Signatures(raw.Entities), Signatures(disabled.Entities)); } - Assert.Empty(CadImporter.Import(path).BendRepairReports); - var disabled = CadImporter.Import(path, new CadImportOptions { DetectBends = false, BendRepair = Options() }); - Assert.Empty(disabled.Bends); - Assert.Equal(Signatures(raw.Entities), Signatures(disabled.Entities)); - }); + ); } [Fact] @@ -47,52 +80,101 @@ public class BendRepairImportTests { var doc = Fixture("ETCH"); doc.Header.InsUnits = ACadSharp.Types.Units.UnitsType.Unitless; - WithFile(doc, path => - { - var configured = CadImporter.Import(path, new CadImportOptions { BendRepair = Options() }); - Assert.Equal("Repaired", Assert.Single(configured.BendRepairReports).Status); - var unspecified = CadImporter.Import(path, new CadImportOptions { BendRepair = new BendRepairOptions { MaxEndpointMovementMillimeters = 2 } }); - Assert.Equal("Skipped", Assert.Single(unspecified.BendRepairReports).Status); - Assert.Equal(Signatures(Dxf.Import(path, true).Entities), Signatures(unspecified.Entities)); - }); + WithFile( + doc, + path => + { + var configured = CadImporter.Import( + path, + new CadImportOptions { BendRepair = Options() } + ); + Assert.Equal("Repaired", Assert.Single(configured.BendRepairReports).Status); + var unspecified = CadImporter.Import( + path, + new CadImportOptions + { + BendRepair = new BendRepairOptions { MaxEndpointMovementMillimeters = 2 }, + } + ); + Assert.Equal("Skipped", Assert.Single(unspecified.BendRepairReports).Status); + Assert.Equal( + Signatures(Dxf.Import(path, true).Entities), + Signatures(unspecified.Entities) + ); + } + ); } [Fact] public void MarkCircleDoesNotDeduplicateCutCircle() { var doc = Fixture("SCRIBE"); - doc.Entities.Add(new ACadSharp.Entities.Circle { Center = new XYZ(2, 2, 0), Radius = 0.2, Layer = new CadLayer("SCRIBE") }); + doc.Entities.Add( + new ACadSharp.Entities.Circle + { + Center = new XYZ(2, 2, 0), + Radius = 0.2, + Layer = new CadLayer("SCRIBE"), + } + ); doc.Entities.Add(new ACadSharp.Entities.Circle { Center = new XYZ(2, 2, 0), Radius = 0.2 }); - WithFile(doc, path => - { - var result = CadImporter.Import(path, new CadImportOptions { BendRepair = Options() }); - Assert.Equal(2, result.Entities.OfType().Count()); - Assert.Single(result.Entities.OfType().Where(e => e.Layer.Name == "0")); - Assert.Single(result.Entities.OfType().Where(e => e.Layer.Name == "SCRIBE")); - }); + WithFile( + doc, + path => + { + var result = CadImporter.Import( + path, + new CadImportOptions { BendRepair = Options() } + ); + Assert.Equal(2, result.Entities.OfType().Count()); + Assert.Single(result.Entities.OfType().Where(e => e.Layer.Name == "0")); + Assert.Single( + result.Entities.OfType().Where(e => e.Layer.Name == "SCRIBE") + ); + } + ); } - private static BendRepairOptions Options() => new() { DrawingUnits = BendRepairUnits.Inches, MaxEndpointMovementMillimeters = 2 }; + private static BendRepairOptions Options() => + new() { DrawingUnits = BendRepairUnits.Inches, MaxEndpointMovementMillimeters = 2 }; private static CadDocument Fixture(string layer) { var doc = new CadDocument(); doc.Header.InsUnits = ACadSharp.Types.Units.UnitsType.Inches; - foreach (var line in new[] { - new CadLine(new XYZ(0, 0, 0), new XYZ(10, 0, 0)), - new CadLine(new XYZ(10, 0, 0), new XYZ(10, 10, 0)), - new CadLine(new XYZ(10, 10, 0), new XYZ(0, 10, 0)), - new CadLine(new XYZ(0, 10, 0), new XYZ(0, 0, 0)), - new CadLine(new XYZ(0.05, 5, 0), new XYZ(0.55, 5, 0)) { Layer = new CadLayer(layer) }, - new CadLine(new XYZ(9.45, 5, 0), new XYZ(9.95, 5, 0)) { Layer = new CadLayer(layer) }, - new CadLine(new XYZ(4, 2, 0), new XYZ(4, 3, 0)) { Layer = new CadLayer(layer) }, - new CadLine(new XYZ(0.05, 5, 0), new XYZ(9.95, 5, 0)) { Layer = new CadLayer("BEND"), LineType = new ACadSharp.Tables.LineType("CENTER") } - }) doc.Entities.Add(line); + foreach ( + var line in new[] + { + new CadLine(new XYZ(0, 0, 0), new XYZ(10, 0, 0)), + new CadLine(new XYZ(10, 0, 0), new XYZ(10, 10, 0)), + new CadLine(new XYZ(10, 10, 0), new XYZ(0, 10, 0)), + new CadLine(new XYZ(0, 10, 0), new XYZ(0, 0, 0)), + new CadLine(new XYZ(0.05, 5, 0), new XYZ(0.55, 5, 0)) + { + Layer = new CadLayer(layer), + }, + new CadLine(new XYZ(9.45, 5, 0), new XYZ(9.95, 5, 0)) + { + Layer = new CadLayer(layer), + }, + new CadLine(new XYZ(4, 2, 0), new XYZ(4, 3, 0)) { Layer = new CadLayer(layer) }, + new CadLine(new XYZ(0.05, 5, 0), new XYZ(9.95, 5, 0)) + { + Layer = new CadLayer("BEND"), + LineType = new ACadSharp.Tables.LineType("CENTER"), + }, + } + ) + doc.Entities.Add(line); return doc; } - private static string[] Signatures(IEnumerable entities) => entities.OfType() - .Select(l => $"{l.Layer.Name}:{l.StartPoint}:{l.EndPoint}:{l.LineTypeName}").Order().ToArray(); + private static string[] Signatures(IEnumerable entities) => + entities + .OfType() + .Select(l => $"{l.Layer.Name}:{l.StartPoint}:{l.EndPoint}:{l.LineTypeName}") + .Order() + .ToArray(); private static void WithFile(CadDocument doc, Action action) { @@ -102,6 +184,9 @@ public class BendRepairImportTests DxfWriter.Write(path, doc, false); action(path); } - finally { File.Delete(path); } + finally + { + File.Delete(path); + } } } diff --git a/OpenNest.IO.Tests/BendRepairTests.cs b/OpenNest.IO.Tests/BendRepairTests.cs index 754c0e6..9da5663 100644 --- a/OpenNest.IO.Tests/BendRepairTests.cs +++ b/OpenNest.IO.Tests/BendRepairTests.cs @@ -6,10 +6,15 @@ namespace OpenNest.IO.Tests; public class BendRepairTests { - private static BendRepairOptions Options(BendRepairUnits units = BendRepairUnits.Inches, double limit = 2) => - new() { DrawingUnits = units, MaxEndpointMovementMillimeters = limit }; + private static BendRepairOptions Options( + BendRepairUnits units = BendRepairUnits.Inches, + double limit = 2 + ) => new() { DrawingUnits = units, MaxEndpointMovementMillimeters = limit }; - private static (List entities, List bends) Fixture(double start = 0.05, double end = 9.95) + private static (List entities, List bends) Fixture( + double start = 0.05, + double end = 9.95 + ) { var entities = new List { @@ -17,36 +22,65 @@ public class BendRepairTests new Line(new Vector(10, 0), new Vector(10, 10)), new Line(new Vector(10, 10), new Vector(0, 10)), new Line(new Vector(0, 10), new Vector(0, 0)), - Mark(start, 5, start + 0.5, 5), Mark(end - 0.5, 5, end, 5), - Mark(4, 2, 4, 3) + Mark(start, 5, start + 0.5, 5), + Mark(end - 0.5, 5, end, 5), + Mark(4, 2, 4, 3), }; - return (entities, new List { new() { StartPoint = new Vector(start, 5), EndPoint = new Vector(end, 5), Direction = BendDirection.Up } }); + return ( + entities, + new List + { + new() + { + StartPoint = new Vector(start, 5), + EndPoint = new Vector(end, 5), + Direction = BendDirection.Up, + }, + } + ); } private static Line Mark(double x, double y, double x2, double y2) => - new(new Vector(x, y), new Vector(x2, y2)) { Layer = new Layer("SCRIBE") { IsVisible = true } }; + new(new Vector(x, y), new Vector(x2, y2)) + { + Layer = new Layer("SCRIBE") { IsVisible = true }, + }; [Theory] [InlineData(0.05, 9.95)] [InlineData(-0.05, 10.05)] [InlineData(0, 9.95)] - public void RepairsAlongAxisPreservingCutAndUnrelatedMarksAndIsIdempotent(double start, double end) + public void RepairsAlongAxisPreservingCutAndUnrelatedMarksAndIsIdempotent( + double start, + double end + ) { var (entities, bends) = Fixture(start, end); var originals = entities.ToArray(); - var cutPoints = entities.Take(4).Cast().Select(l => (l.StartPoint, l.EndPoint)).ToArray(); + var cutPoints = entities + .Take(4) + .Cast() + .Select(l => (l.StartPoint, l.EndPoint)) + .ToArray(); var report = Assert.Single(BendRepair.Apply(entities, bends, Options())); Assert.Equal("Repaired", report.Status); Assert.Equal(new Vector(0, 5), bends[0].StartPoint); Assert.Equal(new Vector(10, 5), bends[0].EndPoint); - for (var i = 0; i < 4; i++) Assert.Same(originals[i], entities[i]); - Assert.Equal(cutPoints, entities.Take(4).Cast().Select(l => (l.StartPoint, l.EndPoint)).ToArray()); + for (var i = 0; i < 4; i++) + Assert.Same(originals[i], entities[i]); + Assert.Equal( + cutPoints, + entities.Take(4).Cast().Select(l => (l.StartPoint, l.EndPoint)).ToArray() + ); Assert.Same(originals[6], entities[6]); Assert.Equal(new Vector(0, 5), ((Line)entities[4]).StartPoint); Assert.Equal(new Vector(10, 5), ((Line)entities[5]).EndPoint); Assert.Equal(0.5, entities[4].Length, 8); var after = entities.ToArray(); - Assert.Equal("Unchanged", Assert.Single(BendRepair.Apply(entities, bends, Options())).Status); + Assert.Equal( + "Unchanged", + Assert.Single(BendRepair.Apply(entities, bends, Options())).Status + ); Assert.Equal(after, entities); } @@ -56,18 +90,30 @@ public class BendRepairTests var (entities, bends) = Fixture(); entities[4] = Mark(0.05, 5, 1.05, 5); entities[5] = Mark(8.95, 5, 9.95, 5); - Assert.Equal("Repaired", Assert.Single(BendRepair.Apply(entities, bends, Options())).Status); - Assert.Equal("Unchanged", Assert.Single(BendRepair.Apply(entities, bends, Options())).Status); + Assert.Equal( + "Repaired", + Assert.Single(BendRepair.Apply(entities, bends, Options())).Status + ); + Assert.Equal( + "Unchanged", + Assert.Single(BendRepair.Apply(entities, bends, Options())).Status + ); } [Fact] public void MillimeterCoordinatesUseSamePhysicalLimit() { var (entities, bends) = Fixture(); - foreach (var entity in entities) entity.Scale(25.4); + foreach (var entity in entities) + entity.Scale(25.4); bends[0].StartPoint *= 25.4; bends[0].EndPoint *= 25.4; - Assert.Equal("Repaired", Assert.Single(BendRepair.Apply(entities, bends, Options(BendRepairUnits.Millimeters))).Status); + Assert.Equal( + "Repaired", + Assert + .Single(BendRepair.Apply(entities, bends, Options(BendRepairUnits.Millimeters))) + .Status + ); Assert.Equal(254, bends[0].EndPoint.X, 8); } @@ -75,12 +121,16 @@ public class BendRepairTests public void RotatedAxisIsNotRotatedByRepair() { var (entities, bends) = Fixture(); - foreach (var entity in entities) entity.Rotate(0.7); + foreach (var entity in entities) + entity.Rotate(0.7); var axis = bends[0].ToLine(); axis.Rotate(0.7); bends[0].StartPoint = axis.StartPoint; bends[0].EndPoint = axis.EndPoint; - Assert.Equal("Repaired", Assert.Single(BendRepair.Apply(entities, bends, Options())).Status); + Assert.Equal( + "Repaired", + Assert.Single(BendRepair.Apply(entities, bends, Options())).Status + ); Assert.Equal(0.7, bends[0].LineAngle, 8); Assert.Equal(10, bends[0].Length, 8); } @@ -102,17 +152,43 @@ public class BendRepairTests var (entities, bends) = Fixture(); switch (failure) { - case "missing": entities.RemoveAt(5); break; - case "duplicate": entities.Add(entities[4].Clone()); break; - case "perpendicular": entities[5] = Mark(9.95, 5, 9.95, 5.5); break; - case "offset": entities[5].Offset(0, 0.01); break; - case "excessive": bends[0].EndPoint = new Vector(9, 5); entities[5] = Mark(8.5, 5, 9, 5); break; - case "open": entities.RemoveAt(0); break; - case "hole": entities.Add(new Circle(new Vector(5, 5), 1)); break; - case "shared": bends.Add(new Bend { StartPoint = bends[0].StartPoint, EndPoint = bends[0].EndPoint }); break; - case "unknown-layer": foreach (var e in entities.Take(4)) e.Layer = new Layer("UNKNOWN"); break; - case "cut-tick": entities[5].Layer = Layer.Default; break; - case "nonfinite": bends[0].StartPoint = new Vector(double.NaN, 5); break; + case "missing": + entities.RemoveAt(5); + break; + case "duplicate": + entities.Add(entities[4].Clone()); + break; + case "perpendicular": + entities[5] = Mark(9.95, 5, 9.95, 5.5); + break; + case "offset": + entities[5].Offset(0, 0.01); + break; + case "excessive": + bends[0].EndPoint = new Vector(9, 5); + entities[5] = Mark(8.5, 5, 9, 5); + break; + case "open": + entities.RemoveAt(0); + break; + case "hole": + entities.Add(new Circle(new Vector(5, 5), 1)); + break; + case "shared": + bends.Add( + new Bend { StartPoint = bends[0].StartPoint, EndPoint = bends[0].EndPoint } + ); + break; + case "unknown-layer": + foreach (var e in entities.Take(4)) + e.Layer = new Layer("UNKNOWN"); + break; + case "cut-tick": + entities[5].Layer = Layer.Default; + break; + case "nonfinite": + bends[0].StartPoint = new Vector(double.NaN, 5); + break; } var before = entities.ToArray(); var start = bends[0].StartPoint; @@ -136,7 +212,10 @@ public class BendRepairTests { var (entities, bends) = Fixture(); var before = entities.ToArray(); - Assert.Equal("Skipped", Assert.Single(BendRepair.Apply(entities, bends, Options(units, limit))).Status); + Assert.Equal( + "Skipped", + Assert.Single(BendRepair.Apply(entities, bends, Options(units, limit))).Status + ); Assert.Equal(before, entities); Assert.Equal(0.05, bends[0].StartPoint.X); } diff --git a/OpenNest.IO/Bending/BendDetectorRegistry.cs b/OpenNest.IO/Bending/BendDetectorRegistry.cs index d78c6b2..f6a1936 100644 --- a/OpenNest.IO/Bending/BendDetectorRegistry.cs +++ b/OpenNest.IO/Bending/BendDetectorRegistry.cs @@ -1,7 +1,7 @@ -using ACadSharp; -using OpenNest.Bending; using System.Collections.Generic; using System.Linq; +using ACadSharp; +using OpenNest.Bending; namespace OpenNest.IO.Bending { diff --git a/OpenNest.IO/Bending/IBendDetector.cs b/OpenNest.IO/Bending/IBendDetector.cs index 900b24c..b7b0e45 100644 --- a/OpenNest.IO/Bending/IBendDetector.cs +++ b/OpenNest.IO/Bending/IBendDetector.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using ACadSharp; using OpenNest.Bending; -using System.Collections.Generic; namespace OpenNest.IO.Bending { diff --git a/OpenNest.IO/Bending/SolidWorksBendDetector.cs b/OpenNest.IO/Bending/SolidWorksBendDetector.cs index 251c019..5d0e44e 100644 --- a/OpenNest.IO/Bending/SolidWorksBendDetector.cs +++ b/OpenNest.IO/Bending/SolidWorksBendDetector.cs @@ -1,12 +1,12 @@ -using ACadSharp; -using ACadSharp.Entities; -using OpenNest.Bending; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; +using ACadSharp; +using ACadSharp.Entities; +using OpenNest.Bending; +using OpenNest.Geometry; namespace OpenNest.IO.Bending { @@ -18,15 +18,18 @@ namespace OpenNest.IO.Bending private static readonly Regex BendNoteRegex = new Regex( @"(?UP|DOWN|DN)\s+(?\d+(\.\d+)?)[^A-Z\d]*R\s*(?\d+(\.\d+)?)", - RegexOptions.Compiled | RegexOptions.IgnoreCase); + RegexOptions.Compiled | RegexOptions.IgnoreCase + ); private static readonly Regex MTextFormatRegex = new Regex( @"\\[fHCTQWASpOoLlKk][^;]*;|\\P|[{}]|%%[dDpPcC]", - RegexOptions.Compiled); + RegexOptions.Compiled + ); private static readonly Regex UnicodeEscapeRegex = new Regex( @"\\U\+([0-9A-Fa-f]{4})", - RegexOptions.Compiled); + RegexOptions.Compiled + ); public List DetectBends(CadDocument document) { @@ -47,7 +50,7 @@ namespace OpenNest.IO.Bending { StartPoint = start, EndPoint = end, - Direction = BendDirection.Unknown + Direction = BendDirection.Unknown, }; var note = FindClosestBendNote(line, bendNotes); @@ -101,7 +104,12 @@ namespace OpenNest.IO.Bending } } - private static bool AreCollinear(Bend a, Bend b, double angleTolerance, double distanceTolerance) + private static bool AreCollinear( + Bend a, + Bend b, + double angleTolerance, + double distanceTolerance + ) { var angleA = a.StartPoint.AngleTo(a.EndPoint); var angleB = b.StartPoint.AngleTo(b.EndPoint); @@ -114,7 +122,8 @@ namespace OpenNest.IO.Bending // Perpendicular distance from midpoint of A to the infinite line through B var midA = new Vector( (a.StartPoint.X + a.EndPoint.X) / 2.0, - (a.StartPoint.Y + a.EndPoint.Y) / 2.0); + (a.StartPoint.Y + a.EndPoint.Y) / 2.0 + ); var dx = b.EndPoint.X - b.StartPoint.X; var dy = b.EndPoint.Y - b.StartPoint.Y; @@ -133,18 +142,22 @@ namespace OpenNest.IO.Bending private List FindBendLines(CadDocument document) { - return document.Entities - .OfType() - .Where(l => (l.Layer?.Name == "BEND" || l.Layer?.Name == "0") - && (l.LineType?.Name?.Contains("CENTER") == true - || l.LineType?.Name == "CENTERX2")) + return document + .Entities.OfType() + .Where(l => + (l.Layer?.Name == "BEND" || l.Layer?.Name == "0") + && ( + l.LineType?.Name?.Contains("CENTER") == true + || l.LineType?.Name == "CENTERX2" + ) + ) .ToList(); } private List FindBendNotes(CadDocument document) { - return document.Entities - .OfType() + return document + .Entities.OfType() .Where(t => GetBendDirection(t.Value) != BendDirection.Unknown) .ToList(); } @@ -172,10 +185,24 @@ namespace OpenNest.IO.Bending if (match.Success) { - if (double.TryParse(match.Groups["radius"].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var radius)) + if ( + double.TryParse( + match.Groups["radius"].Value, + NumberStyles.Any, + CultureInfo.InvariantCulture, + out var radius + ) + ) bend.Radius = radius; - if (double.TryParse(match.Groups["angle"].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var angle)) + if ( + double.TryParse( + match.Groups["angle"].Value, + NumberStyles.Any, + CultureInfo.InvariantCulture, + out var angle + ) + ) bend.Angle = angle; } } @@ -186,17 +213,27 @@ namespace OpenNest.IO.Bending return text; // Convert \U+XXXX DXF unicode escapes to actual characters - var result = UnicodeEscapeRegex.Replace(text, m => - { - var codePoint = int.Parse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture); - return char.ConvertFromUtf32(codePoint); - }); + var result = UnicodeEscapeRegex.Replace( + text, + m => + { + var codePoint = int.Parse( + m.Groups[1].Value, + NumberStyles.HexNumber, + CultureInfo.InvariantCulture + ); + return char.ConvertFromUtf32(codePoint); + } + ); // Replace known DXF special characters result = result - .Replace("%%d", "°").Replace("%%D", "°") - .Replace("%%p", "±").Replace("%%P", "±") - .Replace("%%c", "⌀").Replace("%%C", "⌀"); + .Replace("%%d", "°") + .Replace("%%D", "°") + .Replace("%%p", "±") + .Replace("%%P", "±") + .Replace("%%c", "⌀") + .Replace("%%C", "⌀"); // Strip MText formatting codes and braces result = MTextFormatRegex.Replace(result, " "); @@ -207,7 +244,8 @@ namespace OpenNest.IO.Bending private MText FindClosestBendNote(ACadSharp.Entities.Line bendLine, List notes) { - if (notes.Count == 0) return null; + if (notes.Count == 0) + return null; MText closest = null; var closestDist = double.MaxValue; @@ -223,7 +261,8 @@ namespace OpenNest.IO.Bending var dist = notePos.DistanceTo(perpPoint); var maxAcceptable = note.Height * 2.0; - if (dist > maxAcceptable) continue; + if (dist > maxAcceptable) + continue; if (dist < closestDist) { diff --git a/OpenNest.IO/Bom/BomAnalyzer.cs b/OpenNest.IO/Bom/BomAnalyzer.cs index 6873447..a595a0e 100644 --- a/OpenNest.IO/Bom/BomAnalyzer.cs +++ b/OpenNest.IO/Bom/BomAnalyzer.cs @@ -62,8 +62,10 @@ namespace OpenNest.IO.Bom var lookupName = item.FileName; - if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) - || lookupName.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)) + if ( + lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) + || lookupName.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) + ) lookupName = Path.GetFileNameWithoutExtension(lookupName); if (!folderExists) @@ -86,13 +88,13 @@ namespace OpenNest.IO.Bom .GroupBy(p => new { Material = (p.Item.Material ?? "").ToUpperInvariant(), - Thickness = p.Item.Thickness.Value + Thickness = p.Item.Thickness.Value, }) .Select(g => new MaterialGroup { Material = g.First().Item.Material ?? "", Thickness = g.Key.Thickness, - Parts = g.ToList() + Parts = g.ToList(), }) .OrderBy(g => g.Material) .ThenBy(g => g.Thickness) diff --git a/OpenNest.IO/Bom/BomReader.cs b/OpenNest.IO/Bom/BomReader.cs index 2351fb4..203f122 100644 --- a/OpenNest.IO/Bom/BomReader.cs +++ b/OpenNest.IO/Bom/BomReader.cs @@ -1,8 +1,8 @@ -using ClosedXML.Excel; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using ClosedXML.Excel; namespace OpenNest.IO.Bom { @@ -20,7 +20,9 @@ namespace OpenNest.IO.Bom private IXLWorksheet GetPartsWorksheet() { if (!workbook.TryGetWorksheet("Parts", out var worksheet)) - throw new InvalidOperationException("BOM file does not contain a 'Parts' worksheet."); + throw new InvalidOperationException( + "BOM file does not contain a 'Parts' worksheet." + ); return worksheet; } @@ -41,7 +43,8 @@ namespace OpenNest.IO.Bom for (var columnIndex = 1; columnIndex <= lastColumn; columnIndex++) { var cell = worksheet.Cell(1, columnIndex); - if (cell.IsEmpty()) continue; + if (cell.IsEmpty()) + continue; var excelColumnName = cell.GetString().ToUpper(); var isMatch = classColumnNames.Any(n => n == excelColumnName); diff --git a/OpenNest.IO/Bom/CellExtensions.cs b/OpenNest.IO/Bom/CellExtensions.cs index 8d35afd..3e6e591 100644 --- a/OpenNest.IO/Bom/CellExtensions.cs +++ b/OpenNest.IO/Bom/CellExtensions.cs @@ -6,17 +6,23 @@ namespace OpenNest.IO.Bom { public static int? ToIntOrNull(this IXLCell cell) { - if (cell.IsEmpty()) return null; - if (cell.DataType == XLDataType.Number) return (int)cell.GetDouble(); - if (int.TryParse(cell.GetString(), out var i)) return i; + if (cell.IsEmpty()) + return null; + if (cell.DataType == XLDataType.Number) + return (int)cell.GetDouble(); + if (int.TryParse(cell.GetString(), out var i)) + return i; return null; } public static double? ToDoubleOrNull(this IXLCell cell) { - if (cell.IsEmpty()) return null; - if (cell.DataType == XLDataType.Number) return cell.GetDouble(); - if (double.TryParse(cell.GetString(), out var result)) return result; + if (cell.IsEmpty()) + return null; + if (cell.DataType == XLDataType.Number) + return cell.GetDouble(); + if (double.TryParse(cell.GetString(), out var result)) + return result; return null; } } diff --git a/OpenNest.IO/CadImportResult.cs b/OpenNest.IO/CadImportResult.cs index b41013b..46dfafa 100644 --- a/OpenNest.IO/CadImportResult.cs +++ b/OpenNest.IO/CadImportResult.cs @@ -24,7 +24,8 @@ namespace OpenNest.IO /// public List Bends { get; set; } = new List(); - public List BendRepairReports { get; set; } = new List(); + public List BendRepairReports { get; set; } = + new List(); /// /// Bounding box of at import time. May be stale diff --git a/OpenNest.IO/CadImporter.cs b/OpenNest.IO/CadImporter.cs index d0a83c2..c363838 100644 --- a/OpenNest.IO/CadImporter.cs +++ b/OpenNest.IO/CadImporter.cs @@ -26,8 +26,10 @@ namespace OpenNest.IO var dxf = Dxf.Import(path, preserveRepairMarks: options.BendRepair != null); - var cleanup = options.BendRepair == null ? dxf.Entities : dxf.Entities - .Where(e => !IsRepairMark(e)).ToList(); + var cleanup = + options.BendRepair == null + ? dxf.Entities + : dxf.Entities.Where(e => !IsRepairMark(e)).ToList(); RemoveDuplicateArcs(cleanup); RemoveZeroSweepArcs(cleanup); if (options.BendRepair != null) @@ -36,11 +38,13 @@ namespace OpenNest.IO 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(); + bends = + options.BendDetectorName == null + ? BendDetectorRegistry.AutoDetect(dxf.Document) + : BendDetectorRegistry + .GetByName(options.BendDetectorName) + ?.DetectBends(dxf.Document) + ?? new List(); } var repairReports = new List(); @@ -50,11 +54,23 @@ namespace OpenNest.IO { // 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; + 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(); + 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); } @@ -85,7 +101,8 @@ namespace OpenNest.IO result.Bends, options.Quantity, options.Customer, - editedProgram: null); + editedProgram: null + ); } /// @@ -119,7 +136,8 @@ namespace OpenNest.IO IEnumerable bends, int quantity, string customer, - OpenNest.CNC.Program editedProgram) + OpenNest.CNC.Program editedProgram + ) { var visible = entities as IList ?? new List(entities); var bendList = bends as IList ?? new List(bends); @@ -128,7 +146,11 @@ namespace OpenNest.IO var pgm = ConvertGeometry.ToProgram(normalized); var offset = Vector.Zero; - if (pgm != null && pgm.Codes.Count > 0 && pgm[0].Type == OpenNest.CNC.CodeType.RapidMove) + if ( + pgm != null + && pgm.Codes.Count > 0 + && pgm[0].Type == OpenNest.CNC.CodeType.RapidMove + ) { var rapid = (OpenNest.CNC.RapidMove)pgm[0]; offset = rapid.EndPoint; @@ -147,16 +169,18 @@ namespace OpenNest.IO drawing.Program = editedProgram ?? pgm; var bendSources = new HashSet( - bendList.Where(b => b.SourceEntity != null).Select(b => b.SourceEntity)); + bendList.Where(b => b.SourceEntity != null).Select(b => b.SourceEntity) + ); - drawing.SourceEntities = result.Entities - .Where(e => !bendSources.Contains(e)) - .ToList(); + 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)); + drawing + .SourceEntities.Where(e => + !(e.Layer != null && e.Layer.IsVisible && e.IsVisible) + ) + .Select(e => e.Id) + ); return drawing; } @@ -168,7 +192,8 @@ namespace OpenNest.IO internal static void RemoveZeroSweepArcs(List entities) { entities.RemoveAll(e => - e is Arc arc && arc.StartAngle.IsEqualTo(arc.EndAngle, Tolerance.ChainTolerance)); + e is Arc arc && arc.StartAngle.IsEqualTo(arc.EndAngle, Tolerance.ChainTolerance) + ); } internal static void RemoveDuplicateArcs(List entities) diff --git a/OpenNest.IO/ChrFont.cs b/OpenNest.IO/ChrFont.cs index 4e8c9f6..14686b8 100644 --- a/OpenNest.IO/ChrFont.cs +++ b/OpenNest.IO/ChrFont.cs @@ -42,7 +42,12 @@ namespace OpenNest.IO return width; } - public List RenderText(string text, double height, Vector position, Layer layer = null) + public List RenderText( + string text, + double height, + Vector position, + Layer layer = null + ) { var scale = height / CapHeight; var entities = new List(); @@ -97,7 +102,8 @@ namespace OpenNest.IO while (i + 5 < data.Length) { var charCode = data[i] | (data[i + 1] << 8); - var offset = data[i + 2] | (data[i + 3] << 8) | (data[i + 4] << 16) | (data[i + 5] << 24); + var offset = + data[i + 2] | (data[i + 3] << 8) | (data[i + 4] << 16) | (data[i + 5] << 24); if (charCode < 0x20 || offset == 0 || offset >= data.Length) break; @@ -110,9 +116,10 @@ namespace OpenNest.IO { var (charCode, offset) = charTable[c]; - var nextOffset = c + 1 < charTable.Count - ? FindNextOffset(charTable, offset, data.Length) - : data.Length; + var nextOffset = + c + 1 < charTable.Count + ? FindNextOffset(charTable, offset, data.Length) + : data.Length; var glyph = ParseGlyph(data, offset, nextOffset); if (glyph != null) @@ -134,7 +141,11 @@ namespace OpenNest.IO return font; } - private static int FindNextOffset(List<(int charCode, int offset)> table, int currentOffset, int fileLength) + private static int FindNextOffset( + List<(int charCode, int offset)> table, + int currentOffset, + int fileLength + ) { var best = fileLength; foreach (var (_, off) in table) @@ -199,7 +210,8 @@ namespace OpenNest.IO private static int ReadBE16(byte[] data, int offset) { var val = (data[offset] << 8) | data[offset + 1]; - if (val > 32767) val -= 65536; + if (val > 32767) + val -= 65536; return val; } } @@ -234,19 +246,26 @@ namespace OpenNest.IO private const int ArcSamples = 16; - public List ToEntities(double scale, double offsetX, double offsetY, Layer layer = null) + public List ToEntities( + double scale, + double offsetX, + double offsetY, + Layer layer = null + ) { var entities = new List(); layer ??= Layer.Default; foreach (var stroke in Strokes) { - if (stroke.Count < 2) continue; + if (stroke.Count < 2) + continue; var segments = BuildSegments(stroke); foreach (var seg in segments) { - if (seg.Points.Count < 2) continue; + if (seg.Points.Count < 2) + continue; var scaled = new List(seg.Points.Count); foreach (var pt in seg.Points) @@ -324,14 +343,23 @@ namespace OpenNest.IO public bool HasCurves; } - private static void SampleCircularArc(List output, Vector p0, Vector pMid, Vector p1, int samples) + private static void SampleCircularArc( + List output, + Vector p0, + Vector pMid, + Vector p1, + int samples + ) { if (output.Count == 0 || output[^1].DistanceTo(p0) > 0.01) output.Add(p0); - double ax = p0.X, ay = p0.Y; - double bx = pMid.X, by = pMid.Y; - double cx = p1.X, cy = p1.Y; + double ax = p0.X, + ay = p0.Y; + double bx = pMid.X, + by = pMid.Y; + double cx = p1.X, + cy = p1.Y; var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); @@ -342,8 +370,18 @@ namespace OpenNest.IO return; } - var ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d; - var uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d; + var ux = + ( + (ax * ax + ay * ay) * (by - cy) + + (bx * bx + by * by) * (cy - ay) + + (cx * cx + cy * cy) * (ay - by) + ) / d; + var uy = + ( + (ax * ax + ay * ay) * (cx - bx) + + (bx * bx + by * by) * (ax - cx) + + (cx * cx + cy * cy) * (bx - ax) + ) / d; var radius = System.Math.Sqrt((ax - ux) * (ax - ux) + (ay - uy) * (ay - uy)); var a0 = System.Math.Atan2(ay - uy, ax - ux); @@ -351,10 +389,12 @@ namespace OpenNest.IO var a1 = System.Math.Atan2(cy - uy, cx - ux); var ccwSweep = a1 - a0; - while (ccwSweep <= 0) ccwSweep += 2 * System.Math.PI; + while (ccwSweep <= 0) + ccwSweep += 2 * System.Math.PI; var midRel = am - a0; - while (midRel < 0) midRel += 2 * System.Math.PI; + while (midRel < 0) + midRel += 2 * System.Math.PI; var sweep = midRel < ccwSweep ? ccwSweep : ccwSweep - 2 * System.Math.PI; @@ -362,7 +402,12 @@ namespace OpenNest.IO { var t = (double)i / samples; var angle = a0 + sweep * t; - output.Add(new Vector(ux + radius * System.Math.Cos(angle), uy + radius * System.Math.Sin(angle))); + output.Add( + new Vector( + ux + radius * System.Math.Cos(angle), + uy + radius * System.Math.Sin(angle) + ) + ); } } } diff --git a/OpenNest.IO/Dxf.cs b/OpenNest.IO/Dxf.cs index 3e4387d..12b0b8a 100644 --- a/OpenNest.IO/Dxf.cs +++ b/OpenNest.IO/Dxf.cs @@ -1,13 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; using ACadSharp; using ACadSharp.IO; using CSMath; using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; namespace OpenNest.IO { @@ -35,14 +35,12 @@ namespace OpenNest.IO if (preserveRepairMarks) { // Keep source marks separate: optimization could merge two ticks or unrelated scribing. - entities.AddRange(ConvertEntities(doc, name => !IsRepairMarkLayer(name), optimize: false)); + entities.AddRange( + ConvertEntities(doc, name => !IsRepairMarkLayer(name), optimize: false) + ); } - return new DxfImportResult - { - Entities = entities, - Document = doc - }; + return new DxfImportResult { Entities = entities, Document = doc }; } public static List GetGeometry(string path) @@ -167,7 +165,11 @@ namespace OpenNest.IO } } - private static List ConvertEntities(CadDocument doc, Func layerFilter = null, bool optimize = true) + private static List ConvertEntities( + CadDocument doc, + Func layerFilter = null, + bool optimize = true + ) { var entities = new List(); var lines = new List(); @@ -197,8 +199,10 @@ namespace OpenNest.IO case ACadSharp.Entities.Spline spline: foreach (var e in spline.ToOpenNest()) { - if (e is Line l) lines.Add(l); - else if (e is Arc a) arcs.Add(a); + if (e is Line l) + lines.Add(l); + else if (e is Arc a) + arcs.Add(a); } break; @@ -213,8 +217,10 @@ namespace OpenNest.IO case ACadSharp.Entities.Ellipse ellipse: foreach (var e in ellipse.ToOpenNest()) { - if (e is Line l) lines.Add(l); - else if (e is Arc a) arcs.Add(a); + if (e is Line l) + lines.Add(l); + else if (e is Arc a) + arcs.Add(a); } break; } @@ -276,14 +282,17 @@ namespace OpenNest.IO { StartPoint = start, EndPoint = end, - Layer = layer + Layer = layer, }; Document.Entities.Add(ln); } public void AddPlateOutline(Plate plate) { - XYZ pt1, pt2, pt3, pt4; + XYZ pt1, + pt2, + pt3, + pt4; switch (plate.Quadrant) { @@ -324,7 +333,11 @@ namespace OpenNest.IO AddLine(pt3, pt4, PlateLayer); AddLine(pt4, pt1, PlateLayer); - var m1 = new XYZ(pt1.X + plate.EdgeSpacing.Left, pt1.Y + plate.EdgeSpacing.Bottom, 0); + var m1 = new XYZ( + pt1.X + plate.EdgeSpacing.Left, + pt1.Y + plate.EdgeSpacing.Bottom, + 0 + ); var m2 = new XYZ(m1.X, pt2.Y - plate.EdgeSpacing.Top, 0); var m3 = new XYZ(pt3.X - plate.EdgeSpacing.Right, m2.Y, 0); var m4 = new XYZ(m3.X, m1.Y, 0); @@ -399,13 +412,9 @@ namespace OpenNest.IO center = new XYZ(center.X + CurPos.X, center.Y + CurPos.Y, 0); } - var startAngle = System.Math.Atan2( - CurPos.Y - center.Y, - CurPos.X - center.X); + var startAngle = System.Math.Atan2(CurPos.Y - center.Y, CurPos.X - center.X); - var endAngle = System.Math.Atan2( - endpt.Y - center.Y, - endpt.X - center.X); + var endAngle = System.Math.Atan2(endpt.Y - center.Y, endpt.X - center.X); if (arc.Rotation == RotationType.CW) Generic.Swap(ref startAngle, ref endAngle); @@ -420,7 +429,7 @@ namespace OpenNest.IO { Center = center, Radius = radius, - Layer = CutLayer + Layer = CutLayer, }; Document.Entities.Add(circle); } @@ -432,7 +441,7 @@ namespace OpenNest.IO Radius = radius, StartAngle = startAngle, EndAngle = endAngle, - Layer = CutLayer + Layer = CutLayer, }; Document.Entities.Add(acadArc); } diff --git a/OpenNest.IO/DxfImportResult.cs b/OpenNest.IO/DxfImportResult.cs index d329cef..3feaed7 100644 --- a/OpenNest.IO/DxfImportResult.cs +++ b/OpenNest.IO/DxfImportResult.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using ACadSharp; using OpenNest.Geometry; -using System.Collections.Generic; namespace OpenNest.IO { diff --git a/OpenNest.IO/EntitySerializer.cs b/OpenNest.IO/EntitySerializer.cs index 0f71646..d9570c7 100644 --- a/OpenNest.IO/EntitySerializer.cs +++ b/OpenNest.IO/EntitySerializer.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; +using OpenNest.Geometry; using static OpenNest.IO.NestFormat; namespace OpenNest.IO @@ -13,7 +13,7 @@ namespace OpenNest.IO return new EntitySetDto { Entities = entities.Select(ToEntityDto).ToList(), - Suppressed = suppressed.Select(id => id.ToString()).ToList() + Suppressed = suppressed.Select(id => id.ToString()).ToList(), }; } @@ -39,7 +39,7 @@ namespace OpenNest.IO X1 = line.StartPoint.X, Y1 = line.StartPoint.Y, X2 = line.EndPoint.X, - Y2 = line.EndPoint.Y + Y2 = line.EndPoint.Y, }; case EntityType.Arc: @@ -55,7 +55,7 @@ namespace OpenNest.IO R = arc.Radius, StartAngle = arc.StartAngle, EndAngle = arc.EndAngle, - Reversed = arc.IsReversed + Reversed = arc.IsReversed, }; case EntityType.Circle: @@ -69,11 +69,13 @@ namespace OpenNest.IO CX = circle.Center.X, CY = circle.Center.Y, R = circle.Radius, - Rotation = circle.Rotation == RotationType.CW ? "CW" : "CCW" + Rotation = circle.Rotation == RotationType.CW ? "CW" : "CCW", }; default: - throw new NotSupportedException($"Entity type {entity.Type} is not supported for serialization."); + throw new NotSupportedException( + $"Entity type {entity.Type} is not supported for serialization." + ); } } @@ -84,9 +86,7 @@ namespace OpenNest.IO switch (dto.Type) { case "line": - entity = new Line( - new Vector(dto.X1, dto.Y1), - new Vector(dto.X2, dto.Y2)); + entity = new Line(new Vector(dto.X1, dto.Y1), new Vector(dto.X2, dto.Y2)); break; case "arc": @@ -95,7 +95,8 @@ namespace OpenNest.IO dto.R, dto.StartAngle, dto.EndAngle, - dto.Reversed); + dto.Reversed + ); break; case "circle": @@ -105,7 +106,9 @@ namespace OpenNest.IO break; default: - throw new NotSupportedException($"Entity type '{dto.Type}' is not supported for deserialization."); + throw new NotSupportedException( + $"Entity type '{dto.Type}' is not supported for deserialization." + ); } entity.Id = Guid.Parse(dto.Id); diff --git a/OpenNest.IO/Extensions.cs b/OpenNest.IO/Extensions.cs index 5128b1b..9fb2c0d 100644 --- a/OpenNest.IO/Extensions.cs +++ b/OpenNest.IO/Extensions.cs @@ -1,10 +1,10 @@ -using ACadSharp.Entities; -using CSMath; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using ACadSharp.Entities; +using CSMath; +using OpenNest.Geometry; namespace OpenNest.IO { @@ -23,11 +23,14 @@ namespace OpenNest.IO public static Geometry.Arc ToOpenNest(this ACadSharp.Entities.Arc arc) { var result = new Geometry.Arc( - arc.Center.X, arc.Center.Y, arc.Radius, + arc.Center.X, + arc.Center.Y, + arc.Radius, arc.StartAngle, - arc.EndAngle) + arc.EndAngle + ) { - Layer = arc.Layer.ToOpenNest() + Layer = arc.Layer.ToOpenNest(), }; result.ApplyDxfProperties(arc); return result; @@ -35,11 +38,9 @@ namespace OpenNest.IO public static Geometry.Circle ToOpenNest(this ACadSharp.Entities.Circle circle) { - var result = new Geometry.Circle( - circle.Center.X, circle.Center.Y, - circle.Radius) + var result = new Geometry.Circle(circle.Center.X, circle.Center.Y, circle.Radius) { - Layer = circle.Layer.ToOpenNest() + Layer = circle.Layer.ToOpenNest(), }; result.ApplyDxfProperties(circle); return result; @@ -48,10 +49,13 @@ namespace OpenNest.IO public static Geometry.Line ToOpenNest(this ACadSharp.Entities.Line line) { var result = new Geometry.Line( - line.StartPoint.X, line.StartPoint.Y, - line.EndPoint.X, line.EndPoint.Y) + line.StartPoint.X, + line.StartPoint.Y, + line.EndPoint.X, + line.EndPoint.Y + ) { - Layer = line.Layer.ToOpenNest() + Layer = line.Layer.ToOpenNest(), }; result.ApplyDxfProperties(line); return result; @@ -115,25 +119,30 @@ namespace OpenNest.IO { var nextPoint = polyline.Vertices[i].Location.ToOpenNest(); - lines.Add(new Geometry.Line(lastPoint, nextPoint) - { - Layer = layer, - Color = color, - LineTypeName = lineTypeName - }); + lines.Add( + new Geometry.Line(lastPoint, nextPoint) + { + Layer = layer, + Color = color, + LineTypeName = lineTypeName, + } + ); lastPoint = nextPoint; } - var isClosed = (polyline.Flags & PolylineFlags.ClosedPolylineOrClosedPolygonMeshInM) != 0; + var isClosed = + (polyline.Flags & PolylineFlags.ClosedPolylineOrClosedPolygonMeshInM) != 0; if (isClosed) - lines.Add(new Geometry.Line(lastPoint, polyline.Vertices[0].Location.ToOpenNest()) - { - Layer = layer, - Color = color, - LineTypeName = lineTypeName - }); + lines.Add( + new Geometry.Line(lastPoint, polyline.Vertices[0].Location.ToOpenNest()) + { + Layer = layer, + Color = color, + LineTypeName = lineTypeName, + } + ); return lines; } @@ -154,12 +163,14 @@ namespace OpenNest.IO { var nextPoint = polyline.Vertices[i].ToOpenNest(); - lines.Add(new Geometry.Line(lastPoint, nextPoint) - { - Layer = layer, - Color = color, - LineTypeName = lineTypeName - }); + lines.Add( + new Geometry.Line(lastPoint, nextPoint) + { + Layer = layer, + Color = color, + LineTypeName = lineTypeName, + } + ); lastPoint = nextPoint; } @@ -167,17 +178,22 @@ namespace OpenNest.IO var isClosed = (polyline.Flags & LwPolylineFlags.Closed) != 0; if (isClosed) - lines.Add(new Geometry.Line(lastPoint, polyline.Vertices[0].ToOpenNest()) - { - Layer = layer, - Color = color, - LineTypeName = lineTypeName - }); + lines.Add( + new Geometry.Line(lastPoint, polyline.Vertices[0].ToOpenNest()) + { + Layer = layer, + Color = color, + LineTypeName = lineTypeName, + } + ); return lines; } - public static List ToOpenNest(this ACadSharp.Entities.Ellipse ellipse, double tolerance = 0.001) + public static List ToOpenNest( + this ACadSharp.Entities.Ellipse ellipse, + double tolerance = 0.001 + ) { var center = new Vector(ellipse.Center.X, ellipse.Center.Y); var majorAxis = new Vector(ellipse.MajorAxisEndPoint.X, ellipse.MajorAxisEndPoint.Y); @@ -201,8 +217,15 @@ namespace OpenNest.IO var color = ellipse.ResolveColor(); var lineTypeName = ellipse.ResolveLineTypeName(); - var entities = EllipseConverter.Convert(center, semiMajor, semiMinor, rotation, - startParam, endParam, tolerance); + var entities = EllipseConverter.Convert( + center, + semiMajor, + semiMinor, + rotation, + startParam, + endParam, + tolerance + ); foreach (var entity in entities) { @@ -220,7 +243,7 @@ namespace OpenNest.IO { Color = Color.FromArgb(layer.Color.R, layer.Color.G, layer.Color.B), IsVisible = layer.IsOn, - LineTypeName = layer.LineType?.Name + LineTypeName = layer.LineType?.Name, }; } @@ -238,13 +261,19 @@ namespace OpenNest.IO { var lt = entity.LineType; - if (lt == null || string.Equals(lt.Name, "ByLayer", System.StringComparison.OrdinalIgnoreCase)) + if ( + lt == null + || string.Equals(lt.Name, "ByLayer", System.StringComparison.OrdinalIgnoreCase) + ) return entity.Layer.LineType?.Name ?? "Continuous"; return lt.Name; } - public static void ApplyDxfProperties(this Geometry.Entity target, ACadSharp.Entities.Entity source) + public static void ApplyDxfProperties( + this Geometry.Entity target, + ACadSharp.Entities.Entity source + ) { target.Color = source.ResolveColor(); target.LineTypeName = source.ResolveLineTypeName(); diff --git a/OpenNest.IO/NestFormat.cs b/OpenNest.IO/NestFormat.cs index 08852f9..c150f6c 100644 --- a/OpenNest.IO/NestFormat.cs +++ b/OpenNest.IO/NestFormat.cs @@ -11,7 +11,7 @@ namespace OpenNest.IO public static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true + WriteIndented = true, }; public record NestDto diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs index 1ee5899..684578c 100644 --- a/OpenNest.IO/NestReader.cs +++ b/OpenNest.IO/NestReader.cs @@ -1,7 +1,3 @@ -using OpenNest.Bending; -using OpenNest.CNC; -using OpenNest.Engine.BestFit; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; @@ -9,6 +5,10 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Text.Json; +using OpenNest.Bending; +using OpenNest.CNC; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; using static OpenNest.IO.NestFormat; namespace OpenNest.IO @@ -49,7 +49,8 @@ namespace OpenNest.IO private string ReadEntry(string name) { - var entry = zipArchive.GetEntry(name) + var entry = + zipArchive.GetEntry(name) ?? throw new InvalidDataException($"Nest file is missing required entry '{name}'."); using var entryStream = entry.Open(); using var reader = new StreamReader(entryStream); @@ -62,7 +63,8 @@ namespace OpenNest.IO for (var i = 1; i <= count; i++) { var entry = zipArchive.GetEntry($"programs/program-{i}"); - if (entry == null) continue; + if (entry == null) + continue; using var entryStream = entry.Open(); var memStream = new MemoryStream(); @@ -120,7 +122,10 @@ namespace OpenNest.IO // Wire up SubProgramCall.Program references foreach (var code in parent.Codes) { - if (code is SubProgramCall call && parent.SubPrograms.TryGetValue(call.Id, out var sub)) + if ( + code is SubProgramCall call + && parent.SubPrograms.TryGetValue(call.Id, out var sub) + ) call.Program = sub; } } @@ -133,13 +138,16 @@ namespace OpenNest.IO return reader.Read(); } - private Dictionary entities, HashSet suppressed)> ReadEntitySets(int count) + private Dictionary entities, HashSet suppressed)> ReadEntitySets( + int count + ) { var result = new Dictionary, HashSet)>(); for (var i = 1; i <= count; i++) { var entry = zipArchive.GetEntry($"entities/entities-{i}"); - if (entry == null) continue; + if (entry == null) + continue; using var entryStream = entry.Open(); using var reader = new StreamReader(entryStream); @@ -150,8 +158,11 @@ namespace OpenNest.IO return result; } - private Dictionary BuildDrawings(NestDto dto, Dictionary programs, - Dictionary entities, HashSet suppressed)> entitySets) + private Dictionary BuildDrawings( + NestDto dto, + Dictionary programs, + Dictionary entities, HashSet suppressed)> entitySets + ) { var map = new Dictionary(); foreach (var d in dto.Drawings) @@ -165,7 +176,11 @@ namespace OpenNest.IO drawing.Constraints.StartAngle = d.Constraints.StartAngle; drawing.Constraints.EndAngle = d.Constraints.EndAngle; drawing.Constraints.Allow180Equivalent = d.Constraints.Allow180Equivalent; - drawing.Material = new Material(d.Material.Name, d.Material.Grade, d.Material.Density); + drawing.Material = new Material( + d.Material.Name, + d.Material.Grade, + d.Material.Density + ); drawing.Source.Path = d.Source.Path; drawing.Source.Offset = new Vector(d.Source.Offset.X, d.Source.Offset.Y); @@ -173,16 +188,23 @@ namespace OpenNest.IO { foreach (var b in d.Bends) { - drawing.Bends.Add(new Bend - { - StartPoint = new Vector(b.StartX, b.StartY), - EndPoint = new Vector(b.EndX, b.EndY), - Direction = Enum.TryParse(b.Direction, true, out var dir) - ? dir : BendDirection.Unknown, - Angle = b.Angle, - Radius = b.Radius, - NoteText = b.NoteText - }); + drawing.Bends.Add( + new Bend + { + StartPoint = new Vector(b.StartX, b.StartY), + EndPoint = new Vector(b.EndX, b.EndY), + Direction = Enum.TryParse( + b.Direction, + true, + out var dir + ) + ? dir + : BendDirection.Unknown, + Angle = b.Angle, + Radius = b.Radius, + NoteText = b.NoteText, + } + ); } } @@ -205,14 +227,16 @@ namespace OpenNest.IO foreach (var kvp in drawingMap) { var entry = zipArchive.GetEntry($"bestfits/bestfit-{kvp.Key}"); - if (entry == null) continue; + if (entry == null) + continue; using var entryStream = entry.Open(); using var reader = new StreamReader(entryStream); var json = reader.ReadToEnd(); var sets = JsonSerializer.Deserialize>(json, JsonOptions); - if (sets == null) continue; + if (sets == null) + continue; PopulateBestFitSets(kvp.Value, sets); } @@ -222,29 +246,37 @@ namespace OpenNest.IO { foreach (var set in sets) { - var results = set.Results.Select(r => new BestFitResult - { - Candidate = new PairCandidate + var results = set + .Results.Select(r => new BestFitResult { - Drawing = drawing, - Part1Rotation = r.Part1Rotation, - Part2Rotation = r.Part2Rotation, - Part2Offset = new Vector(r.Part2OffsetX, r.Part2OffsetY), - StrategyIndex = r.StrategyType, - TestNumber = r.TestNumber, - Spacing = r.CandidateSpacing - }, - RotatedArea = r.RotatedArea, - BoundingWidth = r.BoundingWidth, - BoundingHeight = r.BoundingHeight, - OptimalRotation = r.OptimalRotation, - Keep = r.Keep, - Reason = r.Reason, - TrueArea = r.TrueArea, - HullAngles = r.HullAngles - }).ToList(); + Candidate = new PairCandidate + { + Drawing = drawing, + Part1Rotation = r.Part1Rotation, + Part2Rotation = r.Part2Rotation, + Part2Offset = new Vector(r.Part2OffsetX, r.Part2OffsetY), + StrategyIndex = r.StrategyType, + TestNumber = r.TestNumber, + Spacing = r.CandidateSpacing, + }, + RotatedArea = r.RotatedArea, + BoundingWidth = r.BoundingWidth, + BoundingHeight = r.BoundingHeight, + OptimalRotation = r.OptimalRotation, + Keep = r.Keep, + Reason = r.Reason, + TrueArea = r.TrueArea, + HullAngles = r.HullAngles, + }) + .ToList(); - BestFitCache.Populate(drawing, set.PlateWidth, set.PlateHeight, set.Spacing, results); + BestFitCache.Populate( + drawing, + set.PlateWidth, + set.PlateHeight, + set.Spacing, + results + ); } } @@ -273,18 +305,25 @@ namespace OpenNest.IO nest.PlateDefaults.Size = new OpenNest.Geometry.Size(pd.Size.Width, pd.Size.Length); nest.PlateDefaults.Quadrant = pd.Quadrant; nest.PlateDefaults.PartSpacing = pd.PartSpacing; - nest.PlateDefaults.EdgeSpacing = new Spacing(pd.EdgeSpacing.Left, pd.EdgeSpacing.Bottom, pd.EdgeSpacing.Right, pd.EdgeSpacing.Top); + nest.PlateDefaults.EdgeSpacing = new Spacing( + pd.EdgeSpacing.Left, + pd.EdgeSpacing.Bottom, + pd.EdgeSpacing.Right, + pd.EdgeSpacing.Top + ); // Plate optimizer settings nest.SalvageRate = dto.SalvageRate; if (dto.PlateOptions != null) { - nest.PlateOptions = dto.PlateOptions.Select(o => new PlateOption - { - Width = o.Width, - Length = o.Length, - Cost = o.Cost, - }).ToList(); + nest.PlateOptions = dto + .PlateOptions.Select(o => new PlateOption + { + Width = o.Width, + Length = o.Length, + Cost = o.Cost, + }) + .ToList(); } // Drawings @@ -299,7 +338,12 @@ namespace OpenNest.IO plate.Quadrant = p.Quadrant; plate.Quantity = p.Quantity; plate.PartSpacing = p.PartSpacing; - plate.EdgeSpacing = new Spacing(p.EdgeSpacing.Left, p.EdgeSpacing.Bottom, p.EdgeSpacing.Right, p.EdgeSpacing.Top); + plate.EdgeSpacing = new Spacing( + p.EdgeSpacing.Left, + p.EdgeSpacing.Bottom, + p.EdgeSpacing.Right, + p.EdgeSpacing.Top + ); plate.GrainAngle = p.GrainAngle; foreach (var partDto in p.Parts) @@ -318,13 +362,14 @@ namespace OpenNest.IO { foreach (var cutoffDto in p.CutOffs) { - var axis = cutoffDto.Axis?.ToLowerInvariant() == "horizontal" - ? CutOffAxis.Horizontal - : CutOffAxis.Vertical; + var axis = + cutoffDto.Axis?.ToLowerInvariant() == "horizontal" + ? CutOffAxis.Horizontal + : CutOffAxis.Vertical; var cutoff = new CutOff(new Vector(cutoffDto.X, cutoffDto.Y), axis) { StartLimit = cutoffDto.StartLimit, - EndLimit = cutoffDto.EndLimit + EndLimit = cutoffDto.EndLimit, }; plate.CutOffs.Add(cutoff); } diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index c12b44c..15b06fa 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -1,6 +1,3 @@ -using OpenNest.Bending; -using OpenNest.CNC; -using OpenNest.Engine.BestFit; using System; using System.Collections.Generic; using System.IO; @@ -8,6 +5,9 @@ using System.IO.Compression; using System.Linq; using System.Text; using System.Text.Json; +using OpenNest.Bending; +using OpenNest.CNC; +using OpenNest.Engine.BestFit; using static OpenNest.IO.NestFormat; namespace OpenNest.IO @@ -85,17 +85,20 @@ namespace OpenNest.IO { Name = nest.Material.Name ?? "", Grade = nest.Material.Grade ?? "", - Density = nest.Material.Density + Density = nest.Material.Density, }, PlateDefaults = BuildPlateDefaultsDto(), Drawings = BuildDrawingDtos(), Plates = BuildPlateDtos(), - PlateOptions = nest.PlateOptions?.Select(o => new PlateOptionDto - { - Width = o.Width, - Length = o.Length, - Cost = o.Cost, - }).ToList() ?? new(), + PlateOptions = + nest.PlateOptions?.Select(o => new PlateOptionDto + { + Width = o.Width, + Length = o.Length, + Cost = o.Cost, + }) + .ToList() + ?? new(), SalvageRate = nest.SalvageRate, }; } @@ -113,15 +116,15 @@ namespace OpenNest.IO { Name = nest.Material.Name ?? "", Grade = nest.Material.Grade ?? "", - Density = nest.Material.Density + Density = nest.Material.Density, }, EdgeSpacing = new SpacingDto { Left = pd.EdgeSpacing.Left, Top = pd.EdgeSpacing.Top, Right = pd.EdgeSpacing.Right, - Bottom = pd.EdgeSpacing.Bottom - } + Bottom = pd.EdgeSpacing.Bottom, + }, }; } @@ -131,44 +134,55 @@ namespace OpenNest.IO foreach (var kvp in drawingDict.OrderBy(k => k.Key)) { var d = kvp.Value; - list.Add(new DrawingDto - { - Id = kvp.Key, - Name = d.Name ?? "", - Customer = d.Customer ?? "", - Color = new ColorDto { A = d.Color.A, R = d.Color.R, G = d.Color.G, B = d.Color.B }, - Quantity = new QuantityDto { Required = d.Quantity.Required }, - Priority = d.Priority, - Constraints = new ConstraintsDto + list.Add( + new DrawingDto { - StepAngle = d.Constraints.StepAngle, - StartAngle = d.Constraints.StartAngle, - EndAngle = d.Constraints.EndAngle, - Allow180Equivalent = d.Constraints.Allow180Equivalent - }, - Material = new MaterialDto - { - Name = d.Material.Name ?? "", - Grade = d.Material.Grade ?? "", - Density = d.Material.Density - }, - Source = new SourceDto - { - Path = d.Source.Path ?? "", - Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y } - }, - Bends = d.Bends?.Select(b => new BendDto - { - StartX = b.StartPoint.X, - StartY = b.StartPoint.Y, - EndX = b.EndPoint.X, - EndY = b.EndPoint.Y, - Direction = b.Direction.ToString(), - Angle = b.Angle, - Radius = b.Radius, - NoteText = b.NoteText ?? "" - }).ToList() ?? new List() - }); + Id = kvp.Key, + Name = d.Name ?? "", + Customer = d.Customer ?? "", + Color = new ColorDto + { + A = d.Color.A, + R = d.Color.R, + G = d.Color.G, + B = d.Color.B, + }, + Quantity = new QuantityDto { Required = d.Quantity.Required }, + Priority = d.Priority, + Constraints = new ConstraintsDto + { + StepAngle = d.Constraints.StepAngle, + StartAngle = d.Constraints.StartAngle, + EndAngle = d.Constraints.EndAngle, + Allow180Equivalent = d.Constraints.Allow180Equivalent, + }, + Material = new MaterialDto + { + Name = d.Material.Name ?? "", + Grade = d.Material.Grade ?? "", + Density = d.Material.Density, + }, + Source = new SourceDto + { + Path = d.Source.Path ?? "", + Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y }, + }, + Bends = + d.Bends?.Select(b => new BendDto + { + StartX = b.StartPoint.X, + StartY = b.StartPoint.Y, + EndX = b.EndPoint.X, + EndY = b.EndPoint.Y, + Direction = b.Direction.ToString(), + Angle = b.Angle, + Radius = b.Radius, + NoteText = b.NoteText ?? "", + }) + .ToList() + ?? new List(), + } + ); } return list; } @@ -181,56 +195,67 @@ namespace OpenNest.IO { var plate = nest.Plates[i]; - if (plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0 && plate.CutOffs.Count == 0) + if ( + plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0 + && plate.CutOffs.Count == 0 + ) continue; id++; var parts = new List(); foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff)) { - var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault(); - parts.Add(new PartDto - { - DrawingId = match.Key, - X = part.Location.X, - Y = part.Location.Y, - Rotation = part.Rotation, - HasManualLeadIns = part.HasManualLeadIns, - LeadInsLocked = part.LeadInsLocked - }); + var match = drawingDict + .Where(dwg => dwg.Value == part.BaseDrawing) + .FirstOrDefault(); + parts.Add( + new PartDto + { + DrawingId = match.Key, + X = part.Location.X, + Y = part.Location.Y, + Rotation = part.Rotation, + HasManualLeadIns = part.HasManualLeadIns, + LeadInsLocked = part.LeadInsLocked, + } + ); } var cutoffs = new List(); foreach (var cutoff in plate.CutOffs) { - cutoffs.Add(new CutOffDto - { - X = cutoff.Position.X, - Y = cutoff.Position.Y, - Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal", - StartLimit = cutoff.StartLimit, - EndLimit = cutoff.EndLimit - }); + cutoffs.Add( + new CutOffDto + { + X = cutoff.Position.X, + Y = cutoff.Position.Y, + Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal", + StartLimit = cutoff.StartLimit, + EndLimit = cutoff.EndLimit, + } + ); } - list.Add(new PlateDto - { - Id = id, - Size = new SizeDto { Width = plate.Size.Width, Length = plate.Size.Length }, - Quadrant = plate.Quadrant, - Quantity = plate.Quantity, - PartSpacing = plate.PartSpacing, - EdgeSpacing = new SpacingDto + list.Add( + new PlateDto { - Left = plate.EdgeSpacing.Left, - Top = plate.EdgeSpacing.Top, - Right = plate.EdgeSpacing.Right, - Bottom = plate.EdgeSpacing.Bottom - }, - Parts = parts, - CutOffs = cutoffs, - GrainAngle = plate.GrainAngle - }); + Id = id, + Size = new SizeDto { Width = plate.Size.Width, Length = plate.Size.Length }, + Quadrant = plate.Quadrant, + Quantity = plate.Quantity, + PartSpacing = plate.PartSpacing, + EdgeSpacing = new SpacingDto + { + Left = plate.EdgeSpacing.Left, + Top = plate.EdgeSpacing.Top, + Right = plate.EdgeSpacing.Right, + Bottom = plate.EdgeSpacing.Bottom, + }, + Parts = parts, + CutOffs = cutoffs, + GrainAngle = plate.GrainAngle, + } + ); } return list; } @@ -247,11 +272,13 @@ namespace OpenNest.IO foreach (var kvp in allBestFits) { - if (!plateSizes.Contains((kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing))) + if ( + !plateSizes.Contains((kvp.Key.PlateWidth, kvp.Key.PlateHeight, kvp.Key.Spacing)) + ) continue; - var results = kvp.Value - .Where(r => r.Keep) + var results = kvp + .Value.Where(r => r.Keep) .Select(r => new BestFitResultDto { Part1Rotation = r.Candidate.Part1Rotation, @@ -268,16 +295,19 @@ namespace OpenNest.IO Keep = r.Keep, Reason = r.Reason ?? "", TrueArea = r.TrueArea, - HullAngles = r.HullAngles ?? new List() - }).ToList(); + HullAngles = r.HullAngles ?? new List(), + }) + .ToList(); - sets.Add(new BestFitSetDto - { - PlateWidth = kvp.Key.PlateWidth, - PlateHeight = kvp.Key.PlateHeight, - Spacing = kvp.Key.Spacing, - Results = results - }); + sets.Add( + new BestFitSetDto + { + PlateWidth = kvp.Key.PlateWidth, + PlateHeight = kvp.Key.PlateHeight, + Spacing = kvp.Key.Spacing, + Results = results, + } + ); } return sets; @@ -319,7 +349,11 @@ namespace OpenNest.IO } } - private void WriteSubPrograms(ZipArchive zipArchive, int drawingId, Dictionary subPrograms) + private void WriteSubPrograms( + ZipArchive zipArchive, + int drawingId, + Dictionary subPrograms + ) { var entry = zipArchive.CreateEntry($"programs/program-{drawingId}-subs"); using var entryStream = entry.Open(); @@ -345,7 +379,10 @@ namespace OpenNest.IO if (drawing.SourceEntities == null || drawing.SourceEntities.Count == 0) continue; - var dto = EntitySerializer.ToDto(drawing.SourceEntities, drawing.SuppressedEntityIds); + var dto = EntitySerializer.ToDto( + drawing.SourceEntities, + drawing.SuppressedEntityIds + ); var json = JsonSerializer.Serialize(dto, JsonOptions); var entry = zipArchive.CreateEntry($"entities/entities-{kvp.Key}"); @@ -365,8 +402,10 @@ namespace OpenNest.IO foreach (var v in program.Variables.Values) { var line = $"{v.Name} = {v.Expression}"; - if (v.Inline) line += " inline"; - if (v.Global) line += " global"; + if (v.Inline) + line += " inline"; + if (v.Global) + line += " global"; writer.WriteLine(line); } @@ -381,7 +420,11 @@ namespace OpenNest.IO stream.Position = 0; } - private string FormatCoord(double value, string axis, Dictionary variableRefs) + private string FormatCoord( + double value, + string axis, + Dictionary variableRefs + ) { if (variableRefs != null && variableRefs.TryGetValue(axis, out var varName)) return $"${varName}"; @@ -393,89 +436,100 @@ namespace OpenNest.IO switch (code.Type) { case CodeType.ArcMove: - { - var sb = new StringBuilder(); - var arcMove = (ArcMove)code; - var refs = arcMove.VariableRefs; + { + var sb = new StringBuilder(); + var arcMove = (ArcMove)code; + var refs = arcMove.VariableRefs; - var x = FormatCoord(arcMove.EndPoint.X, "X", refs); - var y = FormatCoord(arcMove.EndPoint.Y, "Y", refs); - var i = FormatCoord(arcMove.CenterPoint.X, "I", refs); - var j = FormatCoord(arcMove.CenterPoint.Y, "J", refs); + var x = FormatCoord(arcMove.EndPoint.X, "X", refs); + var y = FormatCoord(arcMove.EndPoint.Y, "Y", refs); + var i = FormatCoord(arcMove.CenterPoint.X, "I", refs); + var j = FormatCoord(arcMove.CenterPoint.Y, "J", refs); - sb.Append(arcMove.Rotation == RotationType.CW + sb.Append( + arcMove.Rotation == RotationType.CW ? $"G02X{x}Y{y}I{i}J{j}" - : $"G03X{x}Y{y}I{i}J{j}"); + : $"G03X{x}Y{y}I{i}J{j}" + ); - if (arcMove.Layer != LayerType.Cut) - sb.Append(GetLayerString(arcMove.Layer)); + if (arcMove.Layer != LayerType.Cut) + sb.Append(GetLayerString(arcMove.Layer)); - if (arcMove.Suppressed) - sb.Append(":SUPPRESSED"); + if (arcMove.Suppressed) + sb.Append(":SUPPRESSED"); - return sb.ToString(); - } + return sb.ToString(); + } case CodeType.Comment: - { - var comment = (Comment)code; - return ":" + comment.Value; - } + { + var comment = (Comment)code; + return ":" + comment.Value; + } case CodeType.LinearMove: - { - var sb = new StringBuilder(); - var linearMove = (LinearMove)code; - var refs = linearMove.VariableRefs; + { + var sb = new StringBuilder(); + var linearMove = (LinearMove)code; + var refs = linearMove.VariableRefs; - sb.Append($"G01X{FormatCoord(linearMove.EndPoint.X, "X", refs)}Y{FormatCoord(linearMove.EndPoint.Y, "Y", refs)}"); + sb.Append( + $"G01X{FormatCoord(linearMove.EndPoint.X, "X", refs)}Y{FormatCoord(linearMove.EndPoint.Y, "Y", refs)}" + ); - if (linearMove.Layer != LayerType.Cut) - sb.Append(GetLayerString(linearMove.Layer)); + if (linearMove.Layer != LayerType.Cut) + sb.Append(GetLayerString(linearMove.Layer)); - if (linearMove.Suppressed) - sb.Append(":SUPPRESSED"); + if (linearMove.Suppressed) + sb.Append(":SUPPRESSED"); - return sb.ToString(); - } + return sb.ToString(); + } case CodeType.RapidMove: - { - var rapidMove = (RapidMove)code; - var refs = rapidMove.VariableRefs; + { + var rapidMove = (RapidMove)code; + var refs = rapidMove.VariableRefs; - return $"G00X{FormatCoord(rapidMove.EndPoint.X, "X", refs)}Y{FormatCoord(rapidMove.EndPoint.Y, "Y", refs)}"; - } + return $"G00X{FormatCoord(rapidMove.EndPoint.X, "X", refs)}Y{FormatCoord(rapidMove.EndPoint.Y, "Y", refs)}"; + } case CodeType.SetFeedrate: - { - var setFeedrate = (Feedrate)code; - if (setFeedrate.VariableRef != null) - return $"F${setFeedrate.VariableRef}"; - return "F" + setFeedrate.Value; - } + { + var setFeedrate = (Feedrate)code; + if (setFeedrate.VariableRef != null) + return $"F${setFeedrate.VariableRef}"; + return "F" + setFeedrate.Value; + } case CodeType.SetKerf: + { + var setKerf = (Kerf)code; + + switch (setKerf.Value) { - var setKerf = (Kerf)code; - - switch (setKerf.Value) - { - case KerfType.None: return "G40"; - case KerfType.Left: return "G41"; - case KerfType.Right: return "G42"; - } - - break; + case KerfType.None: + return "G40"; + case KerfType.Left: + return "G41"; + case KerfType.Right: + return "G42"; } + break; + } + case CodeType.SubProgramCall: - { - var subProgramCall = (SubProgramCall)code; - var x = System.Math.Round(subProgramCall.Offset.X, OutputPrecision).ToString(CoordinateFormat); - var y = System.Math.Round(subProgramCall.Offset.Y, OutputPrecision).ToString(CoordinateFormat); - return $"G65P{subProgramCall.Id}X{x}Y{y}"; - } + { + var subProgramCall = (SubProgramCall)code; + var x = System + .Math.Round(subProgramCall.Offset.X, OutputPrecision) + .ToString(CoordinateFormat); + var y = System + .Math.Round(subProgramCall.Offset.Y, OutputPrecision) + .ToString(CoordinateFormat); + return $"G65P{subProgramCall.Id}X{x}Y{y}"; + } } return string.Empty; diff --git a/OpenNest.IO/ProgramReader.cs b/OpenNest.IO/ProgramReader.cs index ae2a944..c74ba43 100644 --- a/OpenNest.IO/ProgramReader.cs +++ b/OpenNest.IO/ProgramReader.cs @@ -1,12 +1,12 @@ -using OpenNest.CNC; -using OpenNest.Geometry; -using OpenNest.Math; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.IO { @@ -31,15 +31,25 @@ namespace OpenNest.IO { // First pass: read all lines, collect variable definitions var allLines = new List(); - var variableDefs = new Dictionary( - StringComparer.OrdinalIgnoreCase); + var variableDefs = new Dictionary< + string, + (string expression, bool inline, bool global) + >(StringComparer.OrdinalIgnoreCase); var codeLines = new List(); string line; while ((line = reader.ReadLine()) != null) { allLines.Add(line); - if (TryParseVariableDefinition(line, out var name, out var expression, out var isInline, out var isGlobal)) + if ( + TryParseVariableDefinition( + line, + out var name, + out var expression, + out var isInline, + out var isGlobal + ) + ) variableDefs[name] = (expression, isInline, isGlobal); else codeLines.Add(line); @@ -54,7 +64,13 @@ namespace OpenNest.IO var name = kvp.Key; var (expression, isInline, isGlobal) = kvp.Value; var value = resolvedVariables[name]; - program.Variables[name] = new VariableDefinition(name, expression, value, isInline, isGlobal); + program.Variables[name] = new VariableDefinition( + name, + expression, + value, + isInline, + isGlobal + ); } // Second pass: parse G-code lines with variable substitution @@ -78,7 +94,10 @@ namespace OpenNest.IO { // Read the maximal variable name (letters, digits, underscores) var start = i + 1; - while (start < line.Length && (char.IsLetterOrDigit(line[start]) || line[start] == '_')) + while ( + start < line.Length + && (char.IsLetterOrDigit(line[start]) || line[start] == '_') + ) start++; var maxName = line.Substring(i + 1, start - i - 1); @@ -89,8 +108,9 @@ namespace OpenNest.IO while (nameLen > 0) { var candidate = maxName.Substring(0, nameLen); - lookupKey = resolvedVariables.Keys - .FirstOrDefault(k => string.Equals(k, candidate, StringComparison.OrdinalIgnoreCase)); + lookupKey = resolvedVariables.Keys.FirstOrDefault(k => + string.Equals(k, candidate, StringComparison.OrdinalIgnoreCase) + ); if (lookupKey != null) break; nameLen--; @@ -98,7 +118,8 @@ namespace OpenNest.IO if (lookupKey != null) { - code.Value = resolvedVariables[lookupKey].ToString(CultureInfo.InvariantCulture); + code.Value = resolvedVariables[lookupKey] + .ToString(CultureInfo.InvariantCulture); code.VariableRef = lookupKey; i += nameLen; // advance past the matched variable name } @@ -211,7 +232,8 @@ namespace OpenNest.IO double y = 0; var layer = LayerType.Cut; var suppressed = false; - string xRef = null, yRef = null; + string xRef = null, + yRef = null; while (section == CodeSection.Line) { @@ -235,36 +257,36 @@ namespace OpenNest.IO break; case ':': + { + var tags = code.Value.Trim().ToUpper().Split(':'); + + foreach (var tag in tags) { - var tags = code.Value.Trim().ToUpper().Split(':'); - - foreach (var tag in tags) + switch (tag) { - switch (tag) - { - case "DISPLAY": - layer = LayerType.Display; - break; + case "DISPLAY": + layer = LayerType.Display; + break; - case "LEADIN": - layer = LayerType.Leadin; - break; + case "LEADIN": + layer = LayerType.Leadin; + break; - case "LEADOUT": - layer = LayerType.Leadout; - break; + case "LEADOUT": + layer = LayerType.Leadout; + break; - case "SCRIBE": - layer = LayerType.Scribe; - break; + case "SCRIBE": + layer = LayerType.Scribe; + break; - case "SUPPRESSED": - suppressed = true; - break; - } + case "SUPPRESSED": + suppressed = true; + break; } - break; } + break; + } default: section = CodeSection.Unknown; @@ -277,7 +299,14 @@ namespace OpenNest.IO if (isRapid) program.Codes.Add(new RapidMove(x, y) { VariableRefs = refs }); else - program.Codes.Add(new LinearMove(x, y) { Layer = layer, Suppressed = suppressed, VariableRefs = refs }); + program.Codes.Add( + new LinearMove(x, y) + { + Layer = layer, + Suppressed = suppressed, + VariableRefs = refs, + } + ); } private void ReadArc(RotationType rotation) @@ -288,7 +317,10 @@ namespace OpenNest.IO double j = 0; var layer = LayerType.Cut; var suppressed = false; - string xRef = null, yRef = null, iRef = null, jRef = null; + string xRef = null, + yRef = null, + iRef = null, + jRef = null; while (section == CodeSection.Arc) { @@ -323,51 +355,58 @@ namespace OpenNest.IO break; case ':': + { + var tags = code.Value.Trim().ToUpper().Split(':'); + + foreach (var tag in tags) { - var tags = code.Value.Trim().ToUpper().Split(':'); - - foreach (var tag in tags) + switch (tag) { - switch (tag) - { - case "DISPLAY": - layer = LayerType.Display; - break; + case "DISPLAY": + layer = LayerType.Display; + break; - case "LEADIN": - layer = LayerType.Leadin; - break; + case "LEADIN": + layer = LayerType.Leadin; + break; - case "LEADOUT": - layer = LayerType.Leadout; - break; + case "LEADOUT": + layer = LayerType.Leadout; + break; - case "SCRIBE": - layer = LayerType.Scribe; - break; + case "SCRIBE": + layer = LayerType.Scribe; + break; - case "SUPPRESSED": - suppressed = true; - break; - } + case "SUPPRESSED": + suppressed = true; + break; } - break; } + break; + } default: section = CodeSection.Unknown; break; } } - program.Codes.Add(new ArcMove() - { - EndPoint = new Vector(x, y), - CenterPoint = new Vector(i, j), - Rotation = rotation, - Layer = layer, - Suppressed = suppressed, - VariableRefs = BuildVariableRefs(("X", xRef), ("Y", yRef), ("I", iRef), ("J", jRef)) - }); + program.Codes.Add( + new ArcMove() + { + EndPoint = new Vector(x, y), + CenterPoint = new Vector(i, j), + Rotation = rotation, + Layer = layer, + Suppressed = suppressed, + VariableRefs = BuildVariableRefs( + ("X", xRef), + ("Y", yRef), + ("I", iRef), + ("J", jRef) + ), + } + ); } private void ReadSubProgram() @@ -411,12 +450,14 @@ namespace OpenNest.IO } } - program.Codes.Add(new SubProgramCall - { - Id = p, - Rotation = r, - Offset = new Geometry.Vector(x, y) - }); + program.Codes.Add( + new SubProgramCall + { + Id = p, + Rotation = r, + Offset = new Geometry.Vector(x, y), + } + ); } private Code GetNextCode() @@ -446,8 +487,13 @@ namespace OpenNest.IO return block[codeIndex]; } - private static bool TryParseVariableDefinition(string line, out string name, out string expression, - out bool isInline, out bool isGlobal) + private static bool TryParseVariableDefinition( + string line, + out string name, + out string expression, + out bool isInline, + out bool isGlobal + ) { name = null; expression = null; @@ -467,7 +513,22 @@ namespace OpenNest.IO if (trimmed.Length >= 2 && char.IsDigit(trimmed[1])) { var upper = char.ToUpper(firstChar); - if (upper is 'G' or 'M' or 'N' or 'F' or 'X' or 'Y' or 'I' or 'J' or 'T' or 'S' or 'O' or 'P' or 'R') + if ( + upper + is 'G' + or 'M' + or 'N' + or 'F' + or 'X' + or 'Y' + or 'I' + or 'J' + or 'T' + or 'S' + or 'O' + or 'P' + or 'R' + ) return false; } @@ -515,8 +576,10 @@ namespace OpenNest.IO for (var i = flagStart; i < words.Length; i++) { var word = words[i].ToLowerInvariant(); - if (word == "inline") isInline = true; - else if (word == "global") isGlobal = true; + if (word == "inline") + isInline = true; + else if (word == "global") + isGlobal = true; } name = rawName; @@ -524,13 +587,16 @@ namespace OpenNest.IO } private static Dictionary ResolveVariables( - Dictionary variableDefs) + Dictionary variableDefs + ) { if (variableDefs.Count == 0) return new Dictionary(StringComparer.OrdinalIgnoreCase); // Build dependency graph - var dependencies = new Dictionary>(StringComparer.OrdinalIgnoreCase); + var dependencies = new Dictionary>( + StringComparer.OrdinalIgnoreCase + ); foreach (var kvp in variableDefs) { var deps = new List(); @@ -540,12 +606,16 @@ namespace OpenNest.IO if (expr[i] == '$') { var start = i + 1; - while (start < expr.Length && (char.IsLetterOrDigit(expr[start]) || expr[start] == '_')) + while ( + start < expr.Length + && (char.IsLetterOrDigit(expr[start]) || expr[start] == '_') + ) start++; var refName = expr.Substring(i + 1, start - i - 1); // Find the canonical name (case-insensitive match) - var canonical = variableDefs.Keys - .FirstOrDefault(k => string.Equals(k, refName, StringComparison.OrdinalIgnoreCase)); + var canonical = variableDefs.Keys.FirstOrDefault(k => + string.Equals(k, refName, StringComparison.OrdinalIgnoreCase) + ); if (canonical != null) deps.Add(canonical); i = start - 1; @@ -600,12 +670,16 @@ namespace OpenNest.IO } if (order.Count != variableDefs.Count) - throw new InvalidOperationException("Circular dependency detected among variables."); + throw new InvalidOperationException( + "Circular dependency detected among variables." + ); return resolved; } - private static Dictionary BuildVariableRefs(params (string axis, string varRef)[] refs) + private static Dictionary BuildVariableRefs( + params (string axis, string varRef)[] refs + ) { Dictionary result = null; foreach (var (axis, varRef) in refs) @@ -671,7 +745,7 @@ namespace OpenNest.IO Unknown, Arc, Line, - SubProgram + SubProgram, } } } diff --git a/OpenNest.IO/SplitDxfWriter.cs b/OpenNest.IO/SplitDxfWriter.cs index 6b24ede..cd9a5ed 100644 --- a/OpenNest.IO/SplitDxfWriter.cs +++ b/OpenNest.IO/SplitDxfWriter.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.IO; using ACadSharp; using ACadSharp.Entities; using ACadSharp.IO; @@ -6,9 +8,6 @@ using CSMath; using OpenNest.Bending; using OpenNest.Converters; using OpenNest.Geometry; -using System.Collections.Generic; -using System.IO; - // Disambiguate Entity — both ACadSharp.Entities and OpenNest.Geometry define it using GeoEntity = OpenNest.Geometry.Entity; @@ -50,13 +49,21 @@ namespace OpenNest.IO writer.Write(); } - private static void WriteProgramEntities(CadDocument doc, CNC.Program program, ACadSharp.Tables.Layer layer) + private static void WriteProgramEntities( + CadDocument doc, + CNC.Program program, + ACadSharp.Tables.Layer layer + ) { var geometry = ConvertProgram.ToGeometry(program); WriteGeometryEntities(doc, geometry, layer); } - private static void WriteGeometryEntities(CadDocument doc, List geometry, ACadSharp.Tables.Layer layer) + private static void WriteGeometryEntities( + CadDocument doc, + List geometry, + ACadSharp.Tables.Layer layer + ) { foreach (var entity in geometry) { @@ -67,12 +74,14 @@ namespace OpenNest.IO switch (entity) { case OpenNest.Geometry.Line line: - doc.Entities.Add(new ACadSharp.Entities.Line - { - StartPoint = new XYZ(line.StartPoint.X, line.StartPoint.Y, 0), - EndPoint = new XYZ(line.EndPoint.X, line.EndPoint.Y, 0), - Layer = layer - }); + doc.Entities.Add( + new ACadSharp.Entities.Line + { + StartPoint = new XYZ(line.StartPoint.X, line.StartPoint.Y, 0), + EndPoint = new XYZ(line.EndPoint.X, line.EndPoint.Y, 0), + Layer = layer, + } + ); break; case OpenNest.Geometry.Arc arc: @@ -81,23 +90,27 @@ namespace OpenNest.IO if (arc.IsReversed) OpenNest.Math.Generic.Swap(ref startAngle, ref endAngle); - doc.Entities.Add(new ACadSharp.Entities.Arc - { - Center = new XYZ(arc.Center.X, arc.Center.Y, 0), - Radius = arc.Radius, - StartAngle = startAngle, - EndAngle = endAngle, - Layer = layer - }); + doc.Entities.Add( + new ACadSharp.Entities.Arc + { + Center = new XYZ(arc.Center.X, arc.Center.Y, 0), + Radius = arc.Radius, + StartAngle = startAngle, + EndAngle = endAngle, + Layer = layer, + } + ); break; case OpenNest.Geometry.Circle circle: - doc.Entities.Add(new ACadSharp.Entities.Circle - { - Center = new XYZ(circle.Center.X, circle.Center.Y, 0), - Radius = circle.Radius, - Layer = layer - }); + doc.Entities.Add( + new ACadSharp.Entities.Circle + { + Center = new XYZ(circle.Center.X, circle.Center.Y, 0), + Radius = circle.Radius, + Layer = layer, + } + ); break; case OpenNest.Geometry.Shape shape: @@ -107,14 +120,19 @@ namespace OpenNest.IO } } - private static void WriteBendLine(CadDocument doc, Bend bend, ACadSharp.Tables.Layer layer, LineType lineType) + private static void WriteBendLine( + CadDocument doc, + Bend bend, + ACadSharp.Tables.Layer layer, + LineType lineType + ) { var line = new ACadSharp.Entities.Line { StartPoint = new XYZ(bend.StartPoint.X, bend.StartPoint.Y, 0), EndPoint = new XYZ(bend.EndPoint.X, bend.EndPoint.Y, 0), Layer = layer, - LineType = lineType + LineType = lineType, }; doc.Entities.Add(line); @@ -128,7 +146,7 @@ namespace OpenNest.IO InsertPoint = new XYZ(midX, midY + 0.5, 0), Value = bend.NoteText, Height = 0.1, - Layer = layer + Layer = layer, }; doc.Entities.Add(mtext); } @@ -145,12 +163,14 @@ namespace OpenNest.IO if (length < EtchLength * 3.0) { - doc.Entities.Add(new ACadSharp.Entities.Line - { - StartPoint = new XYZ(start.X, start.Y, 0), - EndPoint = new XYZ(end.X, end.Y, 0), - Layer = layer - }); + doc.Entities.Add( + new ACadSharp.Entities.Line + { + StartPoint = new XYZ(start.X, start.Y, 0), + EndPoint = new XYZ(end.X, end.Y, 0), + Layer = layer, + } + ); } else { @@ -158,19 +178,23 @@ namespace OpenNest.IO var dx = System.Math.Cos(angle) * EtchLength; var dy = System.Math.Sin(angle) * EtchLength; - doc.Entities.Add(new ACadSharp.Entities.Line - { - StartPoint = new XYZ(start.X, start.Y, 0), - EndPoint = new XYZ(start.X + dx, start.Y + dy, 0), - Layer = layer - }); + doc.Entities.Add( + new ACadSharp.Entities.Line + { + StartPoint = new XYZ(start.X, start.Y, 0), + EndPoint = new XYZ(start.X + dx, start.Y + dy, 0), + Layer = layer, + } + ); - doc.Entities.Add(new ACadSharp.Entities.Line - { - StartPoint = new XYZ(end.X, end.Y, 0), - EndPoint = new XYZ(end.X - dx, end.Y - dy, 0), - Layer = layer - }); + doc.Entities.Add( + new ACadSharp.Entities.Line + { + StartPoint = new XYZ(end.X, end.Y, 0), + EndPoint = new XYZ(end.X - dx, end.Y - dy, 0), + Layer = layer, + } + ); } } } diff --git a/OpenNest.Mcp/Program.cs b/OpenNest.Mcp/Program.cs index 3ff768a..35e27d4 100644 --- a/OpenNest.Mcp/Program.cs +++ b/OpenNest.Mcp/Program.cs @@ -5,8 +5,8 @@ using OpenNest.Mcp; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddSingleton(); -builder.Services - .AddMcpServer() +builder + .Services.AddMcpServer() .WithStdioServerTransport() .WithToolsFromAssembly(typeof(Program).Assembly); diff --git a/OpenNest.Mcp/Tools/InputTools.cs b/OpenNest.Mcp/Tools/InputTools.cs index 166e6f0..096705b 100644 --- a/OpenNest.Mcp/Tools/InputTools.cs +++ b/OpenNest.Mcp/Tools/InputTools.cs @@ -1,9 +1,9 @@ -using ModelContextProtocol.Server; -using OpenNest.IO; -using OpenNest.Shapes; using System.ComponentModel; using System.IO; using System.Text; +using ModelContextProtocol.Server; +using OpenNest.IO; +using OpenNest.Shapes; using CncProgram = OpenNest.CNC.Program; namespace OpenNest.Mcp.Tools @@ -19,7 +19,9 @@ namespace OpenNest.Mcp.Tools } [McpServerTool(Name = "load_nest")] - [Description("Load a .nest file into the session. Returns a summary of plates, parts, and drawings.")] + [Description( + "Load a .nest file into the session. Returns a summary of plates, parts, and drawings." + )] public string LoadNest([Description("Absolute path to the .nest file")] string path) { if (!File.Exists(path)) @@ -38,10 +40,12 @@ namespace OpenNest.Mcp.Tools { var plate = nest.Plates[i]; var work = plate.WorkArea(); - sb.AppendLine($" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Length:F1}, " + - $"parts={plate.Parts.Count}, " + - $"utilization={plate.Utilization():P1}, " + - $"work area={work.Width:F1} x {work.Length:F1}"); + sb.AppendLine( + $" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Length:F1}, " + + $"parts={plate.Parts.Count}, " + + $"utilization={plate.Utilization():P1}, " + + $"work area={work.Width:F1} x {work.Length:F1}" + ); } sb.AppendLine($"Drawings: {nest.Drawings.Count}"); @@ -49,8 +53,10 @@ namespace OpenNest.Mcp.Tools foreach (var dwg in nest.Drawings) { var bbox = dwg.Program.BoundingBox(); - sb.AppendLine($" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Length:F2}, " + - $"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}"); + sb.AppendLine( + $" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Length:F2}, " + + $"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}" + ); } return sb.ToString(); @@ -60,7 +66,8 @@ namespace OpenNest.Mcp.Tools [Description("Save the current session (all drawings and plates) to a .nest file.")] public string SaveNest( [Description("Absolute path for the output .nest file")] string path, - [Description("Name for the nest (optional)")] string name = null) + [Description("Name for the nest (optional)")] string name = null + ) { var nest = new Nest(); nest.Name = name ?? Path.GetFileNameWithoutExtension(path); @@ -89,7 +96,9 @@ namespace OpenNest.Mcp.Tools [Description("Import a DXF file as a new drawing. Returns drawing name and bounding box.")] public string ImportDxf( [Description("Absolute path to the DXF file")] string path, - [Description("Name for the drawing (defaults to filename without extension)")] string name = null) + [Description("Name for the drawing (defaults to filename without extension)")] + string name = null + ) { if (!File.Exists(path)) return $"Error: file not found: {path}"; @@ -109,21 +118,29 @@ namespace OpenNest.Mcp.Tools } [McpServerTool(Name = "create_drawing")] - [Description("Create a drawing from a built-in shape or G-code string. Shape can be: rectangle, circle, l_shape, t_shape, gcode.")] + [Description( + "Create a drawing from a built-in shape or G-code string. Shape can be: rectangle, circle, l_shape, t_shape, gcode." + )] public string CreateDrawing( [Description("Name for the drawing")] string name, [Description("Shape type: rectangle, circle, l_shape, t_shape, gcode")] string shape, [Description("Width of the shape (not used for circle or gcode)")] double width = 10, [Description("Length of the shape (not used for circle or gcode)")] double length = 10, [Description("Radius for circle shape")] double radius = 5, - [Description("G-code string (only used when shape is 'gcode')")] string gcode = null) + [Description("G-code string (only used when shape is 'gcode')")] string gcode = null + ) { ShapeDefinition shapeDef; switch (shape.ToLower()) { case "rectangle": - shapeDef = new RectangleShape { Name = name, Width = width, Length = length }; + shapeDef = new RectangleShape + { + Name = name, + Width = width, + Length = length, + }; break; case "circle": @@ -131,11 +148,21 @@ namespace OpenNest.Mcp.Tools break; case "l_shape": - shapeDef = new LShape { Name = name, Width = width, Height = length }; + shapeDef = new LShape + { + Name = name, + Width = width, + Height = length, + }; break; case "t_shape": - shapeDef = new TShape { Name = name, Width = width, Height = length }; + shapeDef = new TShape + { + Name = name, + Width = width, + Height = length, + }; break; case "gcode": diff --git a/OpenNest.Mcp/Tools/InspectionTools.cs b/OpenNest.Mcp/Tools/InspectionTools.cs index 40b2c3b..8ece2e2 100644 --- a/OpenNest.Mcp/Tools/InspectionTools.cs +++ b/OpenNest.Mcp/Tools/InspectionTools.cs @@ -1,9 +1,9 @@ -using ModelContextProtocol.Server; -using OpenNest.Engine.Fill; -using OpenNest.Math; using System.ComponentModel; using System.Linq; using System.Text; +using ModelContextProtocol.Server; +using OpenNest.Engine.Fill; +using OpenNest.Math; namespace OpenNest.Mcp.Tools { @@ -18,9 +18,10 @@ namespace OpenNest.Mcp.Tools } [McpServerTool(Name = "get_plate_info")] - [Description("Get detailed information about a plate including dimensions, part count, utilization, remnants, and drawing breakdown.")] - public string GetPlateInfo( - [Description("Index of the plate")] int plateIndex) + [Description( + "Get detailed information about a plate including dimensions, part count, utilization, remnants, and drawing breakdown." + )] + public string GetPlateInfo([Description("Index of the plate")] int plateIndex) { var plate = _session.GetPlate(plateIndex); if (plate == null) @@ -36,7 +37,9 @@ namespace OpenNest.Mcp.Tools sb.AppendLine($" Thickness: {_session.Nest?.Thickness:F2}"); sb.AppendLine($" Material: {_session.Nest?.Material?.Name}"); sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}"); - sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}"); + sb.AppendLine( + $" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}" + ); sb.AppendLine($" Work area: {work.X:F1},{work.Y:F1} {work.Width:F1}x{work.Length:F1}"); sb.AppendLine($" Parts: {plate.Parts.Count}"); sb.AppendLine($" Utilization: {plate.Utilization():P1}"); @@ -56,17 +59,22 @@ namespace OpenNest.Mcp.Tools for (var i = 0; i < remnants.Count; i++) { var r = remnants[i]; - sb.AppendLine($" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Length:F1}, area={r.Area():F1}"); + sb.AppendLine( + $" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Length:F1}, area={r.Area():F1}" + ); } return sb.ToString(); } [McpServerTool(Name = "get_parts")] - [Description("List placed parts on a plate with index, drawing name, location, rotation, and bounding box.")] + [Description( + "List placed parts on a plate with index, drawing name, location, rotation, and bounding box." + )] public string GetParts( [Description("Index of the plate")] int plateIndex, - [Description("Maximum number of parts to list (default 50)")] int limit = 50) + [Description("Maximum number of parts to list (default 50)")] int limit = 50 + ) { var plate = _session.GetPlate(plateIndex); if (plate == null) @@ -86,10 +94,12 @@ namespace OpenNest.Mcp.Tools var bbox = part.BoundingBox; var rotDeg = Angle.ToDegrees(part.Rotation); - sb.AppendLine($" [{i}] {part.BaseDrawing.Name}: " + - $"loc=({part.Location.X:F2},{part.Location.Y:F2}), " + - $"rot={rotDeg:F1} deg, " + - $"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Length:F2})"); + sb.AppendLine( + $" [{i}] {part.BaseDrawing.Name}: " + + $"loc=({part.Location.X:F2},{part.Location.Y:F2}), " + + $"rot={rotDeg:F1} deg, " + + $"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Length:F2})" + ); } if (plate.Parts.Count > limit) @@ -100,8 +110,7 @@ namespace OpenNest.Mcp.Tools [McpServerTool(Name = "check_overlaps")] [Description("Check a plate for overlapping parts. Reports collision points if any.")] - public string CheckOverlaps( - [Description("Index of the plate")] int plateIndex) + public string CheckOverlaps([Description("Index of the plate")] int plateIndex) { var plate = _session.GetPlate(plateIndex); if (plate == null) diff --git a/OpenNest.Mcp/Tools/NestingTools.cs b/OpenNest.Mcp/Tools/NestingTools.cs index 639e5ae..a603bbd 100644 --- a/OpenNest.Mcp/Tools/NestingTools.cs +++ b/OpenNest.Mcp/Tools/NestingTools.cs @@ -1,11 +1,11 @@ -using ModelContextProtocol.Server; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading; +using ModelContextProtocol.Server; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Mcp.Tools { @@ -20,11 +20,14 @@ namespace OpenNest.Mcp.Tools } [McpServerTool(Name = "fill_plate")] - [Description("Fill an entire plate with a single drawing. Returns parts added and utilization.")] + [Description( + "Fill an entire plate with a single drawing. Returns parts added and utilization." + )] public string FillPlate( [Description("Index of the plate to fill")] int plateIndex, [Description("Name of the drawing to fill with")] string drawingName, - [Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0) + [Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0 + ) { var plate = _session.GetPlate(plateIndex); if (plate == null) @@ -43,7 +46,9 @@ namespace OpenNest.Mcp.Tools var added = countAfter - countBefore; var sb = new StringBuilder(); - sb.AppendLine($"Fill plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}"); + sb.AppendLine( + $"Fill plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}" + ); sb.AppendLine($" Parts added: {added}"); sb.AppendLine($" Total parts: {countAfter}"); sb.AppendLine($" Utilization: {plate.Utilization():P1}"); @@ -60,7 +65,8 @@ namespace OpenNest.Mcp.Tools [Description("Y origin of the area")] double y, [Description("Width of the area")] double width, [Description("Length of the area")] double length, - [Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0) + [Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0 + ) { var plate = _session.GetPlate(plateIndex); if (plate == null) @@ -80,7 +86,9 @@ namespace OpenNest.Mcp.Tools var added = countAfter - countBefore; var sb = new StringBuilder(); - sb.AppendLine($"Fill area ({x:F1},{y:F1} {width:F1}x{length:F1}) on plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}"); + sb.AppendLine( + $"Fill area ({x:F1},{y:F1} {width:F1}x{length:F1}) on plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}" + ); sb.AppendLine($" Parts added: {added}"); sb.AppendLine($" Total parts: {countAfter}"); sb.AppendLine($" Utilization: {plate.Utilization():P1}"); @@ -93,7 +101,8 @@ namespace OpenNest.Mcp.Tools public string FillRemnants( [Description("Index of the plate")] int plateIndex, [Description("Name of the drawing to fill with")] string drawingName, - [Description("Maximum quantity per remnant (0 = unlimited)")] int quantity = 0) + [Description("Maximum quantity per remnant (0 = unlimited)")] int quantity = 0 + ) { var plate = _session.GetPlate(plateIndex); if (plate == null) @@ -124,7 +133,9 @@ namespace OpenNest.Mcp.Tools var added = plate.Parts.Count - countBefore; totalAdded += added; - sb.AppendLine($" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Length:F1}) -> {added} parts {(success ? "" : "(no fit)")}"); + sb.AppendLine( + $" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Length:F1}) -> {added} parts {(success ? "" : "(no fit)")}" + ); } sb.AppendLine($"Total parts added: {totalAdded}"); @@ -134,11 +145,14 @@ namespace OpenNest.Mcp.Tools } [McpServerTool(Name = "pack_plate")] - [Description("Pack multiple drawings onto a plate using bin-packing. Specify drawings and quantities as comma-separated lists.")] + [Description( + "Pack multiple drawings onto a plate using bin-packing. Specify drawings and quantities as comma-separated lists." + )] public string PackPlate( [Description("Index of the plate")] int plateIndex, [Description("Comma-separated drawing names")] string drawingNames, - [Description("Comma-separated quantities for each drawing")] string quantities) + [Description("Comma-separated quantities for each drawing")] string quantities + ) { var plate = _session.GetPlate(plateIndex); if (plate == null) @@ -195,11 +209,14 @@ namespace OpenNest.Mcp.Tools } [McpServerTool(Name = "autonest_plate")] - [Description("Mixed-part autonesting. Fills the plate with multiple different drawings using iterative per-drawing fills with remainder-strip packing.")] + [Description( + "Mixed-part autonesting. Fills the plate with multiple different drawings using iterative per-drawing fills with remainder-strip packing." + )] public string AutoNestPlate( [Description("Index of the plate")] int plateIndex, [Description("Comma-separated drawing names")] string drawingNames, - [Description("Comma-separated quantities for each drawing")] string quantities) + [Description("Comma-separated quantities for each drawing")] string quantities + ) { var plate = _session.GetPlate(plateIndex); if (plate == null) @@ -241,7 +258,9 @@ namespace OpenNest.Mcp.Tools var totalPlaced = nestParts.Count; var sb = new StringBuilder(); - sb.AppendLine($"AutoNest plate {plateIndex} ({engine.Name} engine): {(totalPlaced > 0 ? "success" : "no parts placed")}"); + sb.AppendLine( + $"AutoNest plate {plateIndex} ({engine.Name} engine): {(totalPlaced > 0 ? "success" : "no parts placed")}" + ); sb.AppendLine($" Parts placed: {totalPlaced}"); sb.AppendLine($" Total parts: {plate.Parts.Count}"); sb.AppendLine($" Utilization: {plate.Utilization():P1}"); diff --git a/OpenNest.Mcp/Tools/SetupTools.cs b/OpenNest.Mcp/Tools/SetupTools.cs index bb1daf9..40a8f9f 100644 --- a/OpenNest.Mcp/Tools/SetupTools.cs +++ b/OpenNest.Mcp/Tools/SetupTools.cs @@ -1,7 +1,7 @@ -using ModelContextProtocol.Server; -using OpenNest.Geometry; using System.ComponentModel; using System.Text; +using ModelContextProtocol.Server; +using OpenNest.Geometry; namespace OpenNest.Mcp.Tools { @@ -16,14 +16,20 @@ namespace OpenNest.Mcp.Tools } [McpServerTool(Name = "create_plate")] - [Description("Create a new plate with the given dimensions and spacing. Returns plate index and work area.")] + [Description( + "Create a new plate with the given dimensions and spacing. Returns plate index and work area." + )] public string CreatePlate( [Description("Plate width")] double width, [Description("Plate length")] double length, [Description("Spacing between parts (default 0)")] double partSpacing = 0, [Description("Edge spacing on all sides (default 0)")] double edgeSpacing = 0, - [Description("Quadrant 1-4 (default 1). 1=TopRight, 2=TopLeft, 3=BottomLeft, 4=BottomRight")] int quadrant = 1, - [Description("Material name (optional)")] string material = null) + [Description( + "Quadrant 1-4 (default 1). 1=TopRight, 2=TopLeft, 3=BottomLeft, 4=BottomRight" + )] + int quadrant = 1, + [Description("Material name (optional)")] string material = null + ) { var plate = new Plate(width, length); plate.PartSpacing = partSpacing; @@ -44,7 +50,9 @@ namespace OpenNest.Mcp.Tools sb.AppendLine($"Created plate {index}: {plate.Size.Width:F1} x {plate.Size.Length:F1}"); sb.AppendLine($" Quadrant: {plate.Quadrant}"); sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}"); - sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}"); + sb.AppendLine( + $" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}" + ); sb.AppendLine($" Work area: {work.Width:F1} x {work.Length:F1}"); return sb.ToString(); @@ -52,8 +60,7 @@ namespace OpenNest.Mcp.Tools [McpServerTool(Name = "clear_plate")] [Description("Remove all parts from a plate. Returns how many parts were removed.")] - public string ClearPlate( - [Description("Index of the plate to clear")] int plateIndex) + public string ClearPlate([Description("Index of the plate to clear")] int plateIndex) { var plate = _session.GetPlate(plateIndex); diff --git a/OpenNest.Mcp/Tools/TestTools.cs b/OpenNest.Mcp/Tools/TestTools.cs index 05401e6..72e7337 100644 --- a/OpenNest.Mcp/Tools/TestTools.cs +++ b/OpenNest.Mcp/Tools/TestTools.cs @@ -1,8 +1,8 @@ -using ModelContextProtocol.Server; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Text; +using ModelContextProtocol.Server; namespace OpenNest.Mcp.Tools { @@ -12,15 +12,24 @@ namespace OpenNest.Mcp.Tools private const string SolutionRoot = @"C:\Users\AJ\Desktop\Projects\OpenNest"; private static readonly string HarnessProject = Path.Combine( - SolutionRoot, "OpenNest.Console", "OpenNest.Console.csproj"); + SolutionRoot, + "OpenNest.Console", + "OpenNest.Console.csproj" + ); [McpServerTool(Name = "test_engine")] - [Description("Build and run the nesting engine against a nest file. Returns fill results and a debug log file path for grepping. Use this to test engine changes without restarting the MCP server.")] + [Description( + "Build and run the nesting engine against a nest file. Returns fill results and a debug log file path for grepping. Use this to test engine changes without restarting the MCP server." + )] public string TestEngine( - [Description("Path to the nest .nest file")] string nestFile = @"C:\Users\AJ\Desktop\4980 A24 PT02 60x120 45pcs v2.nest", - [Description("Drawing name to fill with (default: first drawing)")] string drawingName = null, + [Description("Path to the nest .nest file")] + string nestFile = @"C:\Users\AJ\Desktop\4980 A24 PT02 60x120 45pcs v2.nest", + [Description("Drawing name to fill with (default: first drawing)")] + string drawingName = null, [Description("Plate index to fill (default: 0)")] int plateIndex = 0, - [Description("Output nest file path (default: -result.nest)")] string outputFile = null) + [Description("Output nest file path (default: -result.nest)")] + string outputFile = null + ) { if (!File.Exists(nestFile)) return $"Error: nest file not found: {nestFile}"; @@ -44,7 +53,7 @@ namespace OpenNest.Mcp.Tools RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, - WorkingDirectory = SolutionRoot + WorkingDirectory = SolutionRoot, }; var sb = new StringBuilder(); diff --git a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs index c2777a6..034b353 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs @@ -129,14 +129,28 @@ public sealed class CincinnatiFeatureWriter var sb = new StringBuilder(); // Kerf compensation on first cutting move (skip for etch) - if (!ctx.IsEtch && !kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide) + if ( + !ctx.IsEtch + && !kerfEmitted + && _config.KerfCompensation == KerfMode.ControllerSide + ) { sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 "); kerfEmitted = true; } - var xCoord = FormatCoordWithVars(linear.EndPoint.X + offset.X, "X", linear.VariableRefs, ctx); - var yCoord = FormatCoordWithVars(linear.EndPoint.Y + offset.Y, "Y", linear.VariableRefs, ctx); + var xCoord = FormatCoordWithVars( + linear.EndPoint.X + offset.X, + "X", + linear.VariableRefs, + ctx + ); + var yCoord = FormatCoordWithVars( + linear.EndPoint.Y + offset.Y, + "Y", + linear.VariableRefs, + ctx + ); sb.Append($"G1 X{xCoord} Y{yCoord}"); // Feedrate — etch always uses process feedrate @@ -155,7 +169,11 @@ public sealed class CincinnatiFeatureWriter var sb = new StringBuilder(); // Kerf compensation on first cutting move (skip for etch) - if (!ctx.IsEtch && !kerfEmitted && _config.KerfCompensation == KerfMode.ControllerSide) + if ( + !ctx.IsEtch + && !kerfEmitted + && _config.KerfCompensation == KerfMode.ControllerSide + ) { sb.Append(_config.DefaultKerfSide == KerfSide.Left ? "G41 " : "G42 "); kerfEmitted = true; @@ -163,8 +181,18 @@ public sealed class CincinnatiFeatureWriter // G2 = CW, G3 = CCW var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3"; - var xCoord = FormatCoordWithVars(arc.EndPoint.X + offset.X, "X", arc.VariableRefs, ctx); - var yCoord = FormatCoordWithVars(arc.EndPoint.Y + offset.Y, "Y", arc.VariableRefs, ctx); + var xCoord = FormatCoordWithVars( + arc.EndPoint.X + offset.X, + "X", + arc.VariableRefs, + ctx + ); + var yCoord = FormatCoordWithVars( + arc.EndPoint.Y + offset.Y, + "Y", + arc.VariableRefs, + ctx + ); sb.Append($"{gCode} X{xCoord} Y{yCoord}"); // Convert absolute center to incremental I/J @@ -175,8 +203,7 @@ public sealed class CincinnatiFeatureWriter // Feedrate — etch always uses process feedrate, cut uses layer/radius-based var radius = currentPos.DistanceTo(arc.CenterPoint); var isFullCircle = IsFullCircle(currentPos, arc.EndPoint); - var feedVar = ctx.IsEtch ? "#148" - : GetArcFeedrate(arc.Layer, radius, isFullCircle); + var feedVar = ctx.IsEtch ? "#148" : GetArcFeedrate(arc.Layer, radius, isFullCircle); if (feedVar != lastFeedVar) { sb.Append($" F{feedVar}"); @@ -211,14 +238,20 @@ public sealed class CincinnatiFeatureWriter /// the sheet width/length variables. /// Inline variables fall through to literal formatting. /// - private string FormatCoordWithVars(double value, string axis, - Dictionary variableRefs, FeatureContext ctx) + private string FormatCoordWithVars( + double value, + string axis, + Dictionary variableRefs, + FeatureContext ctx + ) { // User-defined variable references take priority - if (variableRefs != null + if ( + variableRefs != null && variableRefs.TryGetValue(axis, out var varName) && ctx.UserVariableMapping != null - && ctx.UserVariableMapping.TryGetValue((ctx.DrawingId, varName), out var varNum)) + && ctx.UserVariableMapping.TryGetValue((ctx.DrawingId, varName), out var varNum) + ) { return $"#{varNum}"; } @@ -268,7 +301,12 @@ public sealed class CincinnatiFeatureWriter return Vector.Zero; } - private void WriteRapidToPierce(TextWriter writer, FeatureContext ctx, Vector piercePoint, Vector offset) + private void WriteRapidToPierce( + TextWriter writer, + FeatureContext ctx, + Vector piercePoint, + Vector offset + ) { var sb = new StringBuilder(); @@ -311,15 +349,18 @@ public sealed class CincinnatiFeatureWriter { LayerType.Leadin => "#126", LayerType.Leadout => "#129", - _ => "#148" + _ => "#148", }; } private string GetArcFeedrate(LayerType layer, double radius, bool isFullCircle) { - if (layer == LayerType.Leadin) return "#127"; - if (layer == LayerType.Leadout) return "#129"; - if (isFullCircle) return "[#148*#128]"; + if (layer == LayerType.Leadin) + return "#127"; + if (layer == LayerType.Leadout) + return "#129"; + if (isFullCircle) + return "[#148*#128]"; return GetArcCutFeedrate(radius); } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs index 7045845..7b40c9f 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs @@ -19,8 +19,10 @@ public sealed class CincinnatiPartSubprogramWriter private readonly CoordinateFormatter _fmt; private readonly Dictionary _holeSubprograms; - public CincinnatiPartSubprogramWriter(CincinnatiPostConfig config, - Dictionary holeSubprograms = null) + public CincinnatiPartSubprogramWriter( + CincinnatiPostConfig config, + Dictionary holeSubprograms = null + ) { _config = config; _featureWriter = new CincinnatiFeatureWriter(config); @@ -32,8 +34,15 @@ public sealed class CincinnatiPartSubprogramWriter /// Writes a complete part sub-program for the given normalized program. /// The program coordinates must already be normalized to origin (0,0). /// - public void Write(TextWriter w, Program normalizedProgram, string drawingName, - int subNumber, string cutLibrary, string etchLibrary, double sheetDiagonal) + public void Write( + TextWriter w, + Program normalizedProgram, + string drawingName, + int subNumber, + string cutLibrary, + string etchLibrary, + double sheetDiagonal + ) { var allFeatures = FeatureUtils.SplitByRapids(normalizedProgram.Codes); if (allFeatures.Count == 0) @@ -58,9 +67,7 @@ public sealed class CincinnatiPartSubprogramWriter continue; } - var featureNumber = i == 0 - ? _config.FeatureLineNumberStart - : 1000 + i + 1; + var featureNumber = i == 0 ? _config.FeatureLineNumberStart : 1000 + i + 1; var cutDistance = FeatureUtils.ComputeCutDistance(codes); var ctx = new FeatureContext @@ -75,7 +82,7 @@ public sealed class CincinnatiPartSubprogramWriter IsEtch = isEtch, LibraryFile = isEtch ? etchLibrary : cutLibrary, CutDistance = cutDistance, - SheetDiagonal = sheetDiagonal + SheetDiagonal = sheetDiagonal, }; _featureWriter.Write(w, ctx); @@ -84,15 +91,20 @@ public sealed class CincinnatiPartSubprogramWriter w.WriteLine($"M99 (END OF {drawingName})"); } - private void WriteHoleSubprogramCall(TextWriter w, SubProgramCall call, - int featureIndex, bool isLastFeature) + private void WriteHoleSubprogramCall( + TextWriter w, + SubProgramCall call, + int featureIndex, + bool isLastFeature + ) { - var postSubNum = _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num) - ? num : call.Id; + var postSubNum = + _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num) + ? num + : call.Id; - var featureNumber = featureIndex == 0 - ? _config.FeatureLineNumberStart - : 1000 + featureIndex + 1; + var featureNumber = + featureIndex == 0 ? _config.FeatureLineNumberStart : 1000 + featureIndex + 1; var sb = new StringBuilder(); if (_config.UseLineNumbers) @@ -138,8 +150,10 @@ public sealed class CincinnatiPartSubprogramWriter /// Scans all plates and builds a mapping of unique part geometries to sub-program numbers, /// along with their normalized programs for writing. /// - internal static (Dictionary<(int, long), int> mapping, List<(int subNum, string name, Program program)> entries) - BuildRegistry(IEnumerable plates, int startNumber) + internal static ( + Dictionary<(int, long), int> mapping, + List<(int subNum, string name, Program program)> entries + ) BuildRegistry(IEnumerable plates, int startNumber) { var mapping = new Dictionary<(int, long), int>(); var entries = new List<(int, string, Program)>(); @@ -149,7 +163,8 @@ public sealed class CincinnatiPartSubprogramWriter { foreach (var part in plate.Parts) { - if (part.BaseDrawing.IsCutOff) continue; + if (part.BaseDrawing.IsCutOff) + continue; var key = SubprogramKey(part); if (!mapping.ContainsKey(key)) { @@ -180,8 +195,10 @@ public sealed class CincinnatiPartSubprogramWriter /// Scans all parts across all plates and builds a nest-level registry of unique /// hole sub-programs. Deduplicates by comparing sub-program code content. /// - internal static (Dictionary modelToPostMapping, List<(int subNum, Program program)> entries) - BuildHoleRegistry(IEnumerable plates, int startNumber) + internal static ( + Dictionary modelToPostMapping, + List<(int subNum, Program program)> entries + ) BuildHoleRegistry(IEnumerable plates, int startNumber) { var mapping = new Dictionary(); var entries = new List<(int, Program)>(); @@ -192,11 +209,14 @@ public sealed class CincinnatiPartSubprogramWriter { foreach (var part in plate.Parts) { - if (part.BaseDrawing.IsCutOff) continue; + if (part.BaseDrawing.IsCutOff) + continue; foreach (var code in part.Program.Codes) { - if (code is not SubProgramCall call) continue; - if (mapping.ContainsKey(call.Id)) continue; + if (code is not SubProgramCall call) + continue; + if (mapping.ContainsKey(call.Id)) + continue; var canonical = ProgramToCanonical(call.Program); if (contentIndex.TryGetValue(canonical, out var existingNum)) @@ -226,7 +246,9 @@ public sealed class CincinnatiPartSubprogramWriter if (code is LinearMove lm) sb.Append($"L{lm.EndPoint.X:F6},{lm.EndPoint.Y:F6},{(int)lm.Layer}"); else if (code is ArcMove am) - sb.Append($"A{am.EndPoint.X:F6},{am.EndPoint.Y:F6},{am.CenterPoint.X:F6},{am.CenterPoint.Y:F6},{(int)am.Rotation},{(int)am.Layer}"); + sb.Append( + $"A{am.EndPoint.X:F6},{am.EndPoint.Y:F6},{am.CenterPoint.X:F6},{am.CenterPoint.Y:F6},{(int)am.Rotation},{(int)am.Layer}" + ); else if (code is RapidMove rm) sb.Append($"R{rm.EndPoint.X:F6},{rm.EndPoint.Y:F6}"); } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs b/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs index fa1783b..64258d7 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs @@ -17,7 +17,7 @@ namespace OpenNest.Posts.Cincinnati G91, /// Use machine coordinate system. - G53 + G53, } /// @@ -29,7 +29,7 @@ namespace OpenNest.Posts.Cincinnati LibraryFile, /// Explicitly define G89 parameters in the program. - Explicit + Explicit, } /// @@ -41,7 +41,7 @@ namespace OpenNest.Posts.Cincinnati ControllerSide, /// Pre-applied to part geometry during post-processing. - PreApplied + PreApplied, } /// @@ -53,7 +53,7 @@ namespace OpenNest.Posts.Cincinnati Left, /// Kerf applied to the right side of the cut. - Right + Right, } /// @@ -71,7 +71,7 @@ namespace OpenNest.Posts.Cincinnati Auto, /// Do not use M47. - None + None, } /// @@ -86,7 +86,7 @@ namespace OpenNest.Posts.Cincinnati EndOfSheet, /// Pallet exchange at start and end of sheet. - StartAndEnd + StartAndEnd, } /// @@ -132,7 +132,9 @@ namespace OpenNest.Posts.Cincinnati [Category("2. Subprograms")] [DisplayName("Use Part Subprograms")] - [Description("Use M98 sub-programs for part geometry. Reduces output size for repeated parts.")] + [Description( + "Use M98 sub-programs for part geometry. Reduces output size for repeated parts." + )] public bool UsePartSubprograms { get; set; } = false; [Category("2. Subprograms")] @@ -247,13 +249,31 @@ namespace OpenNest.Posts.Cincinnati [Category("9. Feedrates")] [DisplayName("Arc Feedrate Ranges")] - [Description("Radius-based arc feedrate ranges. Matched from smallest to largest MaxRadius.")] - public List ArcFeedrateRanges { get; set; } = new() - { - new() { MaxRadius = 0.125, FeedratePercent = 0.25, VariableNumber = 123 }, - new() { MaxRadius = 0.750, FeedratePercent = 0.50, VariableNumber = 124 }, - new() { MaxRadius = 4.500, FeedratePercent = 0.80, VariableNumber = 125 } - }; + [Description( + "Radius-based arc feedrate ranges. Matched from smallest to largest MaxRadius." + )] + public List ArcFeedrateRanges { get; set; } = + new() + { + new() + { + MaxRadius = 0.125, + FeedratePercent = 0.25, + VariableNumber = 123, + }, + new() + { + MaxRadius = 0.750, + FeedratePercent = 0.50, + VariableNumber = 124, + }, + new() + { + MaxRadius = 4.500, + FeedratePercent = 0.80, + VariableNumber = 125, + }, + }; [Category("A. Variables")] [DisplayName("User Variable Start")] @@ -272,7 +292,9 @@ namespace OpenNest.Posts.Cincinnati [Category("B. Libraries")] [DisplayName("Material Libraries")] - [Description("Material-to-library mapping for cut operations. Maps (material, thickness, gas) to a G89 library file.")] + [Description( + "Material-to-library mapping for cut operations. Maps (material, thickness, gas) to a G89 library file." + )] public List MaterialLibraries { get; set; } = new(); [Category("B. Libraries")] @@ -282,7 +304,9 @@ namespace OpenNest.Posts.Cincinnati [Category("B. Libraries")] [DisplayName("Selected Library")] - [Description("Overrides Material/Thickness/Gas auto-resolution. Pick an existing entry from Material Libraries, or leave blank to auto-resolve.")] + [Description( + "Overrides Material/Thickness/Gas auto-resolution. Pick an existing entry from Material Libraries, or leave blank to auto-resolve." + )] [TypeConverter(typeof(MaterialLibraryNameConverter))] public string SelectedLibrary { get; set; } = ""; @@ -292,10 +316,13 @@ namespace OpenNest.Posts.Cincinnati return ""; return MaterialLibraries - .Where(e => string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase)) - .OrderBy(e => System.Math.Abs(e.Thickness - thickness)) - .Select(e => e.Library) - .FirstOrDefault() ?? ""; + .Where(e => + string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) + ) + .OrderBy(e => System.Math.Abs(e.Thickness - thickness)) + .Select(e => e.Library) + .FirstOrDefault() + ?? ""; } } @@ -325,7 +352,7 @@ namespace OpenNest.Posts.Cincinnati Percentages, /// Radius-range-based variables: F #varNum based on radius range. - Variables + Variables, } /// diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs index c899ae6..b337015 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPostProcessor.cs @@ -9,12 +9,15 @@ using OpenNest.CNC; namespace OpenNest.Posts.Cincinnati { - public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor, IPostProcessorNestAware, IMaterialProvidingPostProcessor + public sealed class CincinnatiPostProcessor + : IConfigurablePostProcessor, + IPostProcessorNestAware, + IMaterialProvidingPostProcessor { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, - Converters = { new JsonStringEnumConverter() } + Converters = { new JsonStringEnumConverter() }, }; public string Name => "Cincinnati CL-707"; @@ -30,8 +33,8 @@ namespace OpenNest.Posts.Cincinnati if (Config?.MaterialLibraries == null) return System.Array.Empty(); - return Config.MaterialLibraries - .Select(e => e.Material) + return Config + .MaterialLibraries.Select(e => e.Material) .Where(s => !string.IsNullOrWhiteSpace(s)); } @@ -83,9 +86,7 @@ namespace OpenNest.Posts.Cincinnati var vars = CreateVariableManager(); // 2. Filter to non-empty plates - var plates = nest.Plates - .Where(p => p.Parts.Count > 0) - .ToList(); + var plates = nest.Plates.Where(p => p.Parts.Count > 0).ToList(); // 3. Register user variables from drawing programs var userVarMapping = RegisterUserVariables(vars, plates); @@ -97,36 +98,55 @@ namespace OpenNest.Posts.Cincinnati // Resolve cut library from nest material/thickness for preamble var firstPlate = plates.FirstOrDefault(); - var initialCutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas); + var initialCutLibrary = resolver.ResolveCutLibrary( + nest.Material?.Name ?? "", + nest.Thickness, + gas + ); // 5. Build part sub-program registry (if enabled) Dictionary<(int, long), int> partSubprograms = null; List<(int subNum, string name, Program program)> subprogramEntries = null; if (Config.UsePartSubprograms) - (partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry(plates, Config.PartSubprogramStart); + (partSubprograms, subprogramEntries) = CincinnatiPartSubprogramWriter.BuildRegistry( + plates, + Config.PartSubprogramStart + ); // 5b. Build hole sub-program registry (SubProgramCalls across all parts) - var holeStartNumber = Config.PartSubprogramStart - + (subprogramEntries?.Count ?? 0); - var (holeMapping, holeEntries) = CincinnatiPartSubprogramWriter.BuildHoleRegistry(plates, holeStartNumber); + var holeStartNumber = Config.PartSubprogramStart + (subprogramEntries?.Count ?? 0); + var (holeMapping, holeEntries) = CincinnatiPartSubprogramWriter.BuildHoleRegistry( + plates, + holeStartNumber + ); // 6. Create writers var preamble = new CincinnatiPreambleWriter(Config); - var sheetWriter = new CincinnatiSheetWriter(Config, vars, - holeMapping.Count > 0 ? holeMapping : null); + var sheetWriter = new CincinnatiSheetWriter( + Config, + vars, + holeMapping.Count > 0 ? holeMapping : null + ); // 7. Build material description from nest var material = nest.Material; - var materialDesc = material != null - ? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}" - : ""; + var materialDesc = + material != null + ? $"{material.Name}{(string.IsNullOrEmpty(material.Grade) ? "" : $", {material.Grade}")}" + : ""; // 8. Write to stream using var writer = new StreamWriter(outputStream, Encoding.UTF8, 1024, leaveOpen: true); // Main program - preamble.WriteMainProgram(writer, nest.Name ?? "NEST", materialDesc, plates, initialCutLibrary); + preamble.WriteMainProgram( + writer, + nest.Name ?? "NEST", + materialDesc, + plates, + initialCutLibrary + ); // Variable declaration subprogram preamble.WriteVariableDeclaration(writer, vars); @@ -137,25 +157,50 @@ namespace OpenNest.Posts.Cincinnati var plate = plates[i]; var layoutIndex = i + 1; var subNumber = Config.SheetSubprogramStart + i; - var cutLibrary = resolver.ResolveCutLibrary(nest.Material?.Name ?? "", nest.Thickness, gas); - sheetWriter.Write(writer, plate, nest.Name ?? "NEST", layoutIndex, subNumber, - cutLibrary, etchLibrary, partSubprograms, userVarMapping); + var cutLibrary = resolver.ResolveCutLibrary( + nest.Material?.Name ?? "", + nest.Thickness, + gas + ); + sheetWriter.Write( + writer, + plate, + nest.Name ?? "NEST", + layoutIndex, + subNumber, + cutLibrary, + etchLibrary, + partSubprograms, + userVarMapping + ); } // Part sub-programs (if enabled) if (subprogramEntries != null) { - var partSubWriter = new CincinnatiPartSubprogramWriter(Config, - holeMapping.Count > 0 ? holeMapping : null); - var sheetDiagonal = firstPlate != null - ? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width - + firstPlate.Size.Length * firstPlate.Size.Length) - : 100.0; + var partSubWriter = new CincinnatiPartSubprogramWriter( + Config, + holeMapping.Count > 0 ? holeMapping : null + ); + var sheetDiagonal = + firstPlate != null + ? System.Math.Sqrt( + firstPlate.Size.Width * firstPlate.Size.Width + + firstPlate.Size.Length * firstPlate.Size.Length + ) + : 100.0; foreach (var (subNum, name, pgm) in subprogramEntries) { - partSubWriter.Write(writer, pgm, name, subNum, - initialCutLibrary, etchLibrary, sheetDiagonal); + partSubWriter.Write( + writer, + pgm, + name, + subNum, + initialCutLibrary, + etchLibrary, + sheetDiagonal + ); } } @@ -163,16 +208,26 @@ namespace OpenNest.Posts.Cincinnati if (holeEntries.Count > 0) { var holeSubWriter = new CincinnatiPartSubprogramWriter(Config); - var sheetDiagonal = firstPlate != null - ? System.Math.Sqrt(firstPlate.Size.Width * firstPlate.Size.Width - + firstPlate.Size.Length * firstPlate.Size.Length) - : 100.0; + var sheetDiagonal = + firstPlate != null + ? System.Math.Sqrt( + firstPlate.Size.Width * firstPlate.Size.Width + + firstPlate.Size.Length * firstPlate.Size.Length + ) + : 100.0; foreach (var (subNum, pgm) in holeEntries) { CincinnatiPartSubprogramWriter.EnsureLeadingRapid(pgm); - holeSubWriter.Write(writer, pgm, "HOLE", subNum, - initialCutLibrary, etchLibrary, sheetDiagonal); + holeSubWriter.Write( + writer, + pgm, + "HOLE", + subNum, + initialCutLibrary, + etchLibrary, + sheetDiagonal + ); } } @@ -186,13 +241,17 @@ namespace OpenNest.Posts.Cincinnati } private Dictionary<(int drawingId, string varName), int> RegisterUserVariables( - ProgramVariableManager vars, List plates) + ProgramVariableManager vars, + List plates + ) { var mapping = new Dictionary<(int drawingId, string varName), int>(); var nextNumber = Config.UserVariableStart; // Track global variables by name so they share a single number - var globalNumbers = new Dictionary(System.StringComparer.OrdinalIgnoreCase); + var globalNumbers = new Dictionary( + System.StringComparer.OrdinalIgnoreCase + ); // Collect unique drawings from all plates var seenDrawings = new HashSet(); @@ -285,19 +344,31 @@ namespace OpenNest.Posts.Cincinnati private ProgramVariableManager CreateVariableManager() { var vars = new ProgramVariableManager(); - vars.GetOrCreate("ProcessFeedrate", 148); // Set by G89, no expression + vars.GetOrCreate("ProcessFeedrate", 148); // Set by G89, no expression vars.GetOrCreate("LeadInFeedrate", 126, $"[#148*{Config.LeadInFeedratePercent}]"); - vars.GetOrCreate("LeadInArcLine2Feedrate", 127, $"[#148*{Config.LeadInArcLine2FeedratePercent}]"); - vars.GetOrCreate("CircleFeedrate", 128, Config.CircleFeedrateMultiplier.ToString("0.#")); + vars.GetOrCreate( + "LeadInArcLine2Feedrate", + 127, + $"[#148*{Config.LeadInArcLine2FeedratePercent}]" + ); + vars.GetOrCreate( + "CircleFeedrate", + 128, + Config.CircleFeedrateMultiplier.ToString("0.#") + ); vars.GetOrCreate("LeadOutFeedrate", 129, $"[#148*{Config.LeadOutFeedratePercent}]"); if (Config.ArcFeedrate == ArcFeedrateMode.Variables) { foreach (var range in Config.ArcFeedrateRanges) { - var name = $"ArcFeedR{range.MaxRadius.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)}"; - vars.GetOrCreate(name, range.VariableNumber, - $"[#148*{range.FeedratePercent.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}]"); + var name = + $"ArcFeedR{range.MaxRadius.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)}"; + vars.GetOrCreate( + name, + range.VariableNumber, + $"[#148*{range.FeedratePercent.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)}]" + ); } } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs index 615c791..2dc34f2 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiPreambleWriter.cs @@ -23,12 +23,24 @@ public sealed class CincinnatiPreambleWriter /// Writes the main program header block. /// /// Resolved G89 library file for the initial process setup. - public void WriteMainProgram(TextWriter w, string nestName, string materialDescription, - List plates, string initialLibrary) + public void WriteMainProgram( + TextWriter w, + string nestName, + string materialDescription, + List plates, + string initialLibrary + ) { w.WriteLine(CoordinateFormatter.Comment($"NEST {nestName}")); w.WriteLine(CoordinateFormatter.Comment($"CONFIGURATION - {_config.ConfigurationName}")); - w.WriteLine(CoordinateFormatter.Comment(DateTime.Now.ToString("MM-dd-yyyy hh:mm:ss tt", System.Globalization.CultureInfo.InvariantCulture))); + w.WriteLine( + CoordinateFormatter.Comment( + DateTime.Now.ToString( + "MM-dd-yyyy hh:mm:ss tt", + System.Globalization.CultureInfo.InvariantCulture + ) + ) + ); if (!string.IsNullOrEmpty(materialDescription)) w.WriteLine(CoordinateFormatter.Comment($"Material = {materialDescription}")); @@ -45,7 +57,10 @@ public sealed class CincinnatiPreambleWriter w.WriteLine("M42"); - if (_config.ProcessParameterMode == G89Mode.LibraryFile && !string.IsNullOrEmpty(initialLibrary)) + if ( + _config.ProcessParameterMode == G89Mode.LibraryFile + && !string.IsNullOrEmpty(initialLibrary) + ) w.WriteLine($"G89 P{initialLibrary}"); w.WriteLine($"M98 P{_config.VariableDeclarationSubprogram} (Variable Declaration)"); @@ -61,9 +76,8 @@ public sealed class CincinnatiPreambleWriter var subNum = _config.SheetSubprogramStart + i; var qty = System.Math.Max(plates[i].Quantity, 1); var lParam = qty > 1 ? $" L{qty}" : ""; - var sheetLabel = qty > 1 - ? $"LAYOUT {layoutNumber} - {qty} SHEETS" - : $"LAYOUT {layoutNumber}"; + var sheetLabel = + qty > 1 ? $"LAYOUT {layoutNumber} - {qty} SHEETS" : $"LAYOUT {layoutNumber}"; w.WriteLine($"N{layoutNumber} M98 P{subNum}{lParam} ({sheetLabel})"); } diff --git a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs index 575b922..f099313 100644 --- a/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs +++ b/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs @@ -19,8 +19,11 @@ public sealed class CincinnatiSheetWriter private readonly CincinnatiFeatureWriter _featureWriter; private readonly Dictionary _holeSubprograms; - public CincinnatiSheetWriter(CincinnatiPostConfig config, ProgramVariableManager vars, - Dictionary holeSubprograms = null) + public CincinnatiSheetWriter( + CincinnatiPostConfig config, + ProgramVariableManager vars, + Dictionary holeSubprograms = null + ) { _config = config; _vars = vars; @@ -38,10 +41,17 @@ public sealed class CincinnatiSheetWriter /// Optional mapping of (drawingId, rotationKey) to sub-program number. /// When provided, non-cutoff parts are emitted as M98 calls instead of inline features. /// - public void Write(TextWriter w, Plate plate, string nestName, int layoutIndex, int subNumber, - string cutLibrary, string etchLibrary, + public void Write( + TextWriter w, + Plate plate, + string nestName, + int layoutIndex, + int subNumber, + string cutLibrary, + string etchLibrary, Dictionary<(int, long), int> partSubprograms = null, - Dictionary<(int drawingId, string varName), int> userVarMapping = null) + Dictionary<(int drawingId, string varName), int> userVarMapping = null + ) { if (plate.Parts.Count == 0) return; @@ -59,8 +69,12 @@ public sealed class CincinnatiSheetWriter w.WriteLine($"( Layout {layoutIndex} )"); w.WriteLine($"( SHEET NAME = {_fmt.FormatCoord(width)} X {_fmt.FormatCoord(length)} )"); w.WriteLine($"( Total parts on sheet = {partCount} )"); - w.WriteLine($"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)"); - w.WriteLine($"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)"); + w.WriteLine( + $"#{_config.SheetWidthVariable}={_fmt.FormatCoord(width)} (SHEET WIDTH FOR CUTOFFS)" + ); + w.WriteLine( + $"#{_config.SheetLengthVariable}={_fmt.FormatCoord(length)} (SHEET LENGTH FOR CUTOFFS)" + ); // 2. Coordinate setup w.WriteLine("M42"); @@ -76,23 +90,40 @@ public sealed class CincinnatiSheetWriter w.WriteLine("GOTO1( Goto Feature )"); // 3. Order parts: non-cutoff sorted by Bottom then Left, cutoffs last - var nonCutoffParts = plate.Parts - .Where(p => !p.BaseDrawing.IsCutOff) + var nonCutoffParts = plate + .Parts.Where(p => !p.BaseDrawing.IsCutOff) .OrderBy(p => p.Bottom) .ThenBy(p => p.Left) .ToList(); - var cutoffParts = plate.Parts - .Where(p => p.BaseDrawing.IsCutOff) - .ToList(); + var cutoffParts = plate.Parts.Where(p => p.BaseDrawing.IsCutOff).ToList(); var allParts = nonCutoffParts.Concat(cutoffParts).ToList(); // 4. Emit parts if (partSubprograms != null) - WritePartsWithSubprograms(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, partSubprograms, userVarMapping); + WritePartsWithSubprograms( + w, + allParts, + cutLibrary, + etchLibrary, + sheetDiagonal, + width, + length, + partSubprograms, + userVarMapping + ); else - WritePartsInline(w, allParts, cutLibrary, etchLibrary, sheetDiagonal, width, length, userVarMapping); + WritePartsInline( + w, + allParts, + cutLibrary, + etchLibrary, + sheetDiagonal, + width, + length, + userVarMapping + ); // 5. Footer w.WriteLine("M42"); @@ -101,11 +132,17 @@ public sealed class CincinnatiSheetWriter w.WriteLine($"M99 (END OF {nestName}.{layoutIndex:D3})"); } - private void WritePartsWithSubprograms(TextWriter w, List allParts, - string cutLibrary, string etchLibrary, double sheetDiagonal, - double plateWidth, double plateLength, + private void WritePartsWithSubprograms( + TextWriter w, + List allParts, + string cutLibrary, + string etchLibrary, + double sheetDiagonal, + double plateWidth, + double plateLength, Dictionary<(int, long), int> partSubprograms, - Dictionary<(int drawingId, string varName), int> userVarMapping) + Dictionary<(int drawingId, string varName), int> userVarMapping + ) { var lastPartName = ""; var featureIndex = 0; @@ -124,8 +161,15 @@ public sealed class CincinnatiSheetWriter if (hasSubprogram) { - WriteSubprogramCall(w, part, subNum, featureIndex, partName, - isSafetyHeadraise, isLastPart); + WriteSubprogramCall( + w, + part, + subNum, + featureIndex, + partName, + isSafetyHeadraise, + isLastPart + ); featureIndex++; } else @@ -146,9 +190,10 @@ public sealed class CincinnatiSheetWriter continue; } - var featureNumber = featureIndex == 0 - ? _config.FeatureLineNumberStart - : 1000 + featureIndex + 1; + var featureNumber = + featureIndex == 0 + ? _config.FeatureLineNumberStart + : 1000 + featureIndex + 1; var cutDistance = FeatureUtils.ComputeCutDistance(codes); @@ -170,7 +215,7 @@ public sealed class CincinnatiSheetWriter DrawingId = part.BaseDrawing.Id, IsCutOff = part.BaseDrawing.IsCutOff, PlateWidth = plateWidth, - PlateLength = plateLength + PlateLength = plateLength, }; _featureWriter.Write(w, ctx); @@ -182,17 +227,23 @@ public sealed class CincinnatiSheetWriter } } - private void WriteSubprogramCall(TextWriter w, Part part, int subNum, - int featureIndex, string partName, bool isSafetyHeadraise, bool isLastPart) + private void WriteSubprogramCall( + TextWriter w, + Part part, + int subNum, + int featureIndex, + string partName, + bool isSafetyHeadraise, + bool isLastPart + ) { // Safety headraise before rapid to new part if (isSafetyHeadraise && _config.SafetyHeadraiseDistance.HasValue) w.WriteLine($"M47 P{_config.SafetyHeadraiseDistance.Value} (Safety Headraise)"); // Rapid to part position (bounding box lower-left) - var featureNumber = featureIndex == 0 - ? _config.FeatureLineNumberStart - : 1000 + featureIndex + 1; + var featureNumber = + featureIndex == 0 ? _config.FeatureLineNumberStart : 1000 + featureIndex + 1; var sb = new StringBuilder(); if (_config.UseLineNumbers) @@ -217,14 +268,20 @@ public sealed class CincinnatiSheetWriter w.WriteLine("M47"); } - private void WriteHoleSubprogramCall(TextWriter w, SubProgramCall call, int featureIndex, bool isLastFeature) + private void WriteHoleSubprogramCall( + TextWriter w, + SubProgramCall call, + int featureIndex, + bool isLastFeature + ) { - var postSubNum = _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num) - ? num : call.Id; + var postSubNum = + _holeSubprograms != null && _holeSubprograms.TryGetValue(call.Id, out var num) + ? num + : call.Id; - var featureNumber = featureIndex == 0 - ? _config.FeatureLineNumberStart - : 1000 + featureIndex + 1; + var featureNumber = + featureIndex == 0 ? _config.FeatureLineNumberStart : 1000 + featureIndex + 1; // Shift the local origin to the hole center via G52 (manual §1.52). // G52 does not move the nozzle, so the sub-program's first rapid @@ -247,10 +304,16 @@ public sealed class CincinnatiSheetWriter w.WriteLine("M47"); } - private void WritePartsInline(TextWriter w, List allParts, - string cutLibrary, string etchLibrary, double sheetDiagonal, - double plateWidth, double plateLength, - Dictionary<(int drawingId, string varName), int> userVarMapping) + private void WritePartsInline( + TextWriter w, + List allParts, + string cutLibrary, + string etchLibrary, + double sheetDiagonal, + double plateWidth, + double plateLength, + Dictionary<(int drawingId, string varName), int> userVarMapping + ) { // Split and classify features, ordering etch before cut per part var features = new List<(Part part, List codes, bool isEtch)>(); @@ -279,9 +342,7 @@ public sealed class CincinnatiSheetWriter continue; } - var featureNumber = i == 0 - ? _config.FeatureLineNumberStart - : 1000 + i + 1; + var featureNumber = i == 0 ? _config.FeatureLineNumberStart : 1000 + i + 1; var cutDistance = FeatureUtils.ComputeCutDistance(codes); @@ -303,12 +364,11 @@ public sealed class CincinnatiSheetWriter DrawingId = part.BaseDrawing.Id, IsCutOff = part.BaseDrawing.IsCutOff, PlateWidth = plateWidth, - PlateLength = plateLength + PlateLength = plateLength, }; _featureWriter.Write(w, ctx); lastPartName = partName; } } - } diff --git a/OpenNest.Posts.Cincinnati/CoordinateFormatter.cs b/OpenNest.Posts.Cincinnati/CoordinateFormatter.cs index 0397026..05462c6 100644 --- a/OpenNest.Posts.Cincinnati/CoordinateFormatter.cs +++ b/OpenNest.Posts.Cincinnati/CoordinateFormatter.cs @@ -13,7 +13,8 @@ namespace OpenNest.Posts.Cincinnati public string FormatCoord(double value) { - return System.Math.Round(value, _accuracy) + return System + .Math.Round(value, _accuracy) .ToString(_format, System.Globalization.CultureInfo.InvariantCulture); } diff --git a/OpenNest.Posts.Cincinnati/FeatureUtils.cs b/OpenNest.Posts.Cincinnati/FeatureUtils.cs index e75b5b2..29718b8 100644 --- a/OpenNest.Posts.Cincinnati/FeatureUtils.cs +++ b/OpenNest.Posts.Cincinnati/FeatureUtils.cs @@ -52,7 +52,9 @@ public static class FeatureUtils /// /// Classifies features as etch or cut and orders etch features before cut features. /// - public static List<(List codes, bool isEtch)> ClassifyAndOrder(List> features) + public static List<(List codes, bool isEtch)> ClassifyAndOrder( + List> features + ) { var result = new List<(List, bool)>(); var etch = new List>(); @@ -157,27 +159,33 @@ public static class FeatureUtils return 0.0; // Full circle: start ≈ end - if (Tolerance.IsEqualTo(startPos.X, arc.EndPoint.X) - && Tolerance.IsEqualTo(startPos.Y, arc.EndPoint.Y)) + if ( + Tolerance.IsEqualTo(startPos.X, arc.EndPoint.X) + && Tolerance.IsEqualTo(startPos.Y, arc.EndPoint.Y) + ) return 2.0 * System.Math.PI * radius; var startAngle = System.Math.Atan2( startPos.Y - arc.CenterPoint.Y, - startPos.X - arc.CenterPoint.X); + startPos.X - arc.CenterPoint.X + ); var endAngle = System.Math.Atan2( arc.EndPoint.Y - arc.CenterPoint.Y, - arc.EndPoint.X - arc.CenterPoint.X); + arc.EndPoint.X - arc.CenterPoint.X + ); double sweep; if (arc.Rotation == RotationType.CW) { sweep = startAngle - endAngle; - if (sweep <= 0) sweep += 2.0 * System.Math.PI; + if (sweep <= 0) + sweep += 2.0 * System.Math.PI; } else { sweep = endAngle - startAngle; - if (sweep <= 0) sweep += 2.0 * System.Math.PI; + if (sweep <= 0) + sweep += 2.0 * System.Math.PI; } return radius * sweep; diff --git a/OpenNest.Posts.Cincinnati/MaterialLibraryNameConverter.cs b/OpenNest.Posts.Cincinnati/MaterialLibraryNameConverter.cs index 1013b13..56d1b6c 100644 --- a/OpenNest.Posts.Cincinnati/MaterialLibraryNameConverter.cs +++ b/OpenNest.Posts.Cincinnati/MaterialLibraryNameConverter.cs @@ -18,11 +18,13 @@ namespace OpenNest.Posts.Cincinnati if (config?.MaterialLibraries != null) { - names.AddRange(config.MaterialLibraries - .Select(e => e.Library) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .OrderBy(s => s, StringComparer.OrdinalIgnoreCase)); + names.AddRange( + config + .MaterialLibraries.Select(e => e.Library) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) + ); } return new StandardValuesCollection(names); diff --git a/OpenNest.Posts.Cincinnati/MaterialLibraryResolver.cs b/OpenNest.Posts.Cincinnati/MaterialLibraryResolver.cs index 82ee863..63fd24c 100644 --- a/OpenNest.Posts.Cincinnati/MaterialLibraryResolver.cs +++ b/OpenNest.Posts.Cincinnati/MaterialLibraryResolver.cs @@ -25,9 +25,10 @@ public sealed class MaterialLibraryResolver return EnsureLibExtension(_selectedLibrary); var entry = _materialLibraries.FirstOrDefault(e => - string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) && - System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance && - string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase)); + string.Equals(e.Material, materialName, StringComparison.OrdinalIgnoreCase) + && System.Math.Abs(e.Thickness - thickness) <= ThicknessTolerance + && string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase) + ); return EnsureLibExtension(entry?.Library ?? ""); } @@ -35,7 +36,8 @@ public sealed class MaterialLibraryResolver public string ResolveEtchLibrary(string gas) { var entry = _etchLibraries.FirstOrDefault(e => - string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase)); + string.Equals(e.Gas, gas, StringComparison.OrdinalIgnoreCase) + ); return EnsureLibExtension(entry?.Library ?? ""); } diff --git a/OpenNest.Posts.Cincinnati/SpeedClassifier.cs b/OpenNest.Posts.Cincinnati/SpeedClassifier.cs index f7b6a4a..05acb6f 100644 --- a/OpenNest.Posts.Cincinnati/SpeedClassifier.cs +++ b/OpenNest.Posts.Cincinnati/SpeedClassifier.cs @@ -8,8 +8,10 @@ namespace OpenNest.Posts.Cincinnati public string Classify(double contourLength, double sheetDiagonal) { var ratio = contourLength / sheetDiagonal; - if (ratio >= FastThreshold) return "FAST"; - if (ratio <= SlowThreshold) return "SLOW"; + if (ratio >= FastThreshold) + return "FAST"; + if (ratio <= SlowThreshold) + return "SLOW"; return "MEDIUM"; } diff --git a/OpenNest.Posts.GravographIS/GravographISPort.cs b/OpenNest.Posts.GravographIS/GravographISPort.cs index 29e9bc0..bc1a304 100644 --- a/OpenNest.Posts.GravographIS/GravographISPort.cs +++ b/OpenNest.Posts.GravographIS/GravographISPort.cs @@ -42,8 +42,9 @@ namespace OpenNest.Posts.GravographIS ReadTimeout = WriteTimeoutMs, // DTR/RTS are needed for some USB-serial bridges and for RTS/CTS flow: DtrEnable = true, - RtsEnable = handshake != Handshake.RequestToSend && - handshake != Handshake.RequestToSendXOnXOff, + RtsEnable = + handshake != Handshake.RequestToSend + && handshake != Handshake.RequestToSendXOnXOff, }; port.Open(); @@ -57,7 +58,8 @@ namespace OpenNest.Posts.GravographIS /// public void StreamJob(byte[] data, CancellationToken cancellationToken = default) { - if (data == null) throw new ArgumentNullException(nameof(data)); + if (data == null) + throw new ArgumentNullException(nameof(data)); if (port == null || !port.IsOpen) throw new InvalidOperationException("Port is not open."); @@ -76,16 +78,23 @@ namespace OpenNest.Posts.GravographIS // Block until the OS has handed the last bytes to the line. SerialPort // doesn't expose flush-and-drain directly; BaseStream.Flush is a no-op // on Windows, so this is best-effort. - try { port.BaseStream.Flush(); } - catch { /* ignored — Flush is advisory on SerialPort */ } + try + { + port.BaseStream.Flush(); + } + catch + { /* ignored — Flush is advisory on SerialPort */ + } } public void Close() { - if (port == null) return; + if (port == null) + return; try { - if (port.IsOpen) port.Close(); + if (port.IsOpen) + port.Close(); } finally { diff --git a/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs b/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs index c2b1a2a..7d1fd45 100644 --- a/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs +++ b/OpenNest.Posts.GravographIS/GravographISPostProcessor.cs @@ -27,7 +27,8 @@ namespace OpenNest.Posts.GravographIS public string Name => "Gravograph IS8000"; public string Author => "OpenNest"; - public string Description => "Gravograph IS8000 mechanical engraver (binary HPGL over serial)"; + public string Description => + "Gravograph IS8000 mechanical engraver (binary HPGL over serial)"; public GravographISWriterOptions WriterOptions { get; } = new GravographISWriterOptions(); @@ -79,8 +80,10 @@ namespace OpenNest.Posts.GravographIS public void Post(Nest nest, Stream outputStream) { - if (nest == null) throw new ArgumentNullException(nameof(nest)); - if (outputStream == null) throw new ArgumentNullException(nameof(outputStream)); + if (nest == null) + throw new ArgumentNullException(nameof(nest)); + if (outputStream == null) + throw new ArgumentNullException(nameof(outputStream)); var passes = BuildPasses(Extractor.ExtractLayered(nest)); new GravographISWriter(WriterOptions).Write(passes, outputStream); @@ -141,9 +144,12 @@ namespace OpenNest.Posts.GravographIS /// /// Buffers the encoded job in memory, then streams it to the named COM port. /// - public void Stream(Nest nest, string portName, + public void Stream( + Nest nest, + string portName, Handshake handshake = Handshake.RequestToSend, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default + ) { byte[] bytes; using (var ms = new MemoryStream()) diff --git a/OpenNest.Posts.GravographIS/PolylinePrePass.cs b/OpenNest.Posts.GravographIS/PolylinePrePass.cs index 1b67b8f..f8e6a20 100644 --- a/OpenNest.Posts.GravographIS/PolylinePrePass.cs +++ b/OpenNest.Posts.GravographIS/PolylinePrePass.cs @@ -22,9 +22,11 @@ namespace OpenNest.Posts.GravographIS /// public static List> Stitch( IEnumerable> polylines, - double tolerance = DefaultStitchTolerance) + double tolerance = DefaultStitchTolerance + ) { - if (polylines == null) throw new ArgumentNullException(nameof(polylines)); + if (polylines == null) + throw new ArgumentNullException(nameof(polylines)); var segs = new List>(); foreach (var p in polylines) @@ -44,15 +46,18 @@ namespace OpenNest.Posts.GravographIS for (int j = 0; j < segs.Count; j++) { - if (i == j) continue; + if (i == j) + continue; var b = segs[j]; // a-end ↔ b-start: append b to a (skip duplicated joint) if (Near(a[a.Count - 1], b[0], tolerance)) { - for (int k = 1; k < b.Count; k++) a.Add(b[k]); + for (int k = 1; k < b.Count; k++) + a.Add(b[k]); segs.RemoveAt(j); - if (j < i) i--; + if (j < i) + i--; changed = true; break; } @@ -60,9 +65,11 @@ namespace OpenNest.Posts.GravographIS // a-end ↔ b-end: append reversed b to a if (Near(a[a.Count - 1], b[b.Count - 1], tolerance)) { - for (int k = b.Count - 2; k >= 0; k--) a.Add(b[k]); + for (int k = b.Count - 2; k >= 0; k--) + a.Add(b[k]); segs.RemoveAt(j); - if (j < i) i--; + if (j < i) + i--; changed = true; break; } @@ -72,10 +79,12 @@ namespace OpenNest.Posts.GravographIS { var combined = new List(b.Count + a.Count - 1); combined.AddRange(b); - for (int k = 1; k < a.Count; k++) combined.Add(a[k]); + for (int k = 1; k < a.Count; k++) + combined.Add(a[k]); segs[i] = combined; segs.RemoveAt(j); - if (j < i) i--; + if (j < i) + i--; changed = true; break; } @@ -84,20 +93,23 @@ namespace OpenNest.Posts.GravographIS if (Near(a[0], b[0], tolerance)) { var combined = new List(b.Count + a.Count - 1); - for (int k = b.Count - 1; k >= 0; k--) combined.Add(b[k]); - for (int k = 1; k < a.Count; k++) combined.Add(a[k]); + for (int k = b.Count - 1; k >= 0; k--) + combined.Add(b[k]); + for (int k = 1; k < a.Count; k++) + combined.Add(a[k]); segs[i] = combined; segs.RemoveAt(j); - if (j < i) i--; + if (j < i) + i--; changed = true; break; } } - if (changed) break; + if (changed) + break; } - } - while (changed); + } while (changed); return segs; } @@ -111,9 +123,11 @@ namespace OpenNest.Posts.GravographIS public static List> Reorder( IEnumerable> polylines, bool allowReverse = true, - Vector? origin = null) + Vector? origin = null + ) { - if (polylines == null) throw new ArgumentNullException(nameof(polylines)); + if (polylines == null) + throw new ArgumentNullException(nameof(polylines)); var pool = new List>(); foreach (var p in polylines) @@ -173,7 +187,8 @@ namespace OpenNest.Posts.GravographIS IEnumerable> polylines, double stitchTolerance = DefaultStitchTolerance, bool allowReverse = true, - Vector? origin = null) + Vector? origin = null + ) { var stitched = Stitch(polylines, stitchTolerance); return Reorder(stitched, allowReverse, origin); diff --git a/OpenNest.Tests/Api/CutParametersTests.cs b/OpenNest.Tests/Api/CutParametersTests.cs index 9cca3b5..9f49873 100644 --- a/OpenNest.Tests/Api/CutParametersTests.cs +++ b/OpenNest.Tests/Api/CutParametersTests.cs @@ -25,7 +25,7 @@ public class CutParametersTests PierceTime = TimeSpan.FromSeconds(1.0), LeadInLength = 0.25, PostProcessor = "CL-707", - Units = Units.Millimeters + Units = Units.Millimeters, }; Assert.Equal(200, cp.Feedrate); diff --git a/OpenNest.Tests/Api/NestRequestTests.cs b/OpenNest.Tests/Api/NestRequestTests.cs index fb1a49a..0af676d 100644 --- a/OpenNest.Tests/Api/NestRequestTests.cs +++ b/OpenNest.Tests/Api/NestRequestTests.cs @@ -27,7 +27,7 @@ public class NestRequestTests { var request = new NestRequest { - Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }] + Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }], }; Assert.Single(request.Parts); @@ -60,9 +60,9 @@ public class NestRequestTests Quantity = 3, PartSpacing = 0.2, EdgeSpacing = new Spacing(1, 2, 3, 4), - Quadrant = 3 - } - ] + Quadrant = 3, + }, + ], }; var plate = Assert.Single(request.Plates!); diff --git a/OpenNest.Tests/Api/NestResponsePersistenceTests.cs b/OpenNest.Tests/Api/NestResponsePersistenceTests.cs index 2f3a236..29ba92f 100644 --- a/OpenNest.Tests/Api/NestResponsePersistenceTests.cs +++ b/OpenNest.Tests/Api/NestResponsePersistenceTests.cs @@ -17,11 +17,28 @@ public class NestResponsePersistenceTests var nest = CreateNest("test-part", new Size(60, 120)); var request = new NestRequest { - Parts = [new NestRequestPart { Id = "test-part", DxfPath = "test.dxf", Quantity = 5 }], - Plates = [new NestRequestPlate { Id = "sheet", Size = new Size(60, 120), Quantity = 1, PartSpacing = 0.1 }], + Parts = + [ + new NestRequestPart + { + Id = "test-part", + DxfPath = "test.dxf", + Quantity = 5, + }, + ], + Plates = + [ + new NestRequestPlate + { + Id = "sheet", + Size = new Size(60, 120), + Quantity = 1, + PartSpacing = 0.1, + }, + ], Material = "Steel", Thickness = 0.125, - Spacing = 0.1 + Spacing = 0.1, }; var original = new NestResponse { @@ -35,7 +52,7 @@ public class NestResponsePersistenceTests StockUsage = [new NestStockUsage("sheet", 1, 0)], PlateStockMappings = [new NestPlateStockMapping(0, "sheet")], Nest = nest, - Request = request + Request = request, }; var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.nestquote"); @@ -118,9 +135,25 @@ public class NestResponsePersistenceTests Nest = CreateNest("custom-id", new Size(10, 10)), Request = new NestRequest { - Parts = [new NestRequestPart { Id = "custom-id", DxfPath = dxfPath, Quantity = 3 }], - Plates = [new NestRequestPlate { Id = "finite-stock", Size = new Size(10, 10), Quantity = 1 }] - } + Parts = + [ + new NestRequestPart + { + Id = "custom-id", + DxfPath = dxfPath, + Quantity = 3, + }, + ], + Plates = + [ + new NestRequestPlate + { + Id = "finite-stock", + Size = new Size(10, 10), + Quantity = 1, + }, + ], + }, }; try @@ -131,9 +164,18 @@ public class NestResponsePersistenceTests Assert.False(File.Exists(dxfPath)); Assert.Equal(NestJobStatus.Incomplete, loaded.Status); Assert.Equal(NestJobStopReason.StockExhausted, loaded.StopReason); - Assert.Equal(new NestPartFulfillment("custom-id", 3, 1, 2), Assert.Single(loaded.Fulfillment)); - Assert.Equal(new NestStockUsage("finite-stock", 1, 0), Assert.Single(loaded.StockUsage)); - Assert.Equal(new NestPlateStockMapping(0, "finite-stock"), Assert.Single(loaded.PlateStockMappings)); + Assert.Equal( + new NestPartFulfillment("custom-id", 3, 1, 2), + Assert.Single(loaded.Fulfillment) + ); + Assert.Equal( + new NestStockUsage("finite-stock", 1, 0), + Assert.Single(loaded.StockUsage) + ); + Assert.Equal( + new NestPlateStockMapping(0, "finite-stock"), + Assert.Single(loaded.PlateStockMappings) + ); Assert.Equal("custom-id", Assert.Single(loaded.Request.Parts).Id); Assert.Equal("finite-stock", Assert.Single(loaded.Request.Plates!).Id); Assert.Single(loaded.Nest.Drawings); @@ -159,12 +201,20 @@ public class NestResponsePersistenceTests { using var fs = new FileStream(path, FileMode.Create); using var zip = new ZipArchive(fs, ZipArchiveMode.Create); - await WriteEntryAsync(zip, "request.json", """ + await WriteEntryAsync( + zip, + "request.json", + """ {"parts":[{"dxfPath":"legacy-missing.dxf","quantity":2}],"sheetSize":{"width":60,"length":120},"material":"Steel","thickness":0.06,"spacing":0.1,"strategy":0} - """); - await WriteEntryAsync(zip, "response.json", """ + """ + ); + await WriteEntryAsync( + zip, + "response.json", + """ {"sheetCount":1,"utilization":0.75,"cutTimeTicks":120000,"elapsedTicks":340000} - """); + """ + ); var nestEntry = zip.CreateEntry("nest.nest"); await using var stream = nestEntry.Open(); diff --git a/OpenNest.Tests/Api/NestRunnerTests.cs b/OpenNest.Tests/Api/NestRunnerTests.cs index 21ca635..84d6c2c 100644 --- a/OpenNest.Tests/Api/NestRunnerTests.cs +++ b/OpenNest.Tests/Api/NestRunnerTests.cs @@ -22,7 +22,7 @@ public class NestRunnerTests { Parts = [new NestRequestPart { DxfPath = dxfPath, Quantity = 4 }], SheetSize = new Size(10, 10), - Spacing = 0.1 + Spacing = 0.1, }; var response = await NestRunner.RunAsync(request); @@ -34,7 +34,10 @@ public class NestRunnerTests var stock = Assert.Single(response.StockUsage); Assert.Equal("legacy-sheet", stock.StockId); Assert.Null(stock.Remaining); - Assert.All(response.PlateStockMappings, mapping => Assert.Equal("legacy-sheet", mapping.StockId)); + Assert.All( + response.PlateStockMappings, + mapping => Assert.Equal("legacy-sheet", mapping.StockId) + ); Assert.Equal(response.SheetCount, response.PlateStockMappings.Count); Assert.NotNull(response.Nest); Assert.Contains(response.Nest.Drawings, drawing => drawing.Name == "part-0"); @@ -54,21 +57,44 @@ public class NestRunnerTests try { - var response = await NestRunner.RunAsync(new NestRequest - { - Parts = [new NestRequestPart { Id = "square", DxfPath = dxfPath, Quantity = 5 }], - Plates = - [ - new NestRequestPlate { Id = "small", Size = new Size(5, 5), Quantity = 1 }, - new NestRequestPlate { Id = "large", Size = new Size(9, 9), Quantity = 1 } - ] - }); + var response = await NestRunner.RunAsync( + new NestRequest + { + Parts = + [ + new NestRequestPart + { + Id = "square", + DxfPath = dxfPath, + Quantity = 5, + }, + ], + Plates = + [ + new NestRequestPlate + { + Id = "small", + Size = new Size(5, 5), + Quantity = 1, + }, + new NestRequestPlate + { + Id = "large", + Size = new Size(9, 9), + Quantity = 1, + }, + ], + } + ); Assert.Equal(NestJobStatus.Complete, response.Status); Assert.Equal(2, response.SheetCount); Assert.Equal(5, Assert.Single(response.Fulfillment).Placed); Assert.Equal(0, response.Fulfillment[0].Unplaced); - Assert.Equal(new[] { "large", "small" }, response.PlateStockMappings.Select(mapping => mapping.StockId).Order()); + Assert.Equal( + new[] { "large", "small" }, + response.PlateStockMappings.Select(mapping => mapping.StockId).Order() + ); Assert.Equal(1, response.StockUsage.Single(usage => usage.StockId == "small").Used); Assert.Equal(1, response.StockUsage.Single(usage => usage.StockId == "large").Used); Assert.All(response.StockUsage, usage => Assert.Equal(0, usage.Remaining)); @@ -86,11 +112,21 @@ public class NestRunnerTests try { - var response = await NestRunner.RunAsync(new NestRequest - { - Parts = [new NestRequestPart { Id = "square", DxfPath = dxfPath, Quantity = 1 }], - Plates = [] - }); + var response = await NestRunner.RunAsync( + new NestRequest + { + Parts = + [ + new NestRequestPart + { + Id = "square", + DxfPath = dxfPath, + Quantity = 1, + }, + ], + Plates = [], + } + ); Assert.Equal(NestJobStatus.Incomplete, response.Status); Assert.Equal(NestJobStopReason.StockExhausted, response.StopReason); @@ -115,17 +151,30 @@ public class NestRunnerTests try { - var response = await NestRunner.RunAsync(new NestRequest - { - Parts = [new NestRequestPart + var response = await NestRunner.RunAsync( + new NestRequest { - Id = "locked-square", - DxfPath = dxfPath, - Quantity = 2, - AllowRotation = false - }], - Plates = [new NestRequestPlate { Id = "only-sheet", Size = new Size(5, 5), Quantity = 1 }] - }); + Parts = + [ + new NestRequestPart + { + Id = "locked-square", + DxfPath = dxfPath, + Quantity = 2, + AllowRotation = false, + }, + ], + Plates = + [ + new NestRequestPlate + { + Id = "only-sheet", + Size = new Size(5, 5), + Quantity = 1, + }, + ], + } + ); Assert.Equal(NestJobStatus.Incomplete, response.Status); Assert.Equal(NestJobStopReason.StockExhausted, response.StopReason); @@ -153,15 +202,35 @@ public class NestRunnerTests try { - var response = await NestRunner.RunAsync(new NestRequest - { - Parts = [new NestRequestPart { Id = "square", DxfPath = dxfPath, Quantity = 5 }], - Plates = - [ - new NestRequestPlate { Id = "small", Size = new Size(5, 5), Quantity = 1 }, - new NestRequestPlate { Id = "large", Size = new Size(9, 9), Quantity = 1 } - ] - }); + var response = await NestRunner.RunAsync( + new NestRequest + { + Parts = + [ + new NestRequestPart + { + Id = "square", + DxfPath = dxfPath, + Quantity = 5, + }, + ], + Plates = + [ + new NestRequestPlate + { + Id = "small", + Size = new Size(5, 5), + Quantity = 1, + }, + new NestRequestPlate + { + Id = "large", + Size = new Size(9, 9), + Quantity = 1, + }, + ], + } + ); Assert.Equal(2, response.SheetCount); Assert.Equal(80d / 106d, response.Utilization, precision: 6); @@ -184,8 +253,8 @@ public class NestRunnerTests Parts = [ new NestRequestPart { Id = "duplicate", DxfPath = dxfPath }, - new NestRequestPart { Id = "duplicate", DxfPath = dxfPath } - ] + new NestRequestPart { Id = "duplicate", DxfPath = dxfPath }, + ], }; await Assert.ThrowsAsync(() => NestRunner.RunAsync(request)); @@ -201,7 +270,7 @@ public class NestRunnerTests { var request = new NestRequest { - Parts = [new NestRequestPart { DxfPath = "nonexistent.dxf", Quantity = 1 }] + Parts = [new NestRequestPart { DxfPath = "nonexistent.dxf", Quantity = 1 }], }; await Assert.ThrowsAsync(() => NestRunner.RunAsync(request)); diff --git a/OpenNest.Tests/Bending/BendModelTests.cs b/OpenNest.Tests/Bending/BendModelTests.cs index f0bfa21..9779bdf 100644 --- a/OpenNest.Tests/Bending/BendModelTests.cs +++ b/OpenNest.Tests/Bending/BendModelTests.cs @@ -15,7 +15,7 @@ public class BendModelTests Direction = BendDirection.Up, Angle = 90, Radius = 0.06, - NoteText = "UP 90° R0.06" + NoteText = "UP 90° R0.06", }; Assert.Equal(0, bend.StartPoint.X); @@ -29,11 +29,7 @@ public class BendModelTests [Fact] public void Bend_ToLine_ReturnsGeometryLine() { - var bend = new Bend - { - StartPoint = new Vector(0, 5), - EndPoint = new Vector(10, 5) - }; + var bend = new Bend { StartPoint = new Vector(0, 5), EndPoint = new Vector(10, 5) }; var line = bend.ToLine(); @@ -46,11 +42,7 @@ public class BendModelTests [Fact] public void Bend_Length_ComputesCorrectly() { - var bend = new Bend - { - StartPoint = new Vector(0, 0), - EndPoint = new Vector(3, 4) - }; + var bend = new Bend { StartPoint = new Vector(0, 0), EndPoint = new Vector(3, 4) }; Assert.Equal(5.0, bend.Length, 0.001); } @@ -78,7 +70,7 @@ public class BendModelTests { Direction = BendDirection.Up, Angle = 90, - Radius = 0.06 + Radius = 0.06, }; var str = bend.ToString(); @@ -96,7 +88,7 @@ public class BendModelTests StartPoint = new Vector(0, 0), EndPoint = new Vector(10, 0), Direction = BendDirection.Down, - Angle = 90 + Angle = 90, }; Assert.Null(bend.SourceEntity); } @@ -109,7 +101,7 @@ public class BendModelTests { StartPoint = line.StartPoint, EndPoint = line.EndPoint, - SourceEntity = line + SourceEntity = line, }; Assert.Same(line, bend.SourceEntity); } diff --git a/OpenNest.Tests/Bending/CadBendNoteTests.cs b/OpenNest.Tests/Bending/CadBendNoteTests.cs index c892365..cb279c5 100644 --- a/OpenNest.Tests/Bending/CadBendNoteTests.cs +++ b/OpenNest.Tests/Bending/CadBendNoteTests.cs @@ -14,14 +14,26 @@ public class CadBendNoteTests public void DetectedNote_HidesOnlyItsSourceText_AndReturnsWhenBendRemoved() { var doc = new CadDocument(); - doc.Entities.Add(new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0)) + doc.Entities.Add( + new Line(new XYZ(0, 0, 0), new XYZ(10, 0, 0)) + { + Layer = new Layer("BEND"), + LineType = new LineType("CENTER"), + } + ); + var note = new MText { - Layer = new Layer("BEND"), - LineType = new LineType("CENTER") - }); - var note = new MText { Value = "UP 90° R0.125", InsertPoint = new XYZ(5, 0.1, 0), Height = 0.2 }; + Value = "UP 90° R0.125", + InsertPoint = new XYZ(5, 0.1, 0), + Height = 0.2, + }; doc.Entities.Add(note); - var unrelated = new MText { Value = note.Value, InsertPoint = new XYZ(50, 50, 0), Height = 0.2 }; + var unrelated = new MText + { + Value = note.Value, + InsertPoint = new XYZ(50, 50, 0), + Height = 0.2, + }; doc.Entities.Add(unrelated); var bends = new SolidWorksBendDetector().DetectBends(doc); @@ -29,7 +41,13 @@ public class CadBendNoteTests Assert.Equal(note.Handle, bend.SourceNoteHandle); var text = new CadText { SourceHandle = note.Handle, Value = note.Value }; Assert.True(text.IsReplacedByBendNote(bends)); - Assert.False(new CadText { SourceHandle = unrelated.Handle, Value = note.Value }.IsReplacedByBendNote(bends)); + Assert.False( + new CadText + { + SourceHandle = unrelated.Handle, + Value = note.Value, + }.IsReplacedByBendNote(bends) + ); bends.Clear(); Assert.False(text.IsReplacedByBendNote(bends)); @@ -42,6 +60,8 @@ public class CadBendNoteTests Assert.False(text.IsReplacedByBendNote(null)); Assert.False(text.IsReplacedByBendNote(new[] { new Bend { NoteText = text.Value } })); Assert.False(text.IsReplacedByBendNote(new[] { new Bend { SourceNoteHandle = 42 } })); - Assert.False(new CadText().IsReplacedByBendNote(new[] { new Bend { NoteText = text.Value } })); + Assert.False( + new CadText().IsReplacedByBendNote(new[] { new Bend { NoteText = text.Value } }) + ); } } diff --git a/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs b/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs index 379cdde..f2873ed 100644 --- a/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs +++ b/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs @@ -17,8 +17,7 @@ public class SolidWorksBendDetectorTests [Fact] public void Registry_ContainsSolidWorksDetector() { - Assert.Contains(BendDetectorRegistry.Detectors, - d => d.Name == "SolidWorks"); + Assert.Contains(BendDetectorRegistry.Detectors, d => d.Name == "SolidWorks"); } [Fact] @@ -32,7 +31,12 @@ public class SolidWorksBendDetectorTests [Fact] public void EllipseConverter_ProducesArcsDirectly() { - var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11 Test.dxf"); + var path = Path.Combine( + AppContext.BaseDirectory, + "Bending", + "TestData", + "4526 A14 PT11 Test.dxf" + ); Assert.True(File.Exists(path), $"Test DXF not found: {path}"); var result = OpenNest.IO.Dxf.Import(path); @@ -50,14 +54,21 @@ public class SolidWorksBendDetectorTests var simplifier = new OpenNest.Geometry.GeometrySimplifier(); var candidates = simplifier.Analyze(shape); - Assert.True(candidates.Count <= 10, - $"Expected <=10 simplifier candidates but got {candidates.Count}"); + Assert.True( + candidates.Count <= 10, + $"Expected <=10 simplifier candidates but got {candidates.Count}" + ); } [Fact] public void Import_TrimmedEllipse_NoClosingChord() { - var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT11.dxf"); + var path = Path.Combine( + AppContext.BaseDirectory, + "Bending", + "TestData", + "4526 A14 PT11.dxf" + ); Assert.True(File.Exists(path), $"Test DXF not found: {path}"); var result = OpenNest.IO.Dxf.Import(path); @@ -78,7 +89,12 @@ public class SolidWorksBendDetectorTests [Fact] public void DetectBends_SplitBendLine_PropagatesNote() { - var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT23.dxf"); + var path = Path.Combine( + AppContext.BaseDirectory, + "Bending", + "TestData", + "4526 A14 PT23.dxf" + ); Assert.True(File.Exists(path), $"Test DXF not found: {path}"); using var reader = new DxfReader(path); @@ -88,22 +104,32 @@ public class SolidWorksBendDetectorTests var bends = detector.DetectBends(doc); Assert.Equal(5, bends.Count); - Assert.All(bends, b => - { - Assert.NotNull(b.NoteText); - Assert.NotNull(b.SourceNoteHandle); - Assert.Contains(doc.Entities, e => e.Handle == b.SourceNoteHandle - && e is ACadSharp.Entities.MText); - Assert.Equal(BendDirection.Up, b.Direction); - Assert.Equal(90.0, b.Angle); - Assert.Equal(0.125, b.Radius); - }); + Assert.All( + bends, + b => + { + Assert.NotNull(b.NoteText); + Assert.NotNull(b.SourceNoteHandle); + Assert.Contains( + doc.Entities, + e => e.Handle == b.SourceNoteHandle && e is ACadSharp.Entities.MText + ); + Assert.Equal(BendDirection.Up, b.Direction); + Assert.Equal(90.0, b.Angle); + Assert.Equal(0.125, b.Radius); + } + ); } [Fact] public void DetectBends_RealDxf_ParsesNotesCorrectly() { - var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT45.dxf"); + var path = Path.Combine( + AppContext.BaseDirectory, + "Bending", + "TestData", + "4526 A14 PT45.dxf" + ); Assert.True(File.Exists(path), $"Test DXF not found: {path}"); using var reader = new DxfReader(path); diff --git a/OpenNest.Tests/BestFit/BestFitOverlapTests.cs b/OpenNest.Tests/BestFit/BestFitOverlapTests.cs index 8a819b3..b87e47b 100644 --- a/OpenNest.Tests/BestFit/BestFitOverlapTests.cs +++ b/OpenNest.Tests/BestFit/BestFitOverlapTests.cs @@ -58,9 +58,11 @@ public class BestFitOverlapTests if (parts[0].Intersects(parts[1], out var pts)) { overlapping++; - _output.WriteLine($" OVERLAP #{overlapping}: Test {result.Candidate.TestNumber} " + - $"Part2Rot={OpenNest.Math.Angle.ToDegrees(result.Candidate.Part2Rotation):F1}° " + - $"collision pts={pts.Count}"); + _output.WriteLine( + $" OVERLAP #{overlapping}: Test {result.Candidate.TestNumber} " + + $"Part2Rot={OpenNest.Math.Angle.ToDegrees(result.Candidate.Part2Rotation):F1}° " + + $"collision pts={pts.Count}" + ); } } diff --git a/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs b/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs index 3162ac3..c791c1a 100644 --- a/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs +++ b/OpenNest.Tests/BestFit/BestFitResultFrameTests.cs @@ -16,8 +16,10 @@ public class BestFitResultFrameTests var result = EvaluateOffsetPair(canonical, new Vector(40, 30)); - Assert.True(IsNonAxisAligned(result.OptimalRotation), - $"Expected a non-axis-aligned result, got {Angle.ToDegrees(result.OptimalRotation):F2} degrees."); + Assert.True( + IsNonAxisAligned(result.OptimalRotation), + $"Expected a non-axis-aligned result, got {Angle.ToDegrees(result.OptimalRotation):F2} degrees." + ); var parts = result.BuildCanonicalParts(); var bounds = result.GetCutBounds(parts); @@ -55,7 +57,7 @@ public class BestFitResultFrameTests Part1Rotation = 0, Part2Rotation = System.Math.PI, Part2Offset = offset, - Spacing = 0.25 + Spacing = 0.25, }; return new PairEvaluator().Evaluate(candidate); diff --git a/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs b/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs index 5f4596d..4c0d6f4 100644 --- a/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs +++ b/OpenNest.Tests/BestFit/NfpBestFitIntegrationTests.cs @@ -38,9 +38,7 @@ public class NfpBestFitIntegrationTests var drawing = TestHelpers.MakeLShapeDrawing(); var results = finder.FindBestFits(drawing); - var bestUtilization = results - .Where(r => r.Keep) - .Max(r => r.Utilization); + var bestUtilization = results.Where(r => r.Keep).Max(r => r.Utilization); Assert.True(bestUtilization > 0.5); } @@ -51,7 +49,6 @@ public class NfpBestFitIntegrationTests var drawing = TestHelpers.MakeSquareDrawing(); var results = finder.FindBestFits(drawing); - Assert.All(results.Where(r => r.Keep), r => - Assert.Equal("Valid", r.Reason)); + Assert.All(results.Where(r => r.Keep), r => Assert.Equal("Valid", r.Reason)); } } diff --git a/OpenNest.Tests/BestFit/NfpSlideStrategyTests.cs b/OpenNest.Tests/BestFit/NfpSlideStrategyTests.cs index cef92ed..8329988 100644 --- a/OpenNest.Tests/BestFit/NfpSlideStrategyTests.cs +++ b/OpenNest.Tests/BestFit/NfpSlideStrategyTests.cs @@ -119,6 +119,9 @@ public class NfpSlideStrategyTests validCount++; } - Assert.True(validCount > 0, $"No non-overlapping candidates found out of {candidates.Count} total. Candidate 0 offset: {candidates[0].Part2Offset}"); + Assert.True( + validCount > 0, + $"No non-overlapping candidates found out of {candidates.Count} total. Candidate 0 offset: {candidates[0].Part2Offset}" + ); } } diff --git a/OpenNest.Tests/Bom/BomAnalyzerTests.cs b/OpenNest.Tests/Bom/BomAnalyzerTests.cs index 1e066ac..ff4ba1f 100644 --- a/OpenNest.Tests/Bom/BomAnalyzerTests.cs +++ b/OpenNest.Tests/Bom/BomAnalyzerTests.cs @@ -9,9 +9,27 @@ public class BomAnalyzerTests { var items = new List { - new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 }, - new BomItem { FileName = "PT02", Thickness = 0.25, Material = "AISI 304", Qty = 3 }, - new BomItem { FileName = "PT03", Thickness = 0.375, Material = "AISI 304", Qty = 1 }, + new BomItem + { + FileName = "PT01", + Thickness = 0.25, + Material = "AISI 304", + Qty = 2, + }, + new BomItem + { + FileName = "PT02", + Thickness = 0.25, + Material = "AISI 304", + Qty = 3, + }, + new BomItem + { + FileName = "PT03", + Thickness = 0.375, + Material = "AISI 304", + Qty = 1, + }, }; var result = BomAnalyzer.Analyze(items, "C:\\fake"); @@ -26,9 +44,27 @@ public class BomAnalyzerTests { var items = new List { - new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 }, - new BomItem { FileName = null, Thickness = 0.25, Material = "AISI 304", Qty = 3 }, - new BomItem { FileName = "", Thickness = 0.25, Material = "AISI 304", Qty = 1 }, + new BomItem + { + FileName = "PT01", + Thickness = 0.25, + Material = "AISI 304", + Qty = 2, + }, + new BomItem + { + FileName = null, + Thickness = 0.25, + Material = "AISI 304", + Qty = 3, + }, + new BomItem + { + FileName = "", + Thickness = 0.25, + Material = "AISI 304", + Qty = 1, + }, }; var result = BomAnalyzer.Analyze(items, "C:\\fake"); @@ -41,8 +77,20 @@ public class BomAnalyzerTests { var items = new List { - new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 }, - new BomItem { FileName = "PT02", Thickness = null, Material = "AISI 304", Qty = 3 }, + new BomItem + { + FileName = "PT01", + Thickness = 0.25, + Material = "AISI 304", + Qty = 2, + }, + new BomItem + { + FileName = "PT02", + Thickness = null, + Material = "AISI 304", + Qty = 3, + }, }; var result = BomAnalyzer.Analyze(items, "C:\\fake"); @@ -56,8 +104,20 @@ public class BomAnalyzerTests { var items = new List { - new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 1 }, - new BomItem { FileName = "PT02", Thickness = 0.25, Material = "aisi 304", Qty = 1 }, + new BomItem + { + FileName = "PT01", + Thickness = 0.25, + Material = "AISI 304", + Qty = 1, + }, + new BomItem + { + FileName = "PT02", + Thickness = 0.25, + Material = "aisi 304", + Qty = 1, + }, }; var result = BomAnalyzer.Analyze(items, "C:\\fake"); @@ -69,7 +129,10 @@ public class BomAnalyzerTests [Fact] public void Analyze_MatchesDxfFiles_WithAndWithoutExtension() { - var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N")); + var tempDir = Path.Combine( + Path.GetTempPath(), + "BomAnalyzerTest_" + Guid.NewGuid().ToString("N") + ); Directory.CreateDirectory(tempDir); try @@ -78,14 +141,24 @@ public class BomAnalyzerTests var items = new List { - new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 }, + new BomItem + { + FileName = "PT01", + Thickness = 0.25, + Material = "AISI 304", + Qty = 2, + }, }; var result = BomAnalyzer.Analyze(items, tempDir); Assert.Single(result.Groups); Assert.Single(result.Groups[0].Parts); - Assert.EndsWith(".dxf", result.Groups[0].Parts[0].DxfPath, StringComparison.OrdinalIgnoreCase); + Assert.EndsWith( + ".dxf", + result.Groups[0].Parts[0].DxfPath, + StringComparison.OrdinalIgnoreCase + ); Assert.Empty(result.Unmatched); } finally @@ -97,14 +170,23 @@ public class BomAnalyzerTests [Fact] public void Analyze_ReportsUnmatchedItems_WhenDxfNotFound() { - var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N")); + var tempDir = Path.Combine( + Path.GetTempPath(), + "BomAnalyzerTest_" + Guid.NewGuid().ToString("N") + ); Directory.CreateDirectory(tempDir); try { var items = new List { - new BomItem { FileName = "PT99", Thickness = 0.25, Material = "AISI 304", Qty = 1 }, + new BomItem + { + FileName = "PT99", + Thickness = 0.25, + Material = "AISI 304", + Qty = 1, + }, }; var result = BomAnalyzer.Analyze(items, tempDir); @@ -123,8 +205,20 @@ public class BomAnalyzerTests { var items = new List { - new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 1 }, - new BomItem { FileName = "PT02", Thickness = 0.25, Material = "Plain Carbon Steel", Qty = 1 }, + new BomItem + { + FileName = "PT01", + Thickness = 0.25, + Material = "AISI 304", + Qty = 1, + }, + new BomItem + { + FileName = "PT02", + Thickness = 0.25, + Material = "Plain Carbon Steel", + Qty = 1, + }, }; var result = BomAnalyzer.Analyze(items, "C:\\fake"); @@ -135,7 +229,10 @@ public class BomAnalyzerTests [Fact] public void Analyze_GroupPartsCount_MatchesBomItems() { - var tempDir = Path.Combine(Path.GetTempPath(), "BomAnalyzerTest_" + Guid.NewGuid().ToString("N")); + var tempDir = Path.Combine( + Path.GetTempPath(), + "BomAnalyzerTest_" + Guid.NewGuid().ToString("N") + ); Directory.CreateDirectory(tempDir); try @@ -146,9 +243,27 @@ public class BomAnalyzerTests var items = new List { - new BomItem { FileName = "PT01", Thickness = 0.25, Material = "AISI 304", Qty = 2 }, - new BomItem { FileName = "PT02", Thickness = 0.25, Material = "AISI 304", Qty = 5 }, - new BomItem { FileName = "PT03", Thickness = 0.375, Material = "AISI 304", Qty = 1 }, + new BomItem + { + FileName = "PT01", + Thickness = 0.25, + Material = "AISI 304", + Qty = 2, + }, + new BomItem + { + FileName = "PT02", + Thickness = 0.25, + Material = "AISI 304", + Qty = 5, + }, + new BomItem + { + FileName = "PT03", + Thickness = 0.375, + Material = "AISI 304", + Qty = 1, + }, }; var result = BomAnalyzer.Analyze(items, tempDir); diff --git a/OpenNest.Tests/CNC/RapidEnumeratorTests.cs b/OpenNest.Tests/CNC/RapidEnumeratorTests.cs index 9db9903..0b964b1 100644 --- a/OpenNest.Tests/CNC/RapidEnumeratorTests.cs +++ b/OpenNest.Tests/CNC/RapidEnumeratorTests.cs @@ -14,7 +14,11 @@ namespace OpenNest.Tests.CNC pgm.Codes.Add(new LinearMove(2, 0)); pgm.Codes.Add(new RapidMove(3, 3)); - var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0)); + var segments = RapidEnumerator.Enumerate( + pgm, + basePos: new Vector(100, 200), + startPos: new Vector(0, 0) + ); // Origin → first pierce, then interior rapid from contour end to next rapid target. Assert.Equal(2, segments.Count); @@ -35,7 +39,11 @@ namespace OpenNest.Tests.CNC pgm.Codes.Add(new LinearMove(0, 5)); pgm.Codes.Add(new RapidMove(1, 1)); - var segments = RapidEnumerator.Enumerate(pgm, basePos: new Vector(100, 200), startPos: new Vector(0, 0)); + var segments = RapidEnumerator.Enumerate( + pgm, + basePos: new Vector(100, 200), + startPos: new Vector(0, 0) + ); Assert.Equal(2, segments.Count); // First rapid: plate origin → part pierce at basePos. @@ -56,16 +64,18 @@ namespace OpenNest.Tests.CNC sub.Codes.Add(new LinearMove(0, 0.1)); var pgm = new Program(Mode.Absolute); - pgm.Codes.Add(new RapidMove(0.2, 0.3)); // first pierce (perimeter lead-in) - pgm.Codes.Add(new LinearMove(1.0, 1.0)); // contour move - pgm.Codes.Add(new SubProgramCall - { - Id = 1, - Program = sub, - Offset = new Vector(2, 2), // hole center (drawing-local) - }); + pgm.Codes.Add(new RapidMove(0.2, 0.3)); // first pierce (perimeter lead-in) + pgm.Codes.Add(new LinearMove(1.0, 1.0)); // contour move + pgm.Codes.Add( + new SubProgramCall + { + Id = 1, + Program = sub, + Offset = new Vector(2, 2), // hole center (drawing-local) + } + ); - var basePos = new Vector(100, 200); // part.Location + var basePos = new Vector(100, 200); // part.Location var segments = RapidEnumerator.Enumerate(pgm, basePos, startPos: new Vector(0, 0)); // Expected rapids: diff --git a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs index 860bc35..8475cf9 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs @@ -6,39 +6,43 @@ namespace OpenNest.Tests.Cincinnati; public class CincinnatiFeatureWriterTests { - private static CincinnatiPostConfig DefaultConfig() => new() - { - UseLineNumbers = true, - FeatureLineNumberStart = 1, - UseAntiDive = true, - KerfCompensation = KerfMode.ControllerSide, - DefaultKerfSide = KerfSide.Left, - ProcessParameterMode = G89Mode.LibraryFile, - InteriorM47 = M47Mode.Always, - ExteriorM47 = M47Mode.Always, - UseSpeedGas = false, - PostedAccuracy = 4, - SafetyHeadraiseDistance = 2000 - }; - - private static FeatureContext SimpleContext(List? codes = null) => new() - { - Codes = codes ?? new List + private static CincinnatiPostConfig DefaultConfig() => + new() { - new RapidMove(13.401, 57.4895), - new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin }, - new LinearMove(20.0, 57.5) { Layer = LayerType.Cut } - }, - FeatureNumber = 1, - PartName = "BRACKET", - IsFirstFeatureOfPart = true, - IsLastFeatureOnSheet = false, - IsSafetyHeadraise = false, - IsExteriorFeature = false, - LibraryFile = "MILD10", - CutDistance = 18.0, - SheetDiagonal = 30.0 - }; + UseLineNumbers = true, + FeatureLineNumberStart = 1, + UseAntiDive = true, + KerfCompensation = KerfMode.ControllerSide, + DefaultKerfSide = KerfSide.Left, + ProcessParameterMode = G89Mode.LibraryFile, + InteriorM47 = M47Mode.Always, + ExteriorM47 = M47Mode.Always, + UseSpeedGas = false, + PostedAccuracy = 4, + SafetyHeadraiseDistance = 2000, + }; + + private static FeatureContext SimpleContext(List? codes = null) => + new() + { + Codes = + codes + ?? new List + { + new RapidMove(13.401, 57.4895), + new LinearMove(14.0, 57.5) { Layer = LayerType.Leadin }, + new LinearMove(20.0, 57.5) { Layer = LayerType.Cut }, + }, + FeatureNumber = 1, + PartName = "BRACKET", + IsFirstFeatureOfPart = true, + IsLastFeatureOnSheet = false, + IsSafetyHeadraise = false, + IsExteriorFeature = false, + LibraryFile = "MILD10", + CutDistance = 18.0, + SheetDiagonal = 30.0, + }; private static string WriteFeature(CincinnatiPostConfig config, FeatureContext ctx) { @@ -229,7 +233,10 @@ public class CincinnatiFeatureWriterTests endPoint: new Vector(10.0, 20.0), centerPoint: new Vector(15.0, 20.0), rotation: RotationType.CW - ) { Layer = LayerType.Cut } + ) + { + Layer = LayerType.Cut, + }, }; var ctx = SimpleContext(codes); @@ -247,12 +254,18 @@ public class CincinnatiFeatureWriterTests var cwCodes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut } + new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) + { + Layer = LayerType.Cut, + }, }; var ccwCodes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW) { Layer = LayerType.Cut } + new ArcMove(new Vector(20.0, 20.0), new Vector(15.0, 20.0), RotationType.CCW) + { + Layer = LayerType.Cut, + }, }; var cwOutput = WriteFeature(config, SimpleContext(cwCodes)); @@ -350,7 +363,7 @@ public class CincinnatiFeatureWriterTests { new RapidMove(1.0, 1.0), new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin }, - new LinearMove(3.0, 1.0) { Layer = LayerType.Cut } + new LinearMove(3.0, 1.0) { Layer = LayerType.Cut }, }; var ctx = SimpleContext(codes); ctx.IsEtch = true; @@ -372,7 +385,7 @@ public class CincinnatiFeatureWriterTests new RapidMove(1.0, 1.0), new LinearMove(2.0, 1.0) { Layer = LayerType.Cut }, new LinearMove(3.0, 1.0) { Layer = LayerType.Cut }, - new LinearMove(4.0, 1.0) { Layer = LayerType.Cut } + new LinearMove(4.0, 1.0) { Layer = LayerType.Cut }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -390,7 +403,7 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(1.0, 1.0), - new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin } + new LinearMove(2.0, 1.0) { Layer = LayerType.Leadin }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -407,7 +420,10 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut } + new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) + { + Layer = LayerType.Cut, + }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -478,7 +494,7 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(1.0, 1.0), - new LinearMove(2.0, 1.0) { Layer = LayerType.Leadout } + new LinearMove(2.0, 1.0) { Layer = LayerType.Leadout }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -494,7 +510,10 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(12.0, 20.0), new Vector(11.0, 20.0), RotationType.CCW) { Layer = LayerType.Leadin } + new ArcMove(new Vector(12.0, 20.0), new Vector(11.0, 20.0), RotationType.CCW) + { + Layer = LayerType.Leadin, + }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -512,7 +531,10 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(11.0, 20.0), new Vector(10.5, 20.0), RotationType.CW) { Layer = LayerType.Cut } + new ArcMove(new Vector(11.0, 20.0), new Vector(10.5, 20.0), RotationType.CW) + { + Layer = LayerType.Cut, + }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -530,7 +552,10 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut } + new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) + { + Layer = LayerType.Cut, + }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -548,7 +573,10 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(0.0, 0.0), - new ArcMove(new Vector(20.0, 0.0), new Vector(10.0, 0.0), RotationType.CCW) { Layer = LayerType.Cut } + new ArcMove(new Vector(20.0, 0.0), new Vector(10.0, 0.0), RotationType.CCW) + { + Layer = LayerType.Cut, + }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); @@ -566,7 +594,10 @@ public class CincinnatiFeatureWriterTests var codes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) { Layer = LayerType.Cut } + new ArcMove(new Vector(10.2, 20.0), new Vector(10.1, 20.0), RotationType.CW) + { + Layer = LayerType.Cut, + }, }; var ctx = SimpleContext(codes); var output = WriteFeature(config, ctx); diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs index 1acd4ec..61909e5 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiPostProcessorTests.cs @@ -14,11 +14,7 @@ public class CincinnatiPostProcessorTests public void Post_ProducesOutput_ForSinglePlateNest() { var nest = CreateTestNest(); - var config = new CincinnatiPostConfig - { - ConfigurationName = "CL940", - PostedAccuracy = 4 - }; + var config = new CincinnatiPostConfig { ConfigurationName = "CL940", PostedAccuracy = 4 }; var post = new CincinnatiPostProcessor(config); using var ms = new MemoryStream(); @@ -64,7 +60,7 @@ public class CincinnatiPostProcessorTests var config = new CincinnatiPostConfig { PostedAccuracy = 4, - ArcFeedrate = ArcFeedrateMode.Variables + ArcFeedrate = ArcFeedrateMode.Variables, }; var post = new CincinnatiPostProcessor(config); @@ -84,7 +80,7 @@ public class CincinnatiPostProcessorTests var config = new CincinnatiPostConfig { PostedAccuracy = 4, - ArcFeedrate = ArcFeedrateMode.None + ArcFeedrate = ArcFeedrateMode.None, }; var post = new CincinnatiPostProcessor(config); @@ -189,18 +185,24 @@ public class CincinnatiPostProcessorTests UseAntiDive = true, MaterialLibraries = new() { - new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.135, Gas = "N2", Library = "MS135N2PANEL.lib" } + new MaterialLibraryEntry + { + Material = "Mild Steel", + Thickness = 0.135, + Gas = "N2", + Library = "MS135N2PANEL.lib", + }, }, EtchLibraries = new() { - new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" } - } + new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" }, + }, }; var opts = new JsonSerializerOptions { WriteIndented = true, - Converters = { new JsonStringEnumConverter() } + Converters = { new JsonStringEnumConverter() }, }; var json = JsonSerializer.Serialize(config, opts); var deserialized = JsonSerializer.Deserialize(json, opts); @@ -239,7 +241,7 @@ public class CincinnatiPostProcessorTests { PostedAccuracy = 4, UsePartSubprograms = true, - PartSubprogramStart = 200 + PartSubprogramStart = 200, }; var post = new CincinnatiPostProcessor(config); @@ -279,7 +281,7 @@ public class CincinnatiPostProcessorTests { PostedAccuracy = 4, UsePartSubprograms = true, - PartSubprogramStart = 200 + PartSubprogramStart = 200, }; var post = new CincinnatiPostProcessor(config); @@ -317,7 +319,7 @@ public class CincinnatiPostProcessorTests { PostedAccuracy = 4, UsePartSubprograms = true, - PartSubprogramStart = 200 + PartSubprogramStart = 200, }; var post = new CincinnatiPostProcessor(config); @@ -349,7 +351,7 @@ public class CincinnatiPostProcessorTests { PostedAccuracy = 4, UsePartSubprograms = true, - PartSubprogramStart = 200 + PartSubprogramStart = 200, }; var post = new CincinnatiPostProcessor(config); @@ -372,13 +374,13 @@ public class CincinnatiPostProcessorTests var config = new CincinnatiPostConfig { UsePartSubprograms = true, - PartSubprogramStart = 300 + PartSubprogramStart = 300, }; var opts = new JsonSerializerOptions { WriteIndented = true, - Converters = { new JsonStringEnumConverter() } + Converters = { new JsonStringEnumConverter() }, }; var json = JsonSerializer.Serialize(config, opts); var deserialized = JsonSerializer.Deserialize(json, opts); @@ -394,10 +396,10 @@ public class CincinnatiPostProcessorTests // first segment in the CNC output because the feature writer uses // the first LinearMove endpoint as the pierce point. var pgm = new Program(Mode.Incremental); - pgm.Codes.Add(new LinearMove(0, 2)); // (0,0) → (0,2) - pgm.Codes.Add(new LinearMove(2, 0)); // (0,2) → (2,2) - pgm.Codes.Add(new LinearMove(0, -2)); // (2,2) → (2,0) - pgm.Codes.Add(new LinearMove(-2, 0)); // (2,0) → (0,0) + pgm.Codes.Add(new LinearMove(0, 2)); // (0,0) → (0,2) + pgm.Codes.Add(new LinearMove(2, 0)); // (0,2) → (2,2) + pgm.Codes.Add(new LinearMove(0, -2)); // (2,2) → (2,0) + pgm.Codes.Add(new LinearMove(-2, 0)); // (2,0) → (0,0) var drawing = new Drawing("ClosedSquare", pgm); var nest = new Nest("TestClosure"); @@ -406,11 +408,7 @@ public class CincinnatiPostProcessorTests plate.Parts.Add(new Part(drawing, new Vector(1, 1))); nest.Plates.Add(plate); - var config = new CincinnatiPostConfig - { - UsePartSubprograms = true, - PostedAccuracy = 4 - }; + var config = new CincinnatiPostConfig { UsePartSubprograms = true, PostedAccuracy = 4 }; var post = new CincinnatiPostProcessor(config); using var ms = new MemoryStream(); diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs index 08ed581..2348fc0 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiPreambleWriterTests.cs @@ -14,7 +14,7 @@ public class CincinnatiPreambleWriterTests var config = new CincinnatiPostConfig { ConfigurationName = "CL940", - PostedUnits = Units.Inches + PostedUnits = Units.Inches, }; var sb = new StringBuilder(); using var sw = new StringWriter(sb); @@ -152,7 +152,7 @@ public class CincinnatiPreambleWriterTests { new(48, 96) { Quantity = 5 }, new(72, 48) { Quantity = 2 }, - new(36, 48) { Quantity = 1 } + new(36, 48) { Quantity = 1 }, }; writer.WriteMainProgram(sw, "Test", "", plates, ""); diff --git a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs index 87dbbcd..1b83fca 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiSheetWriterTests.cs @@ -13,10 +13,7 @@ public class CincinnatiSheetWriterTests [Fact] public void WriteSheet_EmitsSheetHeader() { - var config = new CincinnatiPostConfig - { - PostedAccuracy = 4 - }; + var config = new CincinnatiPostConfig { PostedAccuracy = 4 }; var plate = new Plate(48.0, 96.0); plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram()))); @@ -42,7 +39,7 @@ public class CincinnatiSheetWriterTests var config = new CincinnatiPostConfig { PalletExchange = PalletMode.EndOfSheet, - PostedAccuracy = 4 + PostedAccuracy = 4, }; var plate = new Plate(48.0, 96.0); plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram()))); @@ -147,7 +144,7 @@ public class CincinnatiSheetWriterTests var config = new CincinnatiPostConfig { PalletExchange = PalletMode.StartAndEnd, - PostedAccuracy = 4 + PostedAccuracy = 4, }; var plate = new Plate(48.0, 96.0); plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram()))); @@ -168,7 +165,7 @@ public class CincinnatiSheetWriterTests var config = new CincinnatiPostConfig { PalletExchange = PalletMode.None, - PostedAccuracy = 4 + PostedAccuracy = 4, }; var plate = new Plate(48.0, 96.0); plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram()))); @@ -189,7 +186,7 @@ public class CincinnatiSheetWriterTests var config = new CincinnatiPostConfig { PalletExchange = PalletMode.EndOfSheet, - PostedAccuracy = 4 + PostedAccuracy = 4, }; var plate = new Plate(48.0, 96.0); plate.Parts.Add(new Part(new Drawing("TestPart", CreateSimpleProgram()))); @@ -234,7 +231,10 @@ public class CincinnatiSheetWriterTests var codes = new List { new RapidMove(10.0, 20.0), - new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) { Layer = LayerType.Cut } + new ArcMove(new Vector(10.0, 20.0), new Vector(15.0, 20.0), RotationType.CW) + { + Layer = LayerType.Cut, + }, }; var distance = FeatureUtils.ComputeCutDistance(codes); @@ -249,7 +249,7 @@ public class CincinnatiSheetWriterTests { new RapidMove(0, 0), new LinearMove(1, 0) { Layer = LayerType.Scribe }, - new LinearMove(1, 1) { Layer = LayerType.Scribe } + new LinearMove(1, 1) { Layer = LayerType.Scribe }, }; Assert.True(FeatureUtils.IsEtch(codes)); @@ -262,7 +262,7 @@ public class CincinnatiSheetWriterTests { new RapidMove(0, 0), new LinearMove(1, 0) { Layer = LayerType.Cut }, - new LinearMove(1, 1) { Layer = LayerType.Cut } + new LinearMove(1, 1) { Layer = LayerType.Cut }, }; Assert.False(FeatureUtils.IsEtch(codes)); @@ -271,10 +271,7 @@ public class CincinnatiSheetWriterTests [Fact] public void IsFeatureEtch_ReturnsFalseForRapidsOnly() { - var codes = new List - { - new RapidMove(0, 0) - }; + var codes = new List { new RapidMove(0, 0) }; Assert.False(FeatureUtils.IsEtch(codes)); } @@ -303,11 +300,11 @@ public class CincinnatiSheetWriterTests var output = sb.ToString(); // Under G90, coordinates must be plate-absolute (part coords + part location) - Assert.Contains("G0 X10.5 Y5.25", output); // rapid to pierce - Assert.Contains("G1 X12.5 Y5.25", output); // (2,0) + (10.5,5.25) - Assert.Contains("G1 X12.5 Y7.25", output); // (2,2) + (10.5,5.25) - Assert.Contains("G1 X10.5 Y7.25", output); // (0,2) + (10.5,5.25) - Assert.Contains("G1 X10.5 Y5.25", output); // (0,0) + (10.5,5.25) + Assert.Contains("G0 X10.5 Y5.25", output); // rapid to pierce + Assert.Contains("G1 X12.5 Y5.25", output); // (2,0) + (10.5,5.25) + Assert.Contains("G1 X12.5 Y7.25", output); // (2,2) + (10.5,5.25) + Assert.Contains("G1 X10.5 Y7.25", output); // (0,2) + (10.5,5.25) + Assert.Contains("G1 X10.5 Y5.25", output); // (0,0) + (10.5,5.25) } [Fact] diff --git a/OpenNest.Tests/Cincinnati/MaterialLibraryResolverTests.cs b/OpenNest.Tests/Cincinnati/MaterialLibraryResolverTests.cs index 1a9732c..6f5cf6d 100644 --- a/OpenNest.Tests/Cincinnati/MaterialLibraryResolverTests.cs +++ b/OpenNest.Tests/Cincinnati/MaterialLibraryResolverTests.cs @@ -4,24 +4,49 @@ namespace OpenNest.Tests.Cincinnati; public class MaterialLibraryResolverTests { - private static CincinnatiPostConfig ConfigWithLibraries() => new() - { - DefaultAssistGas = "O2", - DefaultEtchGas = "N2", - MaterialLibraries = new() + private static CincinnatiPostConfig ConfigWithLibraries() => + new() { - new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.250, Gas = "O2", Library = "MS250O2.lib" }, - new MaterialLibraryEntry { Material = "Mild Steel", Thickness = 0.250, Gas = "N2", Library = "MS250N2.lib" }, - new MaterialLibraryEntry { Material = "Aluminum", Thickness = 0.125, Gas = "N2", Library = "AL125N2.lib" }, - new MaterialLibraryEntry { Material = "Stainless Steel", Thickness = 0.375, Gas = "AIR", Library = "SS375AIR.lib" } - }, - EtchLibraries = new() - { - new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" }, - new EtchLibraryEntry { Gas = "O2", Library = "EtchO2.lib" }, - new EtchLibraryEntry { Gas = "AIR", Library = "EtchAIR.lib" } - } - }; + DefaultAssistGas = "O2", + DefaultEtchGas = "N2", + MaterialLibraries = new() + { + new MaterialLibraryEntry + { + Material = "Mild Steel", + Thickness = 0.250, + Gas = "O2", + Library = "MS250O2.lib", + }, + new MaterialLibraryEntry + { + Material = "Mild Steel", + Thickness = 0.250, + Gas = "N2", + Library = "MS250N2.lib", + }, + new MaterialLibraryEntry + { + Material = "Aluminum", + Thickness = 0.125, + Gas = "N2", + Library = "AL125N2.lib", + }, + new MaterialLibraryEntry + { + Material = "Stainless Steel", + Thickness = 0.375, + Gas = "AIR", + Library = "SS375AIR.lib", + }, + }, + EtchLibraries = new() + { + new EtchLibraryEntry { Gas = "N2", Library = "EtchN2.lib" }, + new EtchLibraryEntry { Gas = "O2", Library = "EtchO2.lib" }, + new EtchLibraryEntry { Gas = "AIR", Library = "EtchAIR.lib" }, + }, + }; [Fact] public void ResolveCutLibrary_ExactMatch() diff --git a/OpenNest.Tests/Cincinnati/SpeedClassifierTests.cs b/OpenNest.Tests/Cincinnati/SpeedClassifierTests.cs index 2956ce5..8975d98 100644 --- a/OpenNest.Tests/Cincinnati/SpeedClassifierTests.cs +++ b/OpenNest.Tests/Cincinnati/SpeedClassifierTests.cs @@ -9,7 +9,11 @@ public class SpeedClassifierTests [InlineData(5.0, 10.0, "FAST")] [InlineData(4.9, 10.0, "MEDIUM")] [InlineData(0.5, 10.0, "SLOW")] - public void Classify_ReturnsExpectedClass(double contourLength, double sheetDiagonal, string expected) + public void Classify_ReturnsExpectedClass( + double contourLength, + double sheetDiagonal, + string expected + ) { var classifier = new SpeedClassifier(); Assert.Equal(expected, classifier.Classify(contourLength, sheetDiagonal)); @@ -19,7 +23,11 @@ public class SpeedClassifierTests [InlineData(0.8702, 3.927, "CutDist=.8702/3.927")] [InlineData(18.9722, 3.927, "CutDist=18.9722/3.927")] [InlineData(0.0, 10.0, "CutDist=0/10")] - public void FormatCutDist_IncludesLengthAndDiagonal(double contour, double diag, string expected) + public void FormatCutDist_IncludesLengthAndDiagonal( + double contour, + double diag, + string expected + ) { var classifier = new SpeedClassifier(); Assert.Equal(expected, classifier.FormatCutDist(contour, diag)); diff --git a/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs b/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs index 5c5db2e..a9454b8 100644 --- a/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs +++ b/OpenNest.Tests/Cincinnati/UserVariablePostTests.cs @@ -67,7 +67,8 @@ public class UserVariablePostTests var output = PostToString(post, nest); // Both should use the same #200 — only one declaration - var declarationCount = output.Split('\n') + var declarationCount = output + .Split('\n') .Count(l => l.Contains("#200=") && l.ToUpper().Contains("SHEET WIDTH")); Assert.Equal(1, declarationCount); } @@ -112,7 +113,11 @@ public class UserVariablePostTests public void CutOff_VerticalCut_UsesSheetWidthVariable() { // Create a plate with a vertical cutoff - var config = new CincinnatiPostConfig { SheetWidthVariable = 110, SheetLengthVariable = 111 }; + var config = new CincinnatiPostConfig + { + SheetWidthVariable = 110, + SheetLengthVariable = 111, + }; var nest = new Nest { Name = "Test" }; var plate = new Plate(new Size(48, 96)); @@ -158,7 +163,7 @@ public class UserVariablePostTests partPgm.Codes.Add(new LinearMove(0, 0)); var drawing = new Drawing("Part1", partPgm); nest.Drawings.Add(drawing); - plate.Parts.Add(new Part(drawing, new Vector(15, 20))); // Part at Y=20-30, should create gap + plate.Parts.Add(new Part(drawing, new Vector(15, 20))); // Part at Y=20-30, should create gap var cutoff = new CutOff(new Vector(20, 0), CutOffAxis.Vertical); plate.CutOffs.Add(cutoff); diff --git a/OpenNest.Tests/Converters/SubProgramExpansionTests.cs b/OpenNest.Tests/Converters/SubProgramExpansionTests.cs index 2acc760..0974a16 100644 --- a/OpenNest.Tests/Converters/SubProgramExpansionTests.cs +++ b/OpenNest.Tests/Converters/SubProgramExpansionTests.cs @@ -16,7 +16,14 @@ public class SubProgramExpansionTests // Main program: call sub at offset (10,20) var main = new Program(Mode.Absolute); main.SubPrograms[1] = sub; - main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 20) }); + main.Codes.Add( + new SubProgramCall + { + Id = 1, + Program = sub, + Offset = new Vector(10, 20), + } + ); var geometry = ConvertProgram.ToGeometry(main); @@ -38,8 +45,22 @@ public class SubProgramExpansionTests var main = new Program(Mode.Absolute); main.SubPrograms[1] = sub; - main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(0, 0) }); - main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(5, 5) }); + main.Codes.Add( + new SubProgramCall + { + Id = 1, + Program = sub, + Offset = new Vector(0, 0), + } + ); + main.Codes.Add( + new SubProgramCall + { + Id = 1, + Program = sub, + Offset = new Vector(5, 5), + } + ); var geometry = ConvertProgram.ToGeometry(main); var lines = geometry.OfType().ToList(); diff --git a/OpenNest.Tests/CutOffs/CutOffGeometryTests.cs b/OpenNest.Tests/CutOffs/CutOffGeometryTests.cs index 3020864..75c6410 100644 --- a/OpenNest.Tests/CutOffs/CutOffGeometryTests.cs +++ b/OpenNest.Tests/CutOffs/CutOffGeometryTests.cs @@ -14,12 +14,12 @@ public class CutOffGeometryTests var total = 0.0; for (var i = 0; i < program.Codes.Count - 1; i += 2) { - if (program.Codes[i] is RapidMove rapid && - program.Codes[i + 1] is LinearMove linear) + if (program.Codes[i] is RapidMove rapid && program.Codes[i + 1] is LinearMove linear) { - total += axis == CutOffAxis.Vertical - ? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y) - : System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X); + total += + axis == CutOffAxis.Vertical + ? System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y) + : System.Math.Abs(rapid.EndPoint.X - linear.EndPoint.X); } } return total; @@ -113,7 +113,10 @@ public class CutOffGeometryTests // cover more of the plate than with BB. // Total cut length should be greater than 80 (BB would give 100-20=80) var totalCutLength = TotalCutLength(cutoff.Drawing.Program); - Assert.True(totalCutLength > 80, $"Geometry should give more cut length than BB. Got {totalCutLength:F2}"); + Assert.True( + totalCutLength > 80, + $"Geometry should give more cut length than BB. Got {totalCutLength:F2}" + ); } [Fact] @@ -136,7 +139,10 @@ public class CutOffGeometryTests // BB would exclude full 20 → cut length = 80. // Geometry excludes only 10 → cut length = 90. var totalCutLength = TotalCutLength(cutoff.Drawing.Program); - Assert.True(totalCutLength > 85, $"Diamond geometry should give more cut than BB. Got {totalCutLength:F2}"); + Assert.True( + totalCutLength > 85, + $"Diamond geometry should give more cut than BB. Got {totalCutLength:F2}" + ); } [Fact] @@ -159,7 +165,10 @@ public class CutOffGeometryTests // BB would exclude [10,40] = 30 → cut = 70. // Geometry excludes [10,30] = 20 → cut = 80. var totalCutLength = TotalCutLength(cutoff.Drawing.Program); - Assert.True(totalCutLength > 75, $"Triangle geometry should give more cut than BB. Got {totalCutLength:F2}"); + Assert.True( + totalCutLength > 75, + $"Triangle geometry should give more cut than BB. Got {totalCutLength:F2}" + ); } [Fact] @@ -197,7 +206,10 @@ public class CutOffGeometryTests // BB would exclude X=[0,20] → cut = 80. // Circle chord at Y=2 is much shorter → cut > 80. var totalCutLength = TotalCutLength(cutoff.Drawing.Program, CutOffAxis.Horizontal); - Assert.True(totalCutLength > 80, $"Circle horizontal cut should use geometry. Got {totalCutLength:F2}"); + Assert.True( + totalCutLength > 80, + $"Circle horizontal cut should use geometry. Got {totalCutLength:F2}" + ); } [Fact] @@ -283,7 +295,7 @@ public class CutOffGeometryTests var entities = new List { new Line(new Vector(0, 0), new Vector(10, 0)), - new Arc(new Vector(5, 5), 5, 0, System.Math.PI) + new Arc(new Vector(5, 5), 5, 0, System.Math.PI), }; var points = entities.CollectPoints(); @@ -333,7 +345,10 @@ public class CutOffGeometryTests var cutPart = plate.Parts.First(p => p.BaseDrawing.IsCutOff); // BB would give 80 (100 - 20). Geometry should give more. var totalCutLength = TotalCutLength(cutPart.BaseDrawing.Program); - Assert.True(totalCutLength > 80, $"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}"); + Assert.True( + totalCutLength > 80, + $"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}" + ); } [Fact] @@ -357,7 +372,7 @@ public class CutOffGeometryTests // Combine all entities (simulating what ShapeBuilder.GetShapes would produce) var entities = new List(); - entities.AddRange(inner.Entities); // inner first — worst case for old heuristic + entities.AddRange(inner.Entities); // inner first — worst case for old heuristic entities.AddRange(outer.Entities); var profile = new ShapeProfile(entities); diff --git a/OpenNest.Tests/CutOffs/CutOffSerializationTests.cs b/OpenNest.Tests/CutOffs/CutOffSerializationTests.cs index 2b33b60..96898ce 100644 --- a/OpenNest.Tests/CutOffs/CutOffSerializationTests.cs +++ b/OpenNest.Tests/CutOffs/CutOffSerializationTests.cs @@ -97,8 +97,12 @@ public class CutOffSerializationTests var plate = new Plate(100, 50); plate.Parts.Add(new Part(drawing)); - plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 }); - plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 }); + plate.CutOffs.Add( + new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 } + ); + plate.CutOffs.Add( + new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 } + ); plate.RegenerateCutOffs(new CutOffSettings()); nest.Plates.Add(plate); diff --git a/OpenNest.Tests/CutOffs/CutOffTests.cs b/OpenNest.Tests/CutOffs/CutOffTests.cs index 836c30a..35f8f9a 100644 --- a/OpenNest.Tests/CutOffs/CutOffTests.cs +++ b/OpenNest.Tests/CutOffs/CutOffTests.cs @@ -165,10 +165,7 @@ public class CutOffTests { var plate = new Plate(100, 50); var settings = new CutOffSettings(); - var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical) - { - StartLimit = 20.0 - }; + var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical) { StartLimit = 20.0 }; cutoff.Regenerate(plate, settings); // AwayFromOrigin: RapidMove to near end (StartLimit=20), LinearMove to far end (100). @@ -182,10 +179,7 @@ public class CutOffTests { var plate = new Plate(100, 50); var settings = new CutOffSettings(); - var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical) - { - EndLimit = 80.0 - }; + var cutoff = new CutOff(new Vector(25, 10), CutOffAxis.Vertical) { EndLimit = 80.0 }; cutoff.Regenerate(plate, settings); // AwayFromOrigin: RapidMove to near end (0), LinearMove to far end (EndLimit=80). @@ -200,16 +194,10 @@ public class CutOffTests var plate = new Plate(60, 120); var settings = new CutOffSettings { PartClearance = 0 }; - var hCut = new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) - { - EndLimit = 85.0 - }; + var hCut = new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 }; hCut.Regenerate(plate, settings); - var vCut = new CutOff(new Vector(85, 30), CutOffAxis.Vertical) - { - StartLimit = 30.0 - }; + var vCut = new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 }; vCut.Regenerate(plate, settings); Assert.True(hCut.Drawing.Program.Codes.Count > 0); diff --git a/OpenNest.Tests/CuttingStrategy/ApplySingleTests.cs b/OpenNest.Tests/CuttingStrategy/ApplySingleTests.cs index 7886660..9d3d8e8 100644 --- a/OpenNest.Tests/CuttingStrategy/ApplySingleTests.cs +++ b/OpenNest.Tests/CuttingStrategy/ApplySingleTests.cs @@ -51,15 +51,17 @@ public class ApplySingleTests { Parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } - } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, + }, }; var clickPoint = new Vector(5, 0); var entity = new Line(new Vector(10, 0), new Vector(0, 0)); var result = strategy.ApplySingle(pgm, clickPoint, entity, ContourType.External); - var hasLeadin = result.Program.Codes.OfType().Any(m => m.Layer == LayerType.Leadin); + var hasLeadin = result + .Program.Codes.OfType() + .Any(m => m.Layer == LayerType.Leadin); Assert.True(hasLeadin); } @@ -71,8 +73,8 @@ public class ApplySingleTests { Parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } - } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, + }, }; var clickPoint = new Vector(5, 0); @@ -82,7 +84,8 @@ public class ApplySingleTests // Convert back to absolute to check positions result.Program.Mode = Mode.Absolute; - var firstLinear = result.Program.Codes.OfType() + var firstLinear = result + .Program.Codes.OfType() .First(m => m.Layer == LayerType.Leadin); Assert.Equal(clickPoint.X, firstLinear.EndPoint.X, 4); Assert.Equal(clickPoint.Y, firstLinear.EndPoint.Y, 4); @@ -96,8 +99,8 @@ public class ApplySingleTests { Parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } - } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, + }, }; var clickPoint = new Vector(5, 0); @@ -116,8 +119,8 @@ public class ApplySingleTests Parameters = new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 } - } + InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }, + }, }; var clickPoint = new Vector(10, 0); diff --git a/OpenNest.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs b/OpenNest.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs index f6362f8..780b2ba 100644 --- a/OpenNest.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs +++ b/OpenNest.Tests/CuttingStrategy/CuttingParametersSerializerTests.cs @@ -12,7 +12,7 @@ public class CuttingParametersSerializerTests { AutoTabMinSize = 0.5, AutoTabMaxSize = 3.0, - ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }, }; var json = CuttingParametersSerializer.Serialize(original); @@ -25,7 +25,8 @@ public class CuttingParametersSerializerTests [Fact] public void Deserialize_MissingAutoTabFields_DefaultsToZero() { - var json = "{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625}"; + var json = + "{\"externalLeadIn\":{\"type\":\"None\"},\"externalLeadOut\":{\"type\":\"None\"},\"internalLeadIn\":{\"type\":\"None\"},\"internalLeadOut\":{\"type\":\"None\"},\"arcCircleLeadIn\":{\"type\":\"None\"},\"arcCircleLeadOut\":{\"type\":\"None\"},\"tabsEnabled\":false,\"tabWidth\":0.25,\"pierceClearance\":0.0625}"; var restored = CuttingParametersSerializer.Deserialize(json); diff --git a/OpenNest.Tests/CuttingStrategy/HoleSubProgramTests.cs b/OpenNest.Tests/CuttingStrategy/HoleSubProgramTests.cs index abeefb7..2036619 100644 --- a/OpenNest.Tests/CuttingStrategy/HoleSubProgramTests.cs +++ b/OpenNest.Tests/CuttingStrategy/HoleSubProgramTests.cs @@ -1,8 +1,8 @@ +using System.Linq; using OpenNest.CNC; using OpenNest.CNC.CuttingStrategy; using OpenNest.Converters; using OpenNest.Geometry; -using System.Linq; namespace OpenNest.Tests.CuttingStrategy; @@ -47,7 +47,12 @@ public class HoleSubProgramTests [Fact] public void SubProgramCall_ToString_IncludesOffsetAndRotation() { - var call = new SubProgramCall { Id = 1000, Offset = new Vector(1.5, 2.5), Rotation = 30 }; + var call = new SubProgramCall + { + Id = 1000, + Offset = new Vector(1.5, 2.5), + Rotation = 30, + }; var str = call.ToString(); Assert.Contains("P1000", str); Assert.Contains("X1.5", str); @@ -119,8 +124,8 @@ public class HoleSubProgramTests Parameters = new CuttingParameters { ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }, - ArcCircleLeadOut = new NoLeadOut() - } + ArcCircleLeadOut = new NoLeadOut(), + }, }; var result = strategy.Apply(pgm, new Vector(10, 10)); @@ -163,8 +168,8 @@ public class HoleSubProgramTests RoundLeadInAngles = true, LeadInAngleIncrement = 5.0, ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }, - ArcCircleLeadOut = new NoLeadOut() - } + ArcCircleLeadOut = new NoLeadOut(), + }, }; var result = strategy.Apply(pgm, new Vector(10, 10)); @@ -196,22 +201,30 @@ public class HoleSubProgramTests pgm.Codes.Add(new LinearMove(0, 0)); // Hole 1 at (3, 3) pgm.Codes.Add(new RapidMove(holeCenter1.X + holeRadius, holeCenter1.Y)); - pgm.Codes.Add(new ArcMove( - new Vector(holeCenter1.X + holeRadius, holeCenter1.Y), - holeCenter1, RotationType.CW)); + pgm.Codes.Add( + new ArcMove( + new Vector(holeCenter1.X + holeRadius, holeCenter1.Y), + holeCenter1, + RotationType.CW + ) + ); // Hole 2 at (7, 5) pgm.Codes.Add(new RapidMove(holeCenter2.X + holeRadius, holeCenter2.Y)); - pgm.Codes.Add(new ArcMove( - new Vector(holeCenter2.X + holeRadius, holeCenter2.Y), - holeCenter2, RotationType.CW)); + pgm.Codes.Add( + new ArcMove( + new Vector(holeCenter2.X + holeRadius, holeCenter2.Y), + holeCenter2, + RotationType.CW + ) + ); var strategy = new ContourCuttingStrategy { Parameters = new CuttingParameters { ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }, - ArcCircleLeadOut = new NoLeadOut() - } + ArcCircleLeadOut = new NoLeadOut(), + }, }; var result = strategy.Apply(pgm, new Vector(10, 10)); @@ -247,13 +260,21 @@ public class HoleSubProgramTests pgm.Codes.Add(new LinearMove(0, 10)); pgm.Codes.Add(new LinearMove(0, 0)); pgm.Codes.Add(new RapidMove(holeCenter1.X + holeRadius, holeCenter1.Y)); - pgm.Codes.Add(new ArcMove( - new Vector(holeCenter1.X + holeRadius, holeCenter1.Y), - holeCenter1, RotationType.CW)); + pgm.Codes.Add( + new ArcMove( + new Vector(holeCenter1.X + holeRadius, holeCenter1.Y), + holeCenter1, + RotationType.CW + ) + ); pgm.Codes.Add(new RapidMove(holeCenter2.X + holeRadius, holeCenter2.Y)); - pgm.Codes.Add(new ArcMove( - new Vector(holeCenter2.X + holeRadius, holeCenter2.Y), - holeCenter2, RotationType.CW)); + pgm.Codes.Add( + new ArcMove( + new Vector(holeCenter2.X + holeRadius, holeCenter2.Y), + holeCenter2, + RotationType.CW + ) + ); var drawing = new Drawing("TestPart") { Program = pgm }; var part = new Part(drawing); @@ -265,7 +286,7 @@ public class HoleSubProgramTests ArcCircleLeadIn = new LineLeadIn { Length = 0.125, ApproachAngle = 90 }, ArcCircleLeadOut = new NoLeadOut(), ExternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }, - ExternalLeadOut = new NoLeadOut() + ExternalLeadOut = new NoLeadOut(), }; part.ApplyLeadIns(parameters, new Vector(10, 10)); @@ -289,14 +310,22 @@ public class HoleSubProgramTests // by the last hole's position. foreach (var line in lines) { - Assert.True(line.StartPoint.X >= -1 && line.StartPoint.X <= 11, - $"Perimeter line start X={line.StartPoint.X} is outside the 10x10 part bounds"); - Assert.True(line.StartPoint.Y >= -1 && line.StartPoint.Y <= 11, - $"Perimeter line start Y={line.StartPoint.Y} is outside the 10x10 part bounds"); - Assert.True(line.EndPoint.X >= -1 && line.EndPoint.X <= 11, - $"Perimeter line end X={line.EndPoint.X} is outside the 10x10 part bounds"); - Assert.True(line.EndPoint.Y >= -1 && line.EndPoint.Y <= 11, - $"Perimeter line end Y={line.EndPoint.Y} is outside the 10x10 part bounds"); + Assert.True( + line.StartPoint.X >= -1 && line.StartPoint.X <= 11, + $"Perimeter line start X={line.StartPoint.X} is outside the 10x10 part bounds" + ); + Assert.True( + line.StartPoint.Y >= -1 && line.StartPoint.Y <= 11, + $"Perimeter line start Y={line.StartPoint.Y} is outside the 10x10 part bounds" + ); + Assert.True( + line.EndPoint.X >= -1 && line.EndPoint.X <= 11, + $"Perimeter line end X={line.EndPoint.X} is outside the 10x10 part bounds" + ); + Assert.True( + line.EndPoint.Y >= -1 && line.EndPoint.Y <= 11, + $"Perimeter line end Y={line.EndPoint.Y} is outside the 10x10 part bounds" + ); } } @@ -308,7 +337,14 @@ public class HoleSubProgramTests var main = new Program(Mode.Absolute); main.SubPrograms[1] = sub; - main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 20) }); + main.Codes.Add( + new SubProgramCall + { + Id = 1, + Program = sub, + Offset = new Vector(10, 20), + } + ); var box = main.BoundingBox(); @@ -325,7 +361,14 @@ public class HoleSubProgramTests var main = new Program(Mode.Absolute); main.SubPrograms[1] = sub; - main.Codes.Add(new SubProgramCall { Id = 1, Program = sub, Offset = new Vector(10, 0) }); + main.Codes.Add( + new SubProgramCall + { + Id = 1, + Program = sub, + Offset = new Vector(10, 0), + } + ); // Rotate 90 degrees CCW around origin main.Rotate(System.Math.PI / 2); diff --git a/OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs b/OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs index 5ddf475..8794118 100644 --- a/OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs +++ b/OpenNest.Tests/CuttingStrategy/LeadInAssignerTests.cs @@ -28,7 +28,7 @@ public class LeadInAssignerTests plate.Parts.Add(MakeSquarePartAt(30, 30)); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -50,7 +50,7 @@ public class LeadInAssignerTests plate.Parts.Add(MakeSquarePartAt(30, 30)); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -66,7 +66,7 @@ public class LeadInAssignerTests plate.Parts.Add(MakeSquarePartAt(10, 10)); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -86,13 +86,16 @@ public class LeadInAssignerTests plate.Parts.Add(MakeSquarePartAt(10, 10)); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; assigner.Assign(plate); - var hasLeadin = plate.Parts[0].Program.Codes.OfType().Any(m => m.Layer == LayerType.Leadin); + var hasLeadin = plate + .Parts[0] + .Program.Codes.OfType() + .Any(m => m.Layer == LayerType.Leadin); Assert.True(hasLeadin); } @@ -108,7 +111,7 @@ public class LeadInAssignerTests plate.Parts.Add(part); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -129,7 +132,7 @@ public class LeadInAssignerTests plate.Parts.Add(part); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -152,7 +155,7 @@ public class LeadInAssignerTests plate.Parts.Add(part); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -175,7 +178,7 @@ public class LeadInAssignerTests plate.Parts.Add(part); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -204,7 +207,7 @@ public class LeadInAssignerTests plate.Parts.Add(part); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -213,10 +216,13 @@ public class LeadInAssignerTests // The lead-in program should produce geometry that contains the // original rotated shape (plus lead-in/out extensions) var leadInGeometry = OpenNest.Converters.ConvertProgram.ToGeometry(part.Program); - var leadInNonRapid = leadInGeometry.Where(e => - e.Layer != SpecialLayers.Rapid && - e.Layer != SpecialLayers.Leadin && - e.Layer != SpecialLayers.Leadout).ToList(); + var leadInNonRapid = leadInGeometry + .Where(e => + e.Layer != SpecialLayers.Rapid + && e.Layer != SpecialLayers.Leadin + && e.Layer != SpecialLayers.Leadout + ) + .ToList(); // The bounding box of the cut geometry should be close to original var origBbox = GetEntityBounds(originalNonRapid); @@ -242,7 +248,7 @@ public class LeadInAssignerTests plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -264,7 +270,7 @@ public class LeadInAssignerTests plate.Parts.Add(part); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -291,7 +297,7 @@ public class LeadInAssignerTests plate.Parts.Add(part); plate.CuttingParameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; @@ -329,8 +335,10 @@ public class LeadInAssignerTests private static Box GetEntityBounds(List entities) { - double minX = double.MaxValue, minY = double.MaxValue; - double maxX = double.MinValue, maxY = double.MinValue; + double minX = double.MaxValue, + minY = double.MaxValue; + double maxX = double.MinValue, + maxY = double.MinValue; foreach (var entity in entities) { @@ -344,11 +352,21 @@ public class LeadInAssignerTests return new Box(minX, minY, maxX - minX, maxY - minY); } - private static void UpdateBounds(Vector pt, ref double minX, ref double minY, ref double maxX, ref double maxY) + private static void UpdateBounds( + Vector pt, + ref double minX, + ref double minY, + ref double maxX, + ref double maxY + ) { - if (pt.X < minX) minX = pt.X; - if (pt.Y < minY) minY = pt.Y; - if (pt.X > maxX) maxX = pt.X; - if (pt.Y > maxY) maxY = pt.Y; + if (pt.X < minX) + minX = pt.X; + if (pt.Y < minY) + minY = pt.Y; + if (pt.X > maxX) + maxX = pt.X; + if (pt.Y > maxY) + maxY = pt.Y; } } diff --git a/OpenNest.Tests/CuttingStrategy/LeadInLayerTagTests.cs b/OpenNest.Tests/CuttingStrategy/LeadInLayerTagTests.cs index 6e433b4..c4814ca 100644 --- a/OpenNest.Tests/CuttingStrategy/LeadInLayerTagTests.cs +++ b/OpenNest.Tests/CuttingStrategy/LeadInLayerTagTests.cs @@ -30,7 +30,12 @@ public class LeadInLayerTagTests [Fact] public void LineArcLeadIn_SetsLeadinLayerOnAllMoves() { - var leadIn = new LineArcLeadIn { LineLength = 0.5, ArcRadius = 0.25, ApproachAngle = 135 }; + var leadIn = new LineArcLeadIn + { + LineLength = 0.5, + ArcRadius = 0.25, + ApproachAngle = 135, + }; var codes = leadIn.Generate(Point, Normal); Assert.All(codes.OfType(), m => Assert.Equal(LayerType.Leadin, m.Layer)); Assert.All(codes.OfType(), m => Assert.Equal(LayerType.Leadin, m.Layer)); @@ -39,7 +44,12 @@ public class LeadInLayerTagTests [Fact] public void CleanHoleLeadIn_SetsLeadinLayerOnAllMoves() { - var leadIn = new CleanHoleLeadIn { LineLength = 0.5, ArcRadius = 0.25, Kerf = 0.05 }; + var leadIn = new CleanHoleLeadIn + { + LineLength = 0.5, + ArcRadius = 0.25, + Kerf = 0.05, + }; var codes = leadIn.Generate(Point, Normal); Assert.All(codes.OfType(), m => Assert.Equal(LayerType.Leadin, m.Layer)); Assert.All(codes.OfType(), m => Assert.Equal(LayerType.Leadin, m.Layer)); @@ -48,7 +58,13 @@ public class LeadInLayerTagTests [Fact] public void LineLineLeadIn_SetsLeadinLayerOnAllMoves() { - var leadIn = new LineLineLeadIn { Length1 = 0.5, Length2 = 0.3, ApproachAngle1 = 90, ApproachAngle2 = 90 }; + var leadIn = new LineLineLeadIn + { + Length1 = 0.5, + Length2 = 0.3, + ApproachAngle1 = 90, + ApproachAngle2 = 90, + }; var codes = leadIn.Generate(Point, Normal); Assert.All(codes.OfType(), m => Assert.Equal(LayerType.Leadin, m.Layer)); } diff --git a/OpenNest.Tests/CuttingStrategy/PartLeadInTests.cs b/OpenNest.Tests/CuttingStrategy/PartLeadInTests.cs index a64b808..c4beec6 100644 --- a/OpenNest.Tests/CuttingStrategy/PartLeadInTests.cs +++ b/OpenNest.Tests/CuttingStrategy/PartLeadInTests.cs @@ -25,7 +25,7 @@ public class PartLeadInTests var parameters = new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 } + InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }, }; part.ApplyLeadIns(parameters, new Vector(-5, -5)); @@ -40,7 +40,7 @@ public class PartLeadInTests var parameters = new CuttingParameters { ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, - InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 } + InternalLeadIn = new LineLeadIn { Length = 0.25, ApproachAngle = 90 }, }; part.ApplyLeadIns(parameters, new Vector(-5, -5)); @@ -54,12 +54,14 @@ public class PartLeadInTests var part = MakeSquarePart(); var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; part.ApplyLeadIns(parameters, new Vector(-5, -5)); - var hasLeadin = part.Program.Codes.OfType().Any(m => m.Layer == LayerType.Leadin); + var hasLeadin = part + .Program.Codes.OfType() + .Any(m => m.Layer == LayerType.Leadin); Assert.True(hasLeadin); } @@ -70,7 +72,7 @@ public class PartLeadInTests var originalCodeCount = part.Program.Codes.Count; var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; part.ApplyLeadIns(parameters, new Vector(-5, -5)); @@ -90,7 +92,7 @@ public class PartLeadInTests var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; part.ApplyLeadIns(parameters, new Vector(-5, -5)); @@ -108,7 +110,7 @@ public class PartLeadInTests var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; part.ApplyLeadIns(parameters, new Vector(-5, -5)); @@ -131,7 +133,7 @@ public class PartLeadInTests var part = MakeSquarePart(); var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var entity = new Line(new Vector(10, 0), new Vector(0, 0)); @@ -146,13 +148,15 @@ public class PartLeadInTests var part = MakeSquarePart(); var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var entity = new Line(new Vector(10, 0), new Vector(0, 0)); part.ApplySingleLeadIn(parameters, new Vector(5, 0), entity, ContourType.External); - var hasLeadin = part.Program.Codes.OfType().Any(m => m.Layer == LayerType.Leadin); + var hasLeadin = part + .Program.Codes.OfType() + .Any(m => m.Layer == LayerType.Leadin); Assert.True(hasLeadin); } @@ -163,7 +167,7 @@ public class PartLeadInTests var originalCodeCount = part.Program.Codes.Count; var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; var entity = new Line(new Vector(10, 0), new Vector(0, 0)); @@ -183,7 +187,7 @@ public class PartLeadInTests var parameters = new CuttingParameters { - ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 } + ExternalLeadIn = new LineLeadIn { Length = 0.5, ApproachAngle = 90 }, }; // After rotation, the edges change. Use a point on the rotated bottom edge. diff --git a/OpenNest.Tests/Data/LocalJsonProviderTests.cs b/OpenNest.Tests/Data/LocalJsonProviderTests.cs index 9225147..a8d46cb 100644 --- a/OpenNest.Tests/Data/LocalJsonProviderTests.cs +++ b/OpenNest.Tests/Data/LocalJsonProviderTests.cs @@ -51,20 +51,26 @@ public class LocalJsonProviderTests : IDisposable Value = 0.250, Kerf = 0.012, AssistGas = "O2", - LeadIn = new LeadConfig { Type = "Arc", Length = 0.25, Angle = 90.0, Radius = 0.125 }, + LeadIn = new LeadConfig + { + Type = "Arc", + Length = 0.25, + Angle = 90.0, + Radius = 0.125, + }, LeadOut = new LeadConfig { Type = "Line", Length = 0.125 }, CutOff = new CutOffConfig { PartClearance = 0.5, Overtravel = 0.25, Direction = "AwayFromOrigin", - MinSegmentLength = 1.0 + MinSegmentLength = 1.0, }, - PlateSizes = new List { "60x120", "48x96" } - } - } - } - } + PlateSizes = new List { "60x120", "48x96" }, + }, + }, + }, + }, }; provider.SaveMachine(machine); diff --git a/OpenNest.Tests/Data/MachineConfigTests.cs b/OpenNest.Tests/Data/MachineConfigTests.cs index 58194bd..227f1d5 100644 --- a/OpenNest.Tests/Data/MachineConfigTests.cs +++ b/OpenNest.Tests/Data/MachineConfigTests.cs @@ -28,7 +28,7 @@ public class MachineConfigTests AssistGas = "O2", LeadIn = new LeadConfig { Type = "Arc", Radius = 0.25 }, LeadOut = new LeadConfig { Type = "Line", Length = 0.125 }, - PlateSizes = new List { "60x120", "48x96" } + PlateSizes = new List { "60x120", "48x96" }, }, new() { @@ -37,9 +37,9 @@ public class MachineConfigTests AssistGas = "O2", LeadIn = new LeadConfig { Type = "Arc", Radius = 0.375 }, LeadOut = new LeadConfig { Type = "Line", Length = 0.25 }, - PlateSizes = new List { "60x120" } - } - } + PlateSizes = new List { "60x120" }, + }, + }, }, new() { @@ -52,11 +52,11 @@ public class MachineConfigTests { Value = 0.250, Kerf = 0.014, - AssistGas = "N2" - } - } - } - } + AssistGas = "N2", + }, + }, + }, + }, }; } diff --git a/OpenNest.Tests/Engine/EngineOverlapTests.cs b/OpenNest.Tests/Engine/EngineOverlapTests.cs index 0e5bdf2..c420e3f 100644 --- a/OpenNest.Tests/Engine/EngineOverlapTests.cs +++ b/OpenNest.Tests/Engine/EngineOverlapTests.cs @@ -44,7 +44,9 @@ public class EngineOverlapTests var item = new NestItem { Drawing = drawing }; var success = engine.Fill(item); - _output.WriteLine($"Engine: {engine.Name}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}"); + _output.WriteLine( + $"Engine: {engine.Name}, Parts: {plate.Parts.Count}, Utilization: {plate.Utilization():P1}" + ); if (engine is DefaultNestEngine defaultEngine) { @@ -54,8 +56,8 @@ public class EngineOverlapTests } // Show rotation distribution - var rotGroups = plate.Parts - .GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1)) + var rotGroups = plate + .Parts.GroupBy(p => System.Math.Round(OpenNest.Math.Angle.ToDegrees(p.Rotation), 1)) .OrderBy(g => g.Key); foreach (var g in rotGroups) _output.WriteLine($" Rotation {g.Key:F1}°: {g.Count()} parts"); @@ -69,20 +71,28 @@ public class EngineOverlapTests _output.WriteLine($" ({collisionPoints[i].X:F2}, {collisionPoints[i].Y:F2})"); } - Assert.False(hasOverlaps, - $"Engine '{engineName}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts"); + Assert.False( + hasOverlaps, + $"Engine '{engineName}' produced {collisionPoints.Count} collision point(s) with {plate.Parts.Count} parts" + ); } [Fact] public void AdjacentParts_ShouldNotOverlap() { - var plate = TestHelpers.MakePlate(60, 120, + var plate = TestHelpers.MakePlate( + 60, + 120, TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(10, 0, 10)); + TestHelpers.MakePartAt(10, 0, 10) + ); var hasOverlaps = plate.HasOverlappingParts(out var pts); _output.WriteLine($"Adjacent squares: overlaps={hasOverlaps}, collision count={pts.Count}"); - Assert.False(hasOverlaps, "Adjacent edge-touching parts should not be reported as overlapping"); + Assert.False( + hasOverlaps, + "Adjacent edge-touching parts should not be reported as overlapping" + ); } } diff --git a/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs b/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs index 43e8b72..e3e2fc3 100644 --- a/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs +++ b/OpenNest.Tests/Engine/EngineRefactorSmokeTests.cs @@ -22,7 +22,12 @@ public class EngineRefactorSmokeTests var engine = new DefaultNestEngine(plate); var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; - var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var parts = engine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts"); } @@ -35,7 +40,12 @@ public class EngineRefactorSmokeTests var drawing = MakeRectDrawing(20, 10); var groupParts = new List { new Part(drawing) }; - var parts = engine.Fill(groupParts, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var parts = engine.Fill( + groupParts, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); Assert.True(parts.Count > 0, "DefaultNestEngine group fill should produce parts"); } @@ -48,7 +58,12 @@ public class EngineRefactorSmokeTests engine.ForceFullAngleSweep = true; var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; - var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var parts = engine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); Assert.True(parts.Count > 0, "ForceFullAngleSweep should still produce results"); } @@ -91,7 +106,11 @@ public class EngineRefactorSmokeTests var plate = new Plate(60, 120); var drawing = MakeRectDrawing(20, 10); - var result = OpenNest.Engine.ML.BruteForceRunner.Run(drawing, plate, forceFullAngleSweep: true); + var result = OpenNest.Engine.ML.BruteForceRunner.Run( + drawing, + plate, + forceFullAngleSweep: true + ); Assert.NotNull(result); Assert.True(result.PartCount > 0); diff --git a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs index c7a3320..97db238 100644 --- a/OpenNest.Tests/Engine/MultiPlateNesterTests.cs +++ b/OpenNest.Tests/Engine/MultiPlateNesterTests.cs @@ -1,10 +1,10 @@ -using OpenNest.Geometry; -using OpenNest.IO; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using OpenNest.Geometry; +using OpenNest.IO; using Xunit; using Xunit.Abstractions; @@ -18,6 +18,7 @@ public class MultiPlateNesterTests { _output = output; } + private static Drawing MakeDrawing(string name, double width, double length) { var program = new OpenNest.CNC.Program(); @@ -33,11 +34,7 @@ public class MultiPlateNesterTests private static NestItem MakeItem(string name, double width, double length, int qty = 1) { - return new NestItem - { - Drawing = MakeDrawing(name, width, length), - Quantity = qty, - }; + return new NestItem { Drawing = MakeDrawing(name, width, length), Quantity = qty }; } [Fact] @@ -62,9 +59,9 @@ public class MultiPlateNesterTests { var items = new List { - MakeItem("short-wide", 50, 20), // longest = 50 - MakeItem("tall-narrow", 10, 80), // longest = 80 - MakeItem("square", 30, 30), // longest = 30 + MakeItem("short-wide", 50, 20), // longest = 50 + MakeItem("tall-narrow", 10, 80), // longest = 80 + MakeItem("square", 30, 30), // longest = 30 }; var sorted = MultiPlateNester.SortItems(items, PartSortOrder.Size); @@ -153,8 +150,10 @@ public class MultiPlateNesterTests // All returned zones should have both dims < 12 foreach (var zone in scrap) { - Assert.True(zone.Width < 12.0 && zone.Length < 12.0, - $"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12"); + Assert.True( + zone.Width < 12.0 && zone.Length < 12.0, + $"Zone {zone.Width:F1}x{zone.Length:F1} is not scrap — at least one dimension >= 12" + ); } } @@ -164,7 +163,13 @@ public class MultiPlateNesterTests public void CreatePlate_UsesTemplateWhenNoOptions() { var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 }; - template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 }; + template.EdgeSpacing = new Spacing + { + Left = 1, + Right = 1, + Top = 1, + Bottom = 1, + }; var plate = MultiPlateNester.CreatePlate(template, null, null); @@ -178,13 +183,34 @@ public class MultiPlateNesterTests public void CreatePlate_PicksSmallestFittingOption() { var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 }; - template.EdgeSpacing = new Spacing { Left = 1, Right = 1, Top = 1, Bottom = 1 }; + template.EdgeSpacing = new Spacing + { + Left = 1, + Right = 1, + Top = 1, + Bottom = 1, + }; var options = new List { - new() { Width = 48, Length = 96, Cost = 100 }, - new() { Width = 60, Length = 120, Cost = 200 }, - new() { Width = 72, Length = 144, Cost = 300 }, + new() + { + Width = 48, + Length = 96, + Cost = 100, + }, + new() + { + Width = 60, + Length = 120, + Cost = 200, + }, + new() + { + Width = 72, + Length = 144, + Cost = 300, + }, }; // Part needs 50x50 work area — 48x96 (after edge spacing: 46x94) — 46 < 50, doesn't fit. @@ -200,9 +226,24 @@ public class MultiPlateNesterTests [Fact] public void EvaluateUpgrade_PrefersCheaperOption() { - var currentOption = new PlateOption { Width = 48, Length = 96, Cost = 100 }; - var upgradeOption = new PlateOption { Width = 60, Length = 120, Cost = 160 }; - var newPlateOption = new PlateOption { Width = 48, Length = 96, Cost = 100 }; + var currentOption = new PlateOption + { + Width = 48, + Length = 96, + Cost = 100, + }; + var upgradeOption = new PlateOption + { + Width = 60, + Length = 120, + Cost = 160, + }; + var newPlateOption = new PlateOption + { + Width = 48, + Length = 96, + Cost = 100, + }; // Upgrade cost = 160 - 100 = 60 // New plate cost with 50% utilization, 50% salvage: @@ -210,7 +251,12 @@ public class MultiPlateNesterTests // netNewCost = 100 - 25 = 75 // Upgrade (60) < new plate (75), so upgrade wins var decision = MultiPlateNester.EvaluateUpgradeVsNew( - currentOption, upgradeOption, newPlateOption, 0.5, 0.5); + currentOption, + upgradeOption, + newPlateOption, + 0.5, + 0.5 + ); Assert.True(decision.ShouldUpgrade); } @@ -223,19 +269,17 @@ public class MultiPlateNesterTests var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 }; template.EdgeSpacing = new Spacing(); - var items = new List - { - MakeItem("big1", 80, 40, 1), - MakeItem("big2", 70, 35, 1), - }; + var items = new List { MakeItem("big1", 80, 40, 1), MakeItem("big2", 70, 35, 1) }; var options = new MultiPlateNestOptions { Template = template }; var result = MultiPlateNester.Nest(items, options); // Each large part should be on its own plate. - Assert.True(result.Plates.Count >= 2, - $"Expected at least 2 plates, got {result.Plates.Count}"); + Assert.True( + result.Plates.Count >= 2, + $"Expected at least 2 plates, got {result.Plates.Count}" + ); } [Fact] @@ -260,8 +304,10 @@ public class MultiPlateNesterTests // Both small drawing types should share space — not each on their own plate. // With consolidation, they pack into remaining space alongside the big part. - Assert.True(result.Plates.Count <= 2, - $"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}"); + Assert.True( + result.Plates.Count <= 2, + $"Expected at most 2 plates (small parts consolidated), got {result.Plates.Count}" + ); Assert.Equal(0, result.UnplacedItems.Count); } @@ -271,17 +317,9 @@ public class MultiPlateNesterTests var template = new Plate(96, 48) { PartSpacing = 0.25, Quadrant = 1 }; template.EdgeSpacing = new Spacing(); - var items = new List - { - MakeItem("big1", 80, 40, 1), - MakeItem("big2", 70, 35, 1), - }; + var items = new List { MakeItem("big1", 80, 40, 1), MakeItem("big2", 70, 35, 1) }; - var options = new MultiPlateNestOptions - { - Template = template, - AllowPlateCreation = false, - }; + var options = new MultiPlateNestOptions { Template = template, AllowPlateCreation = false }; var result = MultiPlateNester.Nest(items, options); @@ -303,15 +341,15 @@ public class MultiPlateNesterTests // Plate WorkArea: Width=96, Length=48. Half: 48, 24. // Part 24x22: Length=24 (not > 24), Width=22 (not > 48) — not Large. // Area = 528 > 4608/9 = 512 — Medium. - var items = new List - { - MakeItem("medium", 24, 22, 1), - }; + var items = new List { MakeItem("medium", 24, 22, 1) }; var options = new MultiPlateNestOptions { Template = template }; - var result = MultiPlateNester.Nest(items, options, - existingPlates: new List { existingPlate }); + var result = MultiPlateNester.Nest( + items, + options, + existingPlates: new List { existingPlate } + ); // Part should be placed on the existing plate, not a new one. Assert.Single(result.Plates); @@ -331,34 +369,43 @@ public class MultiPlateNesterTests var nest = new NestReader(nestPath).Read(); var template = nest.PlateDefaults.CreateNew(); - _output.WriteLine($"Plate: {template.Size.Width}x{template.Size.Length}, " + - $"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})"); + _output.WriteLine( + $"Plate: {template.Size.Width}x{template.Size.Length}, " + + $"spacing={template.PartSpacing}, edge=({template.EdgeSpacing.Left},{template.EdgeSpacing.Bottom},{template.EdgeSpacing.Right},{template.EdgeSpacing.Top})" + ); var wa = template.WorkArea(); _output.WriteLine($"Work area: {wa.Width:F1}x{wa.Length:F1}"); - _output.WriteLine($"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, " + - $"Medium if area > {wa.Width * wa.Length / 9:F0}"); + _output.WriteLine( + $"Classification thresholds: Large if dim > {wa.Width / 2:F1} or {wa.Length / 2:F1}, " + + $"Medium if area > {wa.Width * wa.Length / 9:F0}" + ); _output.WriteLine("---"); var items = new List(); foreach (var d in nest.Drawings) { var qty = d.Quantity.Required > 0 ? d.Quantity.Required : d.Quantity.Remaining; - if (qty <= 0) qty = 1; + if (qty <= 0) + qty = 1; var bb = d.Program.BoundingBox(); var classification = MultiPlateNester.Classify(bb, wa); - _output.WriteLine($" {d.Name,-25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}"); + _output.WriteLine( + $" {d.Name, -25} {bb.Width:F1}x{bb.Length:F1} (area={bb.Width * bb.Length:F0}) qty={qty} class={classification}" + ); - items.Add(new NestItem - { - Drawing = d, - Quantity = qty, - StepAngle = d.Constraints.StepAngle, - RotationStart = d.Constraints.StartAngle, - RotationEnd = d.Constraints.EndAngle, - }); + items.Add( + new NestItem + { + Drawing = d, + Quantity = qty, + StepAngle = d.Constraints.StepAngle, + RotationStart = d.Constraints.StartAngle, + RotationEnd = d.Constraints.EndAngle, + } + ); } _output.WriteLine("---"); @@ -366,18 +413,65 @@ public class MultiPlateNesterTests var plateOptions = new List { - new() { Width = 48, Length = 96, Cost = 0 }, - new() { Width = 48, Length = 120, Cost = 0 }, - new() { Width = 48, Length = 144, Cost = 0 }, - new() { Width = 60, Length = 96, Cost = 0 }, - new() { Width = 60, Length = 120, Cost = 0 }, - new() { Width = 60, Length = 144, Cost = 0 }, - new() { Width = 72, Length = 96, Cost = 0 }, - new() { Width = 72, Length = 120, Cost = 0 }, - new() { Width = 72, Length = 144, Cost = 0 }, + new() + { + Width = 48, + Length = 96, + Cost = 0, + }, + new() + { + Width = 48, + Length = 120, + Cost = 0, + }, + new() + { + Width = 48, + Length = 144, + Cost = 0, + }, + new() + { + Width = 60, + Length = 96, + Cost = 0, + }, + new() + { + Width = 60, + Length = 120, + Cost = 0, + }, + new() + { + Width = 60, + Length = 144, + Cost = 0, + }, + new() + { + Width = 72, + Length = 96, + Cost = 0, + }, + new() + { + Width = 72, + Length = 120, + Cost = 0, + }, + new() + { + Width = 72, + Length = 144, + Cost = 0, + }, }; - _output.WriteLine($"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}"); + _output.WriteLine( + $"Plate options: {string.Join(", ", plateOptions.Select(o => $"{o.Width}x{o.Length}"))}" + ); _output.WriteLine(""); var options = new MultiPlateNestOptions @@ -393,16 +487,21 @@ public class MultiPlateNesterTests for (var i = 0; i < result.Plates.Count; i++) { var pr = result.Plates[i]; - var groups = pr.Parts.GroupBy(p => p.BaseDrawing.Name) + var groups = pr + .Parts.GroupBy(p => p.BaseDrawing.Name) .Select(g => $"{g.Key} x{g.Count()}") .ToList(); - _output.WriteLine($" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): " + - $"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]"); + _output.WriteLine( + $" Plate {i + 1} ({pr.Plate.Size.Width}x{pr.Plate.Size.Length}): " + + $"{pr.Parts.Count} parts, util={pr.Plate.Utilization():P1} [{string.Join(", ", groups)}]" + ); } if (result.UnplacedItems.Count > 0) { - _output.WriteLine($" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}"); + _output.WriteLine( + $" Unplaced: {string.Join(", ", result.UnplacedItems.Select(i => $"{i.Drawing.Name} x{i.Quantity}"))}" + ); } _output.WriteLine($"\nTotal parts placed: {result.Plates.Sum(p => p.Parts.Count)}"); diff --git a/OpenNest.Tests/Engine/NestInvarianceTests.cs b/OpenNest.Tests/Engine/NestInvarianceTests.cs index ed9ac35..7ccd22b 100644 --- a/OpenNest.Tests/Engine/NestInvarianceTests.cs +++ b/OpenNest.Tests/Engine/NestInvarianceTests.cs @@ -1,9 +1,9 @@ +using System.Threading; using OpenNest.CNC; using OpenNest.Engine; using OpenNest.Engine.BestFit; using OpenNest.Geometry; using OpenNest.Math; -using System.Threading; namespace OpenNest.Tests.Engine; @@ -31,18 +31,20 @@ public class NestInvarianceTests return new Drawing("L", pgm); } - private static Plate MakePlate() => new Plate(new Size(500, 500)) - { - Quadrant = 1, - PartSpacing = 2, - }; + private static Plate MakePlate() => + new Plate(new Size(500, 500)) { Quadrant = 1, PartSpacing = 2 }; private static int RunFillCount(Drawing drawing, Plate plate) { BestFitCache.Clear(); var engine = new DefaultNestEngine(plate); var item = new NestItem { Drawing = drawing }; - var parts = engine.Fill(item, plate.WorkArea(), progress: null, token: CancellationToken.None); + var parts = engine.Fill( + item, + plate.WorkArea(), + progress: null, + token: CancellationToken.None + ); return parts?.Count ?? 0; } diff --git a/OpenNest.Tests/Engine/PartClassifierTests.cs b/OpenNest.Tests/Engine/PartClassifierTests.cs index 691ba7e..3e6d09c 100644 --- a/OpenNest.Tests/Engine/PartClassifierTests.cs +++ b/OpenNest.Tests/Engine/PartClassifierTests.cs @@ -30,15 +30,26 @@ public class PartClassifierTests var result = PartClassifier.Classify(drawing); Assert.Equal(PartType.Rectangle, result.Type); - Assert.True(result.Rectangularity >= 0.99, $"Expected rectangularity>=0.99, got {result.Rectangularity:F4}"); - Assert.True(result.PerimeterRatio >= 0.99, $"Expected perimeterRatio>=0.99, got {result.PerimeterRatio:F4}"); + Assert.True( + result.Rectangularity >= 0.99, + $"Expected rectangularity>=0.99, got {result.Rectangularity:F4}" + ); + Assert.True( + result.PerimeterRatio >= 0.99, + $"Expected perimeterRatio>=0.99, got {result.PerimeterRatio:F4}" + ); } [Fact] public void Classify_RoundedRectangle_ReturnsRectangle() { // Use the built-in shape builder so arc geometry is constructed correctly. - var shape = new RoundedRectangleShape { Length = 100, Width = 50, Radius = 5 }; + var shape = new RoundedRectangleShape + { + Length = 100, + Width = 50, + Radius = 5, + }; var drawing = shape.GetDrawing(); var result = PartClassifier.Classify(drawing); @@ -54,14 +65,14 @@ public class PartClassifierTests var pgm = new OpenNest.CNC.Program(); pgm.Codes.Add(new RapidMove(new Vector(0, 0))); // Bottom edge left section -> notch -> bottom edge right section - pgm.Codes.Add(new LinearMove(new Vector(45, 0))); // along bottom to notch start - pgm.Codes.Add(new LinearMove(new Vector(45, 2))); // up into notch - pgm.Codes.Add(new LinearMove(new Vector(50, 2))); // across notch (5 wide) - pgm.Codes.Add(new LinearMove(new Vector(50, 0))); // back down - pgm.Codes.Add(new LinearMove(new Vector(100, 0))); // remainder of bottom edge + pgm.Codes.Add(new LinearMove(new Vector(45, 0))); // along bottom to notch start + pgm.Codes.Add(new LinearMove(new Vector(45, 2))); // up into notch + pgm.Codes.Add(new LinearMove(new Vector(50, 2))); // across notch (5 wide) + pgm.Codes.Add(new LinearMove(new Vector(50, 0))); // back down + pgm.Codes.Add(new LinearMove(new Vector(100, 0))); // remainder of bottom edge pgm.Codes.Add(new LinearMove(new Vector(100, 50))); // right edge - pgm.Codes.Add(new LinearMove(new Vector(0, 50))); // top edge - pgm.Codes.Add(new LinearMove(new Vector(0, 0))); // left edge back to start + pgm.Codes.Add(new LinearMove(new Vector(0, 50))); // top edge + pgm.Codes.Add(new LinearMove(new Vector(0, 0))); // left edge back to start var drawing = new Drawing("rect-notch", pgm); var result = PartClassifier.Classify(drawing); @@ -78,8 +89,10 @@ public class PartClassifierTests var result = PartClassifier.Classify(drawing); Assert.Equal(PartType.Circle, result.Type); - Assert.True(result.Circularity >= PartClassifier.CircularityThreshold, - $"Expected circularity>={PartClassifier.CircularityThreshold}, got {result.Circularity:F4}"); + Assert.True( + result.Circularity >= PartClassifier.CircularityThreshold, + $"Expected circularity>={PartClassifier.CircularityThreshold}, got {result.Circularity:F4}" + ); } [Fact] @@ -151,8 +164,10 @@ public class PartClassifierTests var result = PartClassifier.Classify(drawing); Assert.Equal(PartType.Irregular, result.Type); - Assert.True(result.PerimeterRatio < PartClassifier.PerimeterRatioThreshold, - $"Expected perimeterRatio<{PartClassifier.PerimeterRatioThreshold}, got {result.PerimeterRatio:F4}"); + Assert.True( + result.PerimeterRatio < PartClassifier.PerimeterRatioThreshold, + $"Expected perimeterRatio<{PartClassifier.PerimeterRatioThreshold}, got {result.PerimeterRatio:F4}" + ); } [Fact] @@ -186,8 +201,10 @@ public class PartClassifierTests var result = PartClassifier.Classify(drawing); // The MBR must be tilted — primary angle should be non-zero. - Assert.True(System.Math.Abs(result.PrimaryAngle) > 0.01, - $"Expected non-zero primary angle for 30°-tilted rect, got {result.PrimaryAngle:F4} rad"); + Assert.True( + System.Math.Abs(result.PrimaryAngle) > 0.01, + $"Expected non-zero primary angle for 30°-tilted rect, got {result.PrimaryAngle:F4} rad" + ); } [Fact] diff --git a/OpenNest.Tests/Engine/PlateOptimizerTests.cs b/OpenNest.Tests/Engine/PlateOptimizerTests.cs index 1f00367..55164aa 100644 --- a/OpenNest.Tests/Engine/PlateOptimizerTests.cs +++ b/OpenNest.Tests/Engine/PlateOptimizerTests.cs @@ -20,14 +20,24 @@ public class PlateOptimizerTests { var options = new List { - new() { Width = 20, Length = 20, Cost = 100 }, - new() { Width = 40, Length = 40, Cost = 400 }, + new() + { + Width = 20, + Length = 20, + Cost = 100, + }, + new() + { + Width = 40, + Length = 40, + Cost = 400, + }, }; var templatePlate = new Plate(40, 40) { PartSpacing = 0 }; var items = new List { - new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 } + new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 }, }; var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate); @@ -42,14 +52,24 @@ public class PlateOptimizerTests { var options = new List { - new() { Width = 12, Length = 12, Cost = 50 }, - new() { Width = 24, Length = 12, Cost = 100 }, + new() + { + Width = 12, + Length = 12, + Cost = 50, + }, + new() + { + Width = 24, + Length = 12, + Cost = 100, + }, }; var templatePlate = new Plate(24, 12) { PartSpacing = 0 }; var items = new List { - new() { Drawing = MakeRectDrawing(10, 10), Quantity = 2 } + new() { Drawing = MakeRectDrawing(10, 10), Quantity = 2 }, }; var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate); @@ -68,15 +88,25 @@ public class PlateOptimizerTests // Net = 800 - 1500*(800/1600)*1.0 = 800-750 = 50 var options = new List { - new() { Width = 20, Length = 20, Cost = 400 }, - new() { Width = 40, Length = 40, Cost = 800 }, + new() + { + Width = 20, + Length = 20, + Cost = 400, + }, + new() + { + Width = 40, + Length = 40, + Cost = 800, + }, }; var templatePlate = new Plate(40, 40) { PartSpacing = 0 }; templatePlate.EdgeSpacing = new Spacing(); var items = new List { - new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 } + new() { Drawing = MakeRectDrawing(10, 10), Quantity = 1 }, }; var result = PlateOptimizer.Optimize(items, options, 1.0, templatePlate); @@ -90,14 +120,24 @@ public class PlateOptimizerTests { var options = new List { - new() { Width = 20, Length = 20, Cost = 100 }, - new() { Width = 40, Length = 40, Cost = 400 }, + new() + { + Width = 20, + Length = 20, + Cost = 100, + }, + new() + { + Width = 40, + Length = 40, + Cost = 400, + }, }; var templatePlate = new Plate(40, 40) { PartSpacing = 0 }; var items = new List { - new() { Drawing = MakeRectDrawing(30, 30), Quantity = 1 } + new() { Drawing = MakeRectDrawing(30, 30), Quantity = 1 }, }; var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate); @@ -111,13 +151,18 @@ public class PlateOptimizerTests { var options = new List { - new() { Width = 10, Length = 10, Cost = 50 }, + new() + { + Width = 10, + Length = 10, + Cost = 50, + }, }; var templatePlate = new Plate(10, 10) { PartSpacing = 0 }; var items = new List { - new() { Drawing = MakeRectDrawing(20, 20), Quantity = 1 } + new() { Drawing = MakeRectDrawing(20, 20), Quantity = 1 }, }; var result = PlateOptimizer.Optimize(items, options, 0.0, templatePlate); diff --git a/OpenNest.Tests/Engine/PlateProcessorTests.cs b/OpenNest.Tests/Engine/PlateProcessorTests.cs index 8545e49..34417b8 100644 --- a/OpenNest.Tests/Engine/PlateProcessorTests.cs +++ b/OpenNest.Tests/Engine/PlateProcessorTests.cs @@ -20,7 +20,7 @@ public class PlateProcessorTests var processor = new PlateProcessor { Sequencer = new RightSideSequencer(), - RapidPlanner = new SafeHeightRapidPlanner() + RapidPlanner = new SafeHeightRapidPlanner(), }; var result = processor.Process(plate); @@ -40,7 +40,7 @@ public class PlateProcessorTests var processor = new PlateProcessor { Sequencer = new RightSideSequencer(), - RapidPlanner = new SafeHeightRapidPlanner() + RapidPlanner = new SafeHeightRapidPlanner(), }; var result = processor.Process(plate); @@ -60,11 +60,8 @@ public class PlateProcessorTests var processor = new PlateProcessor { Sequencer = new LeftSideSequencer(), - CuttingStrategy = new ContourCuttingStrategy - { - Parameters = new CuttingParameters() - }, - RapidPlanner = new SafeHeightRapidPlanner() + CuttingStrategy = new ContourCuttingStrategy { Parameters = new CuttingParameters() }, + RapidPlanner = new SafeHeightRapidPlanner(), }; var result = processor.Process(plate); @@ -83,7 +80,7 @@ public class PlateProcessorTests var processor = new PlateProcessor { Sequencer = new LeftSideSequencer(), - RapidPlanner = new SafeHeightRapidPlanner() + RapidPlanner = new SafeHeightRapidPlanner(), }; var result = processor.Process(plate); @@ -101,7 +98,7 @@ public class PlateProcessorTests var processor = new PlateProcessor { Sequencer = new LeftSideSequencer(), - RapidPlanner = new SafeHeightRapidPlanner() + RapidPlanner = new SafeHeightRapidPlanner(), }; var result = processor.Process(plate); @@ -117,7 +114,7 @@ public class PlateProcessorTests var processor = new PlateProcessor { Sequencer = new LeftSideSequencer(), - RapidPlanner = new SafeHeightRapidPlanner() + RapidPlanner = new SafeHeightRapidPlanner(), }; var result = processor.Process(plate); diff --git a/OpenNest.Tests/Engine/RemnantEngineTests.cs b/OpenNest.Tests/Engine/RemnantEngineTests.cs index c32ffd1..4b3dd42 100644 --- a/OpenNest.Tests/Engine/RemnantEngineTests.cs +++ b/OpenNest.Tests/Engine/RemnantEngineTests.cs @@ -42,7 +42,12 @@ public class RemnantEngineTests var engine = new VerticalRemnantEngine(plate); var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; - var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var parts = engine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); Assert.True(parts.Count > 0, "VerticalRemnantEngine should fill parts"); } @@ -54,7 +59,12 @@ public class RemnantEngineTests var engine = new HorizontalRemnantEngine(plate); var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; - var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var parts = engine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); Assert.True(parts.Count > 0, "HorizontalRemnantEngine should fill parts"); } @@ -77,16 +87,30 @@ public class RemnantEngineTests var defaultEngine = new DefaultNestEngine(plate); var remnantEngine = new VerticalRemnantEngine(plate); - var defaultParts = defaultEngine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); - var remnantParts = remnantEngine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var defaultParts = defaultEngine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); + var remnantParts = remnantEngine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); Assert.True(defaultParts.Count > 0); Assert.True(remnantParts.Count > 0); - var defaultXExtent = defaultParts.Max(p => p.BoundingBox.Right) - defaultParts.Min(p => p.BoundingBox.Left); - var remnantXExtent = remnantParts.Max(p => p.BoundingBox.Right) - remnantParts.Min(p => p.BoundingBox.Left); + var defaultXExtent = + defaultParts.Max(p => p.BoundingBox.Right) - defaultParts.Min(p => p.BoundingBox.Left); + var remnantXExtent = + remnantParts.Max(p => p.BoundingBox.Right) - remnantParts.Min(p => p.BoundingBox.Left); - Assert.True(remnantXExtent <= defaultXExtent + 0.01, - $"Remnant X-extent ({remnantXExtent:F1}) should be <= default ({defaultXExtent:F1})"); + Assert.True( + remnantXExtent <= defaultXExtent + 0.01, + $"Remnant X-extent ({remnantXExtent:F1}) should be <= default ({defaultXExtent:F1})" + ); } } diff --git a/OpenNest.Tests/Fill/AccumulatingProgressTests.cs b/OpenNest.Tests/Fill/AccumulatingProgressTests.cs index 7877d4f..e2837e1 100644 --- a/OpenNest.Tests/Fill/AccumulatingProgressTests.cs +++ b/OpenNest.Tests/Fill/AccumulatingProgressTests.cs @@ -7,6 +7,7 @@ public class AccumulatingProgressTests private class CapturingProgress : IProgress { public NestProgress Last { get; private set; } + public void Report(NestProgress value) => Last = value; } diff --git a/OpenNest.Tests/Fill/AngleCandidateBuilderTests.cs b/OpenNest.Tests/Fill/AngleCandidateBuilderTests.cs index 04df498..efc1fd2 100644 --- a/OpenNest.Tests/Fill/AngleCandidateBuilderTests.cs +++ b/OpenNest.Tests/Fill/AngleCandidateBuilderTests.cs @@ -18,8 +18,10 @@ public class AngleCandidateBuilderTests return new Drawing("rect", pgm); } - private static ClassificationResult MakeClassification(double primaryAngle = 0, PartType type = PartType.Irregular) - => new ClassificationResult { PrimaryAngle = primaryAngle, Type = type }; + private static ClassificationResult MakeClassification( + double primaryAngle = 0, + PartType type = PartType.Irregular + ) => new ClassificationResult { PrimaryAngle = primaryAngle, Type = type }; [Fact] public void Build_ReturnsAtLeastTwoAngles() @@ -81,8 +83,10 @@ public class AngleCandidateBuilderTests builder.ForceFullSweep = false; var secondAngles = builder.Build(item, MakeClassification(), workArea); - Assert.True(secondAngles.Count < firstAngles.Count, - $"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})"); + Assert.True( + secondAngles.Count < firstAngles.Count, + $"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})" + ); } [Fact] @@ -128,8 +132,10 @@ public class AngleCandidateBuilderTests var angles = builder.Build(item, classification, workArea); - Assert.True(angles.Count > 2, - $"User constraints should override rect classification, got {angles.Count} angles"); + Assert.True( + angles.Count > 2, + $"User constraints should override rect classification, got {angles.Count} angles" + ); } [Fact] @@ -149,7 +155,9 @@ public class AngleCandidateBuilderTests var angles = builder.Build(item, classification, workArea); // Start=0, End=PI is NOT "no constraints" — it's a real 0-180 range - Assert.True(angles.Count > 2, - $"0-to-PI constraint should produce multiple angles, got {angles.Count}"); + Assert.True( + angles.Count > 2, + $"0-to-PI constraint should produce multiple angles, got {angles.Count}" + ); } } diff --git a/OpenNest.Tests/Fill/CompactorTests.cs b/OpenNest.Tests/Fill/CompactorTests.cs index 7fbef2a..0cc7cb7 100644 --- a/OpenNest.Tests/Fill/CompactorTests.cs +++ b/OpenNest.Tests/Fill/CompactorTests.cs @@ -1,8 +1,8 @@ +using System.Collections.Generic; using OpenNest; using OpenNest.Engine.Fill; using OpenNest.Geometry; using Xunit; -using System.Collections.Generic; namespace OpenNest.Tests.Fill { @@ -31,8 +31,7 @@ namespace OpenNest.Tests.Fill // Verify: after moving, the closest point on the arc should be within // tolerance of the line, not past it. - var theta = System.Math.Atan2( - line.pt2.X - line.pt1.X, -(line.pt2.Y - line.pt1.Y)); + var theta = System.Math.Atan2(line.pt2.X - line.pt1.X, -(line.pt2.Y - line.pt1.Y)); theta = OpenNest.Math.Angle.NormalizeRad(theta + System.Math.PI); var qx = arc.Center.X + arc.Radius * System.Math.Cos(theta); var qy = arc.Center.Y + arc.Radius * System.Math.Sin(theta) + dist; @@ -41,9 +40,11 @@ namespace OpenNest.Tests.Fill // Line equation: (y - 4) / (x - 3) = (6 - 4) / (7 - 3) = 0.5 // y = 0.5x + 2.5 var lineYAtQx = 0.5 * qx + 2.5; - Assert.True(qy <= lineYAtQx + 0.001, - $"Arc point ({qx:F4}, {qy:F4}) should not be past line (line Y={lineYAtQx:F4} at X={qx:F4}). " + - $"dist={dist:F6}, overshot by {qy - lineYAtQx:F6}"); + Assert.True( + qy <= lineYAtQx + 0.001, + $"Arc point ({qx:F4}, {qy:F4}) should not be past line (line Y={lineYAtQx:F4} at X={qx:F4}). " + + $"dist={dist:F6}, overshot by {qy - lineYAtQx:F6}" + ); } [Fact] @@ -57,17 +58,26 @@ namespace OpenNest.Tests.Fill // Phase 1/2 vertex-only distance: sample arc endpoints + cardinal extreme. var vertices = new[] { - new Vector(7, 0), // arc endpoint θ=0 - new Vector(3, 0), // arc endpoint θ=π - new Vector(5, 2), // cardinal extreme θ=π/2 + new Vector(7, 0), // arc endpoint θ=0 + new Vector(3, 0), // arc endpoint θ=π + new Vector(5, 2), // cardinal extreme θ=π/2 }; var vertexMin = double.MaxValue; foreach (var v in vertices) { - var d = SpatialQuery.RayEdgeDistance(v.X, v.Y, - line.pt1.X, line.pt1.Y, line.pt2.X, line.pt2.Y, 0, 1); - if (d < vertexMin) vertexMin = d; + var d = SpatialQuery.RayEdgeDistance( + v.X, + v.Y, + line.pt1.X, + line.pt1.Y, + line.pt2.X, + line.pt2.Y, + 0, + 1 + ); + if (d < vertexMin) + vertexMin = d; } // Full directional distance (includes Phase 3 arc-to-line). @@ -75,9 +85,12 @@ namespace OpenNest.Tests.Fill var stationary = new List { line }; var fullDist = SpatialQuery.DirectionalDistance(moving, stationary, new Vector(0, 1)); - Assert.True(fullDist < vertexMin, - $"Full distance ({fullDist:F6}) should be less than vertex-only ({vertexMin:F6})"); + Assert.True( + fullDist < vertexMin, + $"Full distance ({fullDist:F6}) should be less than vertex-only ({vertexMin:F6})" + ); } + private static Drawing MakeRectDrawing(double w, double h) { var pgm = new OpenNest.CNC.Program(); @@ -187,12 +200,24 @@ namespace OpenNest.Tests.Fill // Push without spacing. var obstacle1 = MakeRectPart(0, 0, 10, 10); var part1 = MakeRectPart(50, 0, 10, 10); - var distNoSpacing = Compactor.Push(new List { part1 }, new List { obstacle1 }, workArea, 0, PushDirection.Left); + var distNoSpacing = Compactor.Push( + new List { part1 }, + new List { obstacle1 }, + workArea, + 0, + PushDirection.Left + ); // Push with spacing. var obstacle2 = MakeRectPart(0, 0, 10, 10); var part2 = MakeRectPart(50, 0, 10, 10); - var distWithSpacing = Compactor.Push(new List { part2 }, new List { obstacle2 }, workArea, 2, PushDirection.Left); + var distWithSpacing = Compactor.Push( + new List { part2 }, + new List { obstacle2 }, + workArea, + 2, + PushDirection.Left + ); // Spacing should cause the part to stop at a different position than without spacing. Assert.NotEqual(distNoSpacing, distWithSpacing); @@ -235,11 +260,19 @@ namespace OpenNest.Tests.Fill public void Push_WithSpacing_StopsBeforeNearMissOutsideRawBounds(double degrees) { var obstacle = MakeRectPart(20, 20, 10, 10); - var moving = Part.CreateAtOrigin(MakeRectDrawing(10, 10), OpenNest.Math.Angle.ToRadians(degrees)); + var moving = Part.CreateAtOrigin( + MakeRectDrawing(10, 10), + OpenNest.Math.Angle.ToRadians(degrees) + ); moving.Offset(60, 31); - Compactor.Push(new List { moving }, new List { obstacle }, - new Box(0, 0, 100, 100), 2, PushDirection.Left); + Compactor.Push( + new List { moving }, + new List { obstacle }, + new Box(0, 0, 100, 100), + 2, + PushDirection.Left + ); // Must stop at the first clearance boundary, not pass the obstacle // and finish in a clear position on the far side. @@ -253,8 +286,13 @@ namespace OpenNest.Tests.Fill var obstacle = MakeRectPart(20, 20, 10, 10); var moving = MakeRectPart(60, 20, 10, 10); - Compactor.Push(new List { moving }, new List { obstacle }, - new Box(31, 0, 100, 100), 2, PushDirection.Left); + Compactor.Push( + new List { moving }, + new List { obstacle }, + new Box(31, 0, 100, 100), + 2, + PushDirection.Left + ); AssertClearance(moving, obstacle, 2); Assert.Equal(32, moving.BoundingBox.Left, 7); @@ -267,31 +305,39 @@ namespace OpenNest.Tests.Fill foreach (var b in PartGeometry.GetPartLines(obstacle)) { Assert.False(Intersect.Intersects(a, b, out _)); - clearance = System.Math.Min(clearance, a.StartPoint.DistanceTo(b.ClosestPointTo(a.StartPoint))); - clearance = System.Math.Min(clearance, b.StartPoint.DistanceTo(a.ClosestPointTo(b.StartPoint))); + clearance = System.Math.Min( + clearance, + a.StartPoint.DistanceTo(b.ClosestPointTo(a.StartPoint)) + ); + clearance = System.Math.Min( + clearance, + b.StartPoint.DistanceTo(a.ClosestPointTo(b.StartPoint)) + ); } - Assert.True(clearance >= spacing - 1e-7, $"Clearance {clearance:R} is less than spacing {spacing:R}"); + Assert.True( + clearance >= spacing - 1e-7, + $"Clearance {clearance:R} is less than spacing {spacing:R}" + ); } [Fact] public void Push_Up_AllowsSharedDiagonalEdgeToSeparate() { var workArea = new Box(0, 0, 20, 20); - var obstacle = MakeTrianglePart( - new Vector(0, 0), - new Vector(10, 0), - new Vector(0, 10)); + var obstacle = MakeTrianglePart(new Vector(0, 0), new Vector(10, 0), new Vector(0, 10)); var movingPart = MakeTrianglePart( new Vector(0, 10), new Vector(10, 0), - new Vector(10, 10)); + new Vector(10, 10) + ); var distance = Compactor.Push( new List { movingPart }, new List { obstacle }, workArea, 0, - PushDirection.Up); + PushDirection.Up + ); Assert.True(distance > 0); Assert.True(movingPart.BoundingBox.Top > 19.9); @@ -303,15 +349,19 @@ namespace OpenNest.Tests.Fill { var workArea = new Box(0, 0, 24, 24); var leftTriangle = MakeTrianglePart( - 2, 2, + 2, + 2, new Vector(0, 0), new Vector(8, 0), - new Vector(4, 10)); + new Vector(4, 10) + ); var rightTriangle = MakeTrianglePart( - 14, 4, + 14, + 4, new Vector(0, 10), new Vector(8, 10), - new Vector(4, 0)); + new Vector(4, 0) + ); var moving = new List { rightTriangle }; var obstacles = new List { leftTriangle }; @@ -333,21 +383,20 @@ namespace OpenNest.Tests.Fill public void Push_Left_BlocksWhenSharedDiagonalEdgeWouldOverlap() { var workArea = new Box(0, 0, 20, 20); - var obstacle = MakeTrianglePart( - new Vector(0, 0), - new Vector(10, 0), - new Vector(0, 10)); + var obstacle = MakeTrianglePart(new Vector(0, 0), new Vector(10, 0), new Vector(0, 10)); var movingPart = MakeTrianglePart( new Vector(0, 10), new Vector(10, 0), - new Vector(10, 10)); + new Vector(10, 10) + ); var distance = Compactor.Push( new List { movingPart }, new List { obstacle }, workArea, 0, - PushDirection.Left); + PushDirection.Left + ); Assert.Equal(0, distance); Assert.Equal(0, movingPart.BoundingBox.Left); @@ -362,7 +411,10 @@ namespace OpenNest.Tests.Fill var obstacles = new List(); // direction = left - var direction = new Vector(System.Math.Cos(System.Math.PI), System.Math.Sin(System.Math.PI)); + var direction = new Vector( + System.Math.Cos(System.Math.PI), + System.Math.Sin(System.Math.PI) + ); var distance = Compactor.Push(moving, obstacles, workArea, 0, direction); Assert.True(distance > 0); @@ -394,7 +446,13 @@ namespace OpenNest.Tests.Fill var moving = new List { part }; var obstacles = new List(); - var distance = Compactor.PushBoundingBox(moving, obstacles, workArea, 0, PushDirection.Left); + var distance = Compactor.PushBoundingBox( + moving, + obstacles, + workArea, + 0, + PushDirection.Left + ); Assert.True(distance > 0); Assert.True(part.BoundingBox.Left < 1); diff --git a/OpenNest.Tests/Fill/FillComparerTests.cs b/OpenNest.Tests/Fill/FillComparerTests.cs index 1fb0951..5c498aa 100644 --- a/OpenNest.Tests/Fill/FillComparerTests.cs +++ b/OpenNest.Tests/Fill/FillComparerTests.cs @@ -37,12 +37,12 @@ public class DefaultFillComparerTests { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(20, 0, 10), - TestHelpers.MakePartAt(40, 0, 10) + TestHelpers.MakePartAt(40, 0, 10), }; var current = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(20, 0, 10) + TestHelpers.MakePartAt(20, 0, 10), }; Assert.True(comparer.IsBetter(candidate, current, workArea)); } @@ -53,12 +53,12 @@ public class DefaultFillComparerTests var candidate = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(12, 0, 10) + TestHelpers.MakePartAt(12, 0, 10), }; var current = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(50, 0, 10) + TestHelpers.MakePartAt(50, 0, 10), }; Assert.True(comparer.IsBetter(candidate, current, workArea)); } @@ -76,12 +76,12 @@ public class VerticalRemnantComparerTests { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(40, 0, 10), - TestHelpers.MakePartAt(80, 0, 10) + TestHelpers.MakePartAt(80, 0, 10), }; var current = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(12, 0, 10) + TestHelpers.MakePartAt(12, 0, 10), }; Assert.True(comparer.IsBetter(candidate, current, workArea)); } @@ -92,12 +92,12 @@ public class VerticalRemnantComparerTests var candidate = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(12, 0, 10) + TestHelpers.MakePartAt(12, 0, 10), }; var current = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(50, 0, 10) + TestHelpers.MakePartAt(50, 0, 10), }; Assert.True(comparer.IsBetter(candidate, current, workArea)); } @@ -108,12 +108,12 @@ public class VerticalRemnantComparerTests var candidate = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(40, 0, 10) + TestHelpers.MakePartAt(40, 0, 10), }; var current = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(40, 40, 10) + TestHelpers.MakePartAt(40, 40, 10), }; Assert.True(comparer.IsBetter(candidate, current, workArea)); } @@ -144,12 +144,12 @@ public class HorizontalRemnantComparerTests var candidate = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(0, 12, 10) + TestHelpers.MakePartAt(0, 12, 10), }; var current = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(0, 50, 10) + TestHelpers.MakePartAt(0, 50, 10), }; Assert.True(comparer.IsBetter(candidate, current, workArea)); } @@ -161,12 +161,12 @@ public class HorizontalRemnantComparerTests { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(0, 40, 10), - TestHelpers.MakePartAt(0, 80, 10) + TestHelpers.MakePartAt(0, 80, 10), }; var current = new List { TestHelpers.MakePartAt(0, 0, 10), - TestHelpers.MakePartAt(0, 12, 10) + TestHelpers.MakePartAt(0, 12, 10), }; Assert.True(comparer.IsBetter(candidate, current, workArea)); } diff --git a/OpenNest.Tests/Fill/FillExtentsTests.cs b/OpenNest.Tests/Fill/FillExtentsTests.cs index e36e95a..7905496 100644 --- a/OpenNest.Tests/Fill/FillExtentsTests.cs +++ b/OpenNest.Tests/Fill/FillExtentsTests.cs @@ -41,10 +41,14 @@ public class FillExtentsTests foreach (var part in parts) { - Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01, - $"Part right edge {part.BoundingBox.Right} exceeds work area {workArea.Right}"); - Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01, - $"Part top edge {part.BoundingBox.Top} exceeds work area {workArea.Top}"); + Assert.True( + part.BoundingBox.Right <= workArea.Right + 0.01, + $"Part right edge {part.BoundingBox.Right} exceeds work area {workArea.Right}" + ); + Assert.True( + part.BoundingBox.Top <= workArea.Top + 0.01, + $"Part top edge {part.BoundingBox.Top} exceeds work area {workArea.Top}" + ); } } @@ -82,8 +86,10 @@ public class FillExtentsTests // After adjustment, the gap should be small (within one part spacing). var gap = workArea.Top - topEdge; - Assert.True(gap < 1.0, - $"Gap of {gap:F2} is too large — adjustment should fill close to the top"); + Assert.True( + gap < 1.0, + $"Gap of {gap:F2} is too large — adjustment should fill close to the top" + ); } [Fact] @@ -96,8 +102,10 @@ public class FillExtentsTests var parts = filler.Fill(drawing); // With a 120-wide sheet and ~10-wide parts, we should get multiple columns. - Assert.True(parts.Count >= 8, - $"Expected multiple columns but got only {parts.Count} parts"); + Assert.True( + parts.Count >= 8, + $"Expected multiple columns but got only {parts.Count} parts" + ); // Verify all parts are within bounds. foreach (var part in parts) @@ -136,10 +144,14 @@ public class FillExtentsTests foreach (var part in parts) { - Assert.True(part.BoundingBox.Left >= workArea.Left - 0.01, - $"Part left {part.BoundingBox.Left} below work area left {workArea.Left}"); - Assert.True(part.BoundingBox.Bottom >= workArea.Bottom - 0.01, - $"Part bottom {part.BoundingBox.Bottom} below work area bottom {workArea.Bottom}"); + Assert.True( + part.BoundingBox.Left >= workArea.Left - 0.01, + $"Part left {part.BoundingBox.Left} below work area left {workArea.Left}" + ); + Assert.True( + part.BoundingBox.Bottom >= workArea.Bottom - 0.01, + $"Part bottom {part.BoundingBox.Bottom} below work area bottom {workArea.Bottom}" + ); Assert.True(part.BoundingBox.Right <= workArea.Right + 0.01); Assert.True(part.BoundingBox.Top <= workArea.Top + 0.01); } diff --git a/OpenNest.Tests/Fill/FillLinearCircleTests.cs b/OpenNest.Tests/Fill/FillLinearCircleTests.cs index d3e6773..3b984c3 100644 --- a/OpenNest.Tests/Fill/FillLinearCircleTests.cs +++ b/OpenNest.Tests/Fill/FillLinearCircleTests.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Linq; using OpenNest; using OpenNest.CNC; using OpenNest.Converters; @@ -6,8 +8,6 @@ using OpenNest.Geometry; using OpenNest.Math; using Xunit; using Xunit.Abstractions; -using System.Collections.Generic; -using System.Linq; namespace OpenNest.Tests.Fill { @@ -32,19 +32,23 @@ namespace OpenNest.Tests.Fill // Outer circle (CCW) var outerStart = new Vector(outerRadius * 2, outerRadius); pgm.Codes.Add(new RapidMove(outerStart)); - pgm.Codes.Add(new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW)); + pgm.Codes.Add( + new ArcMove(outerStart, new Vector(outerRadius, outerRadius), RotationType.CCW) + ); // Inner circle (CW = hole) var innerStart = new Vector(outerRadius + innerRadius, outerRadius); pgm.Codes.Add(new RapidMove(innerStart)); - pgm.Codes.Add(new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW)); + pgm.Codes.Add( + new ArcMove(innerStart, new Vector(outerRadius, outerRadius), RotationType.CW) + ); return new Drawing("ring", pgm); } [Theory] - [InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing - [InlineData(1.0, 0.125)] // 2" diameter circle - [InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing - [InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing + [InlineData(2.0, 0.125)] // 4" diameter circle, 1/8" spacing + [InlineData(1.0, 0.125)] // 2" diameter circle + [InlineData(3.0, 0.0625)] // 6" diameter circle, 1/16" spacing + [InlineData(0.5, 0.25)] // 1" diameter circle, 1/4" spacing public void CircleFill_OffsetBoundaries_DoNotOverlap(double radius, double spacing) { var drawing = MakeCircleDrawing(radius); @@ -58,21 +62,31 @@ namespace OpenNest.Tests.Fill } [Theory] - [InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5 - [InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0 - public void RingFill_OffsetBoundaries_DoNotOverlap(double outerR, double innerR, double spacing) + [InlineData(2.0, 1.5, 0.125)] // Ring: outer R=2, inner R=1.5 + [InlineData(1.5, 1.0, 0.125)] // Ring: outer R=1.5, inner R=1.0 + public void RingFill_OffsetBoundaries_DoNotOverlap( + double outerR, + double innerR, + double spacing + ) { var drawing = MakeRingDrawing(outerR, innerR); var workArea = new Box(0, 0, 48, 48); var engine = new FillLinear(workArea, spacing); var parts = engine.Fill(drawing, 0, NestDirection.Horizontal); - _output.WriteLine($"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts"); + _output.WriteLine( + $"Ring outerR={outerR}, innerR={innerR}, spacing={spacing}: {parts.Count} parts" + ); AssertNoOffsetOverlap(parts, spacing, outerR * 2); } - private void AssertNoOffsetOverlap(List parts, double spacing, double expectedDiameter) + private void AssertNoOffsetOverlap( + List parts, + double spacing, + double expectedDiameter + ) { if (parts.Count < 2) { @@ -109,21 +123,27 @@ namespace OpenNest.Tests.Fill violationCount++; if (violationCount <= 5) { - _output.WriteLine($" SPACING VIOLATION parts[{i}] vs parts[{j}]: " + - $"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " + - $"expected>={spacing:F4}"); + _output.WriteLine( + $" SPACING VIOLATION parts[{i}] vs parts[{j}]: " + + $"centerDist={centerDist:F6}, rawGap={rawGap:F6}, offsetGap={offsetGap:F6}, " + + $"expected>={spacing:F4}" + ); } } } } - _output.WriteLine($" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}"); + _output.WriteLine( + $" Min gap={minGap:F6}, expected>={spacing:F4}, violations={violationCount}" + ); if (violationCount > 0) { var maxDeficit = spacing - minGap; _output.WriteLine($" Max deficit={maxDeficit:F6}"); - Assert.Fail($"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}"); + Assert.Fail( + $"{violationCount} pairs violate spacing: min gap={minGap:F6}, expected>={spacing}, deficit={maxDeficit:F6}" + ); } } } diff --git a/OpenNest.Tests/Fill/FillPolicyTests.cs b/OpenNest.Tests/Fill/FillPolicyTests.cs index b97f048..0eac077 100644 --- a/OpenNest.Tests/Fill/FillPolicyTests.cs +++ b/OpenNest.Tests/Fill/FillPolicyTests.cs @@ -13,12 +13,19 @@ public class FillWithDirectionPreferenceTests [Fact] public void NullPreference_TriesBothDirections_ReturnsBetter() { - var hParts = new List { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(12, 0, 10) }; + var hParts = new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(12, 0, 10), + }; var vParts = new List { TestHelpers.MakePartAt(0, 0, 10) }; var result = FillHelpers.FillWithDirectionPreference( dir => dir == NestDirection.Horizontal ? hParts : vParts, - null, comparer, workArea); + null, + comparer, + workArea + ); Assert.Equal(2, result.Count); } @@ -26,12 +33,24 @@ public class FillWithDirectionPreferenceTests [Fact] public void PreferredDirection_UsedFirst_WhenProducesResults() { - var hParts = new List { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(12, 0, 10) }; - var vParts = new List { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(0, 12, 10), TestHelpers.MakePartAt(0, 24, 10) }; + var hParts = new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(12, 0, 10), + }; + var vParts = new List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(0, 12, 10), + TestHelpers.MakePartAt(0, 24, 10), + }; var result = FillHelpers.FillWithDirectionPreference( dir => dir == NestDirection.Horizontal ? hParts : vParts, - NestDirection.Horizontal, comparer, workArea); + NestDirection.Horizontal, + comparer, + workArea + ); Assert.Equal(2, result.Count); // H has results, so H is returned (preferred) } @@ -43,7 +62,10 @@ public class FillWithDirectionPreferenceTests var result = FillHelpers.FillWithDirectionPreference( dir => dir == NestDirection.Horizontal ? new List() : vParts, - NestDirection.Horizontal, comparer, workArea); + NestDirection.Horizontal, + comparer, + workArea + ); Assert.Equal(1, result.Count); // Falls back to V } diff --git a/OpenNest.Tests/Fill/FillScoreTests.cs b/OpenNest.Tests/Fill/FillScoreTests.cs index 8b303ee..74742e0 100644 --- a/OpenNest.Tests/Fill/FillScoreTests.cs +++ b/OpenNest.Tests/Fill/FillScoreTests.cs @@ -1,5 +1,5 @@ -using OpenNest.Geometry; using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Tests.Fill; @@ -57,7 +57,10 @@ public class FillScoreTests [Fact] public void Compute_EmptyParts_ReturnsDefault() { - var score = FillScore.Compute(new System.Collections.Generic.List(), new Box(0, 0, 100, 100)); + var score = FillScore.Compute( + new System.Collections.Generic.List(), + new Box(0, 0, 100, 100) + ); Assert.Equal(0, score.Count); } @@ -69,7 +72,7 @@ public class FillScoreTests { TestHelpers.MakePartAt(0, 0, 10), TestHelpers.MakePartAt(20, 0, 10), - TestHelpers.MakePartAt(40, 0, 10) + TestHelpers.MakePartAt(40, 0, 10), }; var score = FillScore.Compute(parts, new Box(0, 0, 100, 100)); diff --git a/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs b/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs index 33fabb3..c0dead3 100644 --- a/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs +++ b/OpenNest.Tests/Fill/IterativeShrinkFillerTests.cs @@ -19,7 +19,12 @@ public class IterativeShrinkFillerTests public void Fill_EmptyItems_ReturnsEmpty() { Func> fillFunc = (ni, b) => new List(); - var result = IterativeShrinkFiller.Fill(new List(), new Box(0, 0, 100, 100), fillFunc, 1.0); + var result = IterativeShrinkFiller.Fill( + new List(), + new Box(0, 0, 100, 100), + fillFunc, + 1.0 + ); Assert.Empty(result.Parts); Assert.Empty(result.Leftovers); @@ -42,7 +47,7 @@ public class IterativeShrinkFillerTests var drawing = MakeRectDrawing(20, 10); var items = new List { - new NestItem { Drawing = drawing, Quantity = 5 } + new NestItem { Drawing = drawing, Quantity = 5 }, }; Func> fillFunc = (ni, b) => @@ -110,7 +115,7 @@ public class IterativeShrinkFillerTests { var items = new List { - new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 0 } + new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 0 }, }; Func> fillFunc = (ni, b) => @@ -134,13 +139,19 @@ public class IterativeShrinkFillerTests var items = new List { - new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 10 } + new NestItem { Drawing = MakeRectDrawing(20, 10), Quantity = 10 }, }; Func> fillFunc = (ni, b) => new List { TestHelpers.MakePartAt(0, 0, 10) }; - var result = IterativeShrinkFiller.Fill(items, new Box(0, 0, 100, 100), fillFunc, 1.0, cts.Token); + var result = IterativeShrinkFiller.Fill( + items, + new Box(0, 0, 100, 100), + fillFunc, + 1.0, + cts.Token + ); Assert.NotNull(result); } diff --git a/OpenNest.Tests/Fill/PairOverlapDiagnosticTests.cs b/OpenNest.Tests/Fill/PairOverlapDiagnosticTests.cs index 1abab32..f594d3e 100644 --- a/OpenNest.Tests/Fill/PairOverlapDiagnosticTests.cs +++ b/OpenNest.Tests/Fill/PairOverlapDiagnosticTests.cs @@ -47,10 +47,10 @@ public class PairOverlapDiagnosticTests } [Theory] - [InlineData(0)] // 0 degrees - [InlineData(90)] // 90 degrees - [InlineData(180)] // 180 degrees - [InlineData(270)] // 270 degrees + [InlineData(0)] // 0 degrees + [InlineData(90)] // 90 degrees + [InlineData(180)] // 180 degrees + [InlineData(270)] // 270 degrees public void PartBoundary_HasEdgesAtAllRotations_RoundedRect(double angleDeg) { var drawing = MakeRoundedRect(); @@ -109,8 +109,8 @@ public class PairOverlapDiagnosticTests } [Theory] - [InlineData(false)] // simple rect - [InlineData(true)] // rounded rect + [InlineData(false)] // simple rect + [InlineData(true)] // rounded rect public void FillExtents_NoPairOverlap_At90Degrees(bool rounded) { var drawing = rounded ? MakeRoundedRect() : MakeSimpleRect(); @@ -126,8 +126,10 @@ public class PairOverlapDiagnosticTests for (var i = 0; i < parts.Count; i++) { var p = parts[i]; - _output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " + - $"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})"); + _output.WriteLine( + $" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " + + $"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})" + ); } // Check for overlapping bounding boxes @@ -137,15 +139,21 @@ public class PairOverlapDiagnosticTests for (var j = i + 1; j < parts.Count; j++) { var b2 = parts[j].BoundingBox; - var overlapX = System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left); - var overlapY = System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom); + var overlapX = + System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left); + var overlapY = + System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom); if (overlapX > 0.01 && overlapY > 0.01) - _output.WriteLine($" OVERLAP: [{i}] and [{j}] overlap by ({overlapX:F3}, {overlapY:F3})"); + _output.WriteLine( + $" OVERLAP: [{i}] and [{j}] overlap by ({overlapX:F3}, {overlapY:F3})" + ); - Assert.False(overlapX > 0.01 && overlapY > 0.01, - $"Parts [{i}] and [{j}] have overlapping bounding boxes " + - $"({overlapX:F3} x {overlapY:F3})"); + Assert.False( + overlapX > 0.01 && overlapY > 0.01, + $"Parts [{i}] and [{j}] have overlapping bounding boxes " + + $"({overlapX:F3} x {overlapY:F3})" + ); } } } @@ -172,8 +180,12 @@ public class PairOverlapDiagnosticTests var b1 = new PartBoundary(part1, partSpacing / 2); var b2 = new PartBoundary(part2, partSpacing / 2); - _output.WriteLine($"Part1 (90°) boundary edges: L={b1.GetEdges(PushDirection.Left).Length} R={b1.GetEdges(PushDirection.Right).Length}"); - _output.WriteLine($"Part2 (270°) boundary edges: L={b2.GetEdges(PushDirection.Left).Length} R={b2.GetEdges(PushDirection.Right).Length}"); + _output.WriteLine( + $"Part1 (90°) boundary edges: L={b1.GetEdges(PushDirection.Left).Length} R={b1.GetEdges(PushDirection.Right).Length}" + ); + _output.WriteLine( + $"Part2 (270°) boundary edges: L={b2.GetEdges(PushDirection.Left).Length} R={b2.GetEdges(PushDirection.Right).Length}" + ); var movingLines = b2.GetLines(part2.Location, PushDirection.Left); var stationaryLines = b1.GetLines(part1.Location, PushDirection.Right); @@ -189,7 +201,11 @@ public class PairOverlapDiagnosticTests foreach (var l in stationaryLines) _output.WriteLine($" ({l.pt1.X:F4},{l.pt1.Y:F4})->({l.pt2.X:F4},{l.pt2.Y:F4})"); - var slideDist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left); + var slideDist = SpatialQuery.DirectionalDistance( + movingLines, + stationaryLines, + PushDirection.Left + ); _output.WriteLine($"Slide distance: {slideDist:F4}"); if (slideDist < double.MaxValue && slideDist > 0) @@ -198,8 +214,12 @@ public class PairOverlapDiagnosticTests part2.UpdateBounds(); } - _output.WriteLine($"Part1 bbox: ({part1.BoundingBox.Left:F2},{part1.BoundingBox.Bottom:F2})-({part1.BoundingBox.Right:F2},{part1.BoundingBox.Top:F2})"); - _output.WriteLine($"Part2 bbox: ({part2.BoundingBox.Left:F2},{part2.BoundingBox.Bottom:F2})-({part2.BoundingBox.Right:F2},{part2.BoundingBox.Top:F2})"); + _output.WriteLine( + $"Part1 bbox: ({part1.BoundingBox.Left:F2},{part1.BoundingBox.Bottom:F2})-({part1.BoundingBox.Right:F2},{part1.BoundingBox.Top:F2})" + ); + _output.WriteLine( + $"Part2 bbox: ({part2.BoundingBox.Left:F2},{part2.BoundingBox.Bottom:F2})-({part2.BoundingBox.Right:F2},{part2.BoundingBox.Top:F2})" + ); // Now tile this pair pattern var pattern = new Pattern(); @@ -216,8 +236,10 @@ public class PairOverlapDiagnosticTests for (var i = 0; i < parts.Count; i++) { var p = parts[i]; - _output.WriteLine($" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " + - $"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})"); + _output.WriteLine( + $" [{i}] rot={Angle.ToDegrees(p.Rotation):F1}° " + + $"bbox=({p.BoundingBox.Left:F2},{p.BoundingBox.Bottom:F2})-({p.BoundingBox.Right:F2},{p.BoundingBox.Top:F2})" + ); } // Check for overlaps @@ -230,8 +252,10 @@ public class PairOverlapDiagnosticTests var ox = System.Math.Min(bi.Right, bj.Right) - System.Math.Max(bi.Left, bj.Left); var oy = System.Math.Min(bi.Top, bj.Top) - System.Math.Max(bi.Bottom, bj.Bottom); - Assert.False(ox > 0.01 && oy > 0.01, - $"Parts [{i}] and [{j}] overlap ({ox:F3} x {oy:F3})"); + Assert.False( + ox > 0.01 && oy > 0.01, + $"Parts [{i}] and [{j}] overlap ({ox:F3} x {oy:F3})" + ); } } } diff --git a/OpenNest.Tests/Fill/RemnantFillerTests2.cs b/OpenNest.Tests/Fill/RemnantFillerTests2.cs index 744c555..2e37714 100644 --- a/OpenNest.Tests/Fill/RemnantFillerTests2.cs +++ b/OpenNest.Tests/Fill/RemnantFillerTests2.cs @@ -28,7 +28,7 @@ public class RemnantFillerTests2 var drawing = MakeSquareDrawing(10); var items = new List { - new NestItem { Drawing = drawing, Quantity = 5 } + new NestItem { Drawing = drawing, Quantity = 5 }, }; Func> fillFunc = (ni, b) => @@ -52,7 +52,7 @@ public class RemnantFillerTests2 var drawing = MakeSquareDrawing(10); var items = new List { - new NestItem { Drawing = drawing, Quantity = 3 } + new NestItem { Drawing = drawing, Quantity = 3 }, }; Func> fillFunc = (ni, b) => @@ -92,7 +92,7 @@ public class RemnantFillerTests2 var drawing = MakeSquareDrawing(10); var items = new List { - new NestItem { Drawing = drawing, Quantity = 5 } + new NestItem { Drawing = drawing, Quantity = 5 }, }; Func> fillFunc = (ni, b) => diff --git a/OpenNest.Tests/Fill/RemnantFinderTests.cs b/OpenNest.Tests/Fill/RemnantFinderTests.cs index f667da6..4cfc55f 100644 --- a/OpenNest.Tests/Fill/RemnantFinderTests.cs +++ b/OpenNest.Tests/Fill/RemnantFinderTests.cs @@ -108,16 +108,15 @@ public class RemnantFinderTests var remnants = finder.FindRemnants(); var gap = remnants.FirstOrDefault(r => - r.Length >= 19.9 && r.Length <= 20.1 && - r.Width >= 99.9); + r.Length >= 19.9 && r.Length <= 20.1 && r.Width >= 99.9 + ); Assert.NotNull(gap); } [Fact] public void FromPlate_CreatesFinderWithPartsAsObstacles() { - var plate = TestHelpers.MakePlate(60, 120, - TestHelpers.MakePartAt(0, 0, 20)); + var plate = TestHelpers.MakePlate(60, 120, TestHelpers.MakePartAt(0, 0, 20)); var finder = RemnantFinder.FromPlate(plate); var remnants = finder.FindRemnants(); @@ -146,8 +145,8 @@ public class RemnantFinderTests // Should find the 80x100 strip on the left var left = remnants.FirstOrDefault(r => - r.Length >= 79.9 && r.Length <= 80.1 && - r.Width >= 99.9); + r.Length >= 79.9 && r.Length <= 80.1 && r.Width >= 99.9 + ); Assert.NotNull(left); } @@ -163,9 +162,16 @@ public class RemnantFinderTests foreach (var r in remnants) { Assert.False( - r.Left < 60 && r.Right > 0 && r.Bottom < 60 && r.Top > 0 - && r.Left < 100 && r.Right > 40 && r.Bottom < 100 && r.Top > 40, - "Remnant should not overlap both obstacles simultaneously in their shared region"); + r.Left < 60 + && r.Right > 0 + && r.Bottom < 60 + && r.Top > 0 + && r.Left < 100 + && r.Right > 40 + && r.Bottom < 100 + && r.Top > 40, + "Remnant should not overlap both obstacles simultaneously in their shared region" + ); } // Total remnant area + obstacle coverage should not exceed work area @@ -177,16 +183,11 @@ public class RemnantFinderTests [Fact] public void ConstructorWithObstaclesList() { - var obstacles = new List - { - new Box(0, 0, 40, 100), - new Box(60, 0, 40, 100) - }; + var obstacles = new List { new Box(0, 0, 40, 100), new Box(60, 0, 40, 100) }; var finder = new RemnantFinder(new Box(0, 0, 100, 100), obstacles); var remnants = finder.FindRemnants(); - var gap = remnants.FirstOrDefault(r => - r.Length >= 19.9 && r.Length <= 20.1); + var gap = remnants.FirstOrDefault(r => r.Length >= 19.9 && r.Length <= 20.1); Assert.NotNull(gap); } @@ -194,15 +195,10 @@ public class RemnantFinderTests public void AddObstacles_Plural_AddsMultiple() { var finder = new RemnantFinder(new Box(0, 0, 100, 100)); - finder.AddObstacles(new[] - { - new Box(0, 0, 40, 100), - new Box(60, 0, 40, 100) - }); + finder.AddObstacles(new[] { new Box(0, 0, 40, 100), new Box(60, 0, 40, 100) }); var remnants = finder.FindRemnants(); - var gap = remnants.FirstOrDefault(r => - r.Length >= 19.9 && r.Length <= 20.1); + var gap = remnants.FirstOrDefault(r => r.Length >= 19.9 && r.Length <= 20.1); Assert.NotNull(gap); } @@ -239,11 +235,17 @@ public class RemnantFinderTests { // Check no remnant overlaps obstacle 1 var overlaps1 = r.Left < 50 && r.Right > 20 && r.Bottom < 50 && r.Top > 20; - Assert.False(overlaps1, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 1"); + Assert.False( + overlaps1, + $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 1" + ); // Check no remnant overlaps obstacle 2 var overlaps2 = r.Left < 85 && r.Right > 60 && r.Bottom < 90 && r.Top > 10; - Assert.False(overlaps2, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2"); + Assert.False( + overlaps2, + $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2" + ); } } @@ -254,8 +256,8 @@ public class RemnantFinderTests // Place a 5x5 grid of 10x10 obstacles with 10-unit gaps for (var row = 0; row < 5; row++) - for (var col = 0; col < 5; col++) - finder.AddObstacle(new Box(col * 20, row * 20, 10, 10)); + for (var col = 0; col < 5; col++) + finder.AddObstacle(new Box(col * 20, row * 20, 10, 10)); var remnants = finder.FindRemnants(); @@ -304,16 +306,19 @@ public class RemnantFinderTests // Use smallest drawing bbox dimension as minDim (same as UI). var minDim = nest.Drawings.Min(d => - System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length)); + System.Math.Min(d.Program.BoundingBox().Width, d.Program.BoundingBox().Length) + ); var tiered = finder.FindTieredRemnants(minDim); // Should find a remnant near (0.25, 53.13) — the gap above the main grid. var topGap = tiered.FirstOrDefault(t => - t.Box.Bottom > 50 && t.Box.Bottom < 55 && - t.Box.Left < 1 && - t.Box.Length > 100 && - t.Box.Width > 5); + t.Box.Bottom > 50 + && t.Box.Bottom < 55 + && t.Box.Left < 1 + && t.Box.Length > 100 + && t.Box.Width > 5 + ); Assert.True(topGap.Box.Length > 0, "Expected remnant above main grid"); } @@ -337,24 +342,44 @@ public class RemnantFinderTests double[] oddY = { 0.75, 9.48, 18.21, 26.94, 35.67, 44.40 }; foreach (var cx in colX) - foreach (var ey in evenY) - obstacles.Add(new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2)); + foreach (var ey in evenY) + obstacles.Add( + new Box(cx - spacing, ey - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2) + ); foreach (var cx in colXOdd) - foreach (var oy in oddY) - obstacles.Add(new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2)); + foreach (var oy in oddY) + obstacles.Add( + new Box(cx - spacing, oy - spacing, 20.65 + spacing * 2, 5.56 + spacing * 2) + ); // Right-side rotated parts (only 2 extend high: parts 62 and 66). - obstacles.Add(new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); - obstacles.Add(new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); + obstacles.Add( + new Box(106.70 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); + obstacles.Add( + new Box(114.19 - spacing, 37.59 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); // Parts 63, 67 (lower rotated) - obstacles.Add(new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); - obstacles.Add(new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); + obstacles.Add( + new Box(105.02 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); + obstacles.Add( + new Box(112.51 - spacing, 29.35 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); // Parts 60, 64 (upper-right rotated, lower) - obstacles.Add(new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); - obstacles.Add(new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); + obstacles.Add( + new Box(106.70 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); + obstacles.Add( + new Box(114.19 - spacing, 8.99 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); // Parts 61, 65 - obstacles.Add(new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); - obstacles.Add(new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2)); + obstacles.Add( + new Box(105.02 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); + obstacles.Add( + new Box(112.51 - spacing, 0.75 - spacing, 5.56 + spacing * 2, 20.65 + spacing * 2) + ); var finder = new RemnantFinder(workArea, obstacles); var remnants = finder.FindRemnants(5.375); diff --git a/OpenNest.Tests/Fill/ShrinkFillerTests.cs b/OpenNest.Tests/Fill/ShrinkFillerTests.cs index 380a3bd..2eba14e 100644 --- a/OpenNest.Tests/Fill/ShrinkFillerTests.cs +++ b/OpenNest.Tests/Fill/ShrinkFillerTests.cs @@ -72,8 +72,14 @@ public class ShrinkFillerTests Func> fillFunc = (ni, b) => new List { TestHelpers.MakePartAt(0, 0, 10) }; - var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, - ShrinkAxis.Length, token: cts.Token); + var result = ShrinkFiller.Shrink( + fillFunc, + item, + box, + 1.0, + ShrinkAxis.Length, + token: cts.Token + ); Assert.NotNull(result); Assert.True(result.Parts.Count > 0); @@ -84,10 +90,10 @@ public class ShrinkFillerTests { var parts = new List { - TestHelpers.MakePartAt(0, 0, 5), // Right = 5 - TestHelpers.MakePartAt(10, 0, 5), // Right = 15 - TestHelpers.MakePartAt(20, 0, 5), // Right = 25 - TestHelpers.MakePartAt(30, 0, 5), // Right = 35 + TestHelpers.MakePartAt(0, 0, 5), // Right = 5 + TestHelpers.MakePartAt(10, 0, 5), // Right = 15 + TestHelpers.MakePartAt(20, 0, 5), // Right = 25 + TestHelpers.MakePartAt(30, 0, 5), // Right = 35 }; var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Width); @@ -101,10 +107,10 @@ public class ShrinkFillerTests { var parts = new List { - TestHelpers.MakePartAt(0, 0, 5), // Top = 5 - TestHelpers.MakePartAt(0, 10, 5), // Top = 15 - TestHelpers.MakePartAt(0, 20, 5), // Top = 25 - TestHelpers.MakePartAt(0, 30, 5), // Top = 35 + TestHelpers.MakePartAt(0, 0, 5), // Top = 5 + TestHelpers.MakePartAt(0, 10, 5), // Top = 15 + TestHelpers.MakePartAt(0, 20, 5), // Top = 25 + TestHelpers.MakePartAt(0, 30, 5), // Top = 35 }; var trimmed = ShrinkFiller.TrimToCount(parts, 2, ShrinkAxis.Length); diff --git a/OpenNest.Tests/Geometry/CollisionTests.cs b/OpenNest.Tests/Geometry/CollisionTests.cs index bb3f2cb..488107c 100644 --- a/OpenNest.Tests/Geometry/CollisionTests.cs +++ b/OpenNest.Tests/Geometry/CollisionTests.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Tests.Geometry; @@ -141,8 +141,8 @@ public class CollisionTests public void CheckAll_MultiplePolygons_FindsAllOverlaps() { var a = MakeSquare(0, 0, 1, 1); - var b = MakeSquare(0.5, 0, 1.5, 1); // overlaps A - var c = MakeSquare(5, 5, 6, 6); // overlaps nobody + var b = MakeSquare(0.5, 0, 1.5, 1); // overlaps A + var c = MakeSquare(5, 5, 6, 6); // overlaps nobody var results = Collision.CheckAll(new List { a, b, c }); diff --git a/OpenNest.Tests/Geometry/ContourClassificationTests.cs b/OpenNest.Tests/Geometry/ContourClassificationTests.cs index 593b46a..488f02c 100644 --- a/OpenNest.Tests/Geometry/ContourClassificationTests.cs +++ b/OpenNest.Tests/Geometry/ContourClassificationTests.cs @@ -67,11 +67,7 @@ public class ContourClassificationTests [Fact] public void Classify_identifies_etch_layer_shapes() { - var shapes = new List - { - MakeRectShape(0, 0, 100, 50), - MakeEtchShape(), - }; + var shapes = new List { MakeRectShape(0, 0, 100, 50), MakeEtchShape() }; var contours = ContourInfo.Classify(shapes); @@ -86,11 +82,7 @@ public class ContourClassificationTests openShape.Entities.Add(new Line(new Vector(10, 0), new Vector(10, 5))); // Not closed — doesn't return to (0,0) - var shapes = new List - { - MakeRectShape(0, 0, 100, 50), - openShape, - }; + var shapes = new List { MakeRectShape(0, 0, 100, 50), openShape }; var contours = ContourInfo.Classify(shapes); @@ -100,11 +92,7 @@ public class ContourClassificationTests [Fact] public void Classify_orders_holes_first_perimeter_last() { - var shapes = new List - { - MakeRectShape(0, 0, 100, 50), - MakeCircleShape(25, 25, 5), - }; + var shapes = new List { MakeRectShape(0, 0, 100, 50), MakeCircleShape(25, 25, 5) }; var contours = ContourInfo.Classify(shapes); diff --git a/OpenNest.Tests/Geometry/EllipseConverterTests.cs b/OpenNest.Tests/Geometry/EllipseConverterTests.cs index 89a20e2..37bcc59 100644 --- a/OpenNest.Tests/Geometry/EllipseConverterTests.cs +++ b/OpenNest.Tests/Geometry/EllipseConverterTests.cs @@ -1,9 +1,9 @@ +using System.Linq; using OpenNest.Geometry; using OpenNest.IO; using OpenNest.Math; using Xunit; using Xunit.Abstractions; -using System.Linq; namespace OpenNest.Tests.Geometry; @@ -89,8 +89,14 @@ public class EllipseConverterTests public void Convert_Circle_ProducesOneOrTwoArcs() { var result = EllipseConverter.Convert( - new Vector(0, 0), semiMajor: 10, semiMinor: 10, rotation: 0, - startParam: 0, endParam: Angle.TwoPI, tolerance: 0.001); + new Vector(0, 0), + semiMajor: 10, + semiMinor: 10, + rotation: 0, + startParam: 0, + endParam: Angle.TwoPI, + tolerance: 0.001 + ); Assert.All(result, e => Assert.IsType(e)); Assert.InRange(result.Count, 1, 4); @@ -103,8 +109,14 @@ public class EllipseConverterTests var b = 7.0; var tolerance = 0.001; var result = EllipseConverter.Convert( - new Vector(0, 0), a, b, rotation: 0, - startParam: 0, endParam: Angle.TwoPI, tolerance: tolerance); + new Vector(0, 0), + a, + b, + rotation: 0, + startParam: 0, + endParam: Angle.TwoPI, + tolerance: tolerance + ); Assert.True(result.Count >= 4, $"Expected at least 4 arcs, got {result.Count}"); Assert.All(result, e => Assert.IsType(e)); @@ -113,9 +125,11 @@ public class EllipseConverterTests { var arc = (Arc)entity; var maxDev = MaxDeviationFromEllipse(arc, new Vector(0, 0), a, b, 0, 50); - Assert.True(maxDev <= tolerance, - $"Arc at center ({arc.Center.X:F4},{arc.Center.Y:F4}) r={arc.Radius:F4} " + - $"deviates {maxDev:F6} from ellipse (tolerance={tolerance})"); + Assert.True( + maxDev <= tolerance, + $"Arc at center ({arc.Center.X:F4},{arc.Center.Y:F4}) r={arc.Radius:F4} " + + $"deviates {maxDev:F6} from ellipse (tolerance={tolerance})" + ); } } @@ -126,18 +140,29 @@ public class EllipseConverterTests var b = 3.0; var tolerance = 0.001; var result = EllipseConverter.Convert( - new Vector(0, 0), a, b, rotation: 0, - startParam: 0, endParam: Angle.TwoPI, tolerance: tolerance); + new Vector(0, 0), + a, + b, + rotation: 0, + startParam: 0, + endParam: Angle.TwoPI, + tolerance: tolerance + ); - Assert.True(result.Count >= 8, $"Expected at least 8 arcs for eccentric ellipse, got {result.Count}"); + Assert.True( + result.Count >= 8, + $"Expected at least 8 arcs for eccentric ellipse, got {result.Count}" + ); Assert.All(result, e => Assert.IsType(e)); foreach (var entity in result) { var arc = (Arc)entity; var maxDev = MaxDeviationFromEllipse(arc, new Vector(0, 0), a, b, 0, 50); - Assert.True(maxDev <= tolerance, - $"Deviation {maxDev:F6} exceeds tolerance {tolerance}"); + Assert.True( + maxDev <= tolerance, + $"Deviation {maxDev:F6} exceeds tolerance {tolerance}" + ); } } @@ -148,8 +173,14 @@ public class EllipseConverterTests var b = 5.0; var tolerance = 0.001; var result = EllipseConverter.Convert( - new Vector(0, 0), a, b, rotation: 0, - startParam: 0, endParam: System.Math.PI / 2, tolerance: tolerance); + new Vector(0, 0), + a, + b, + rotation: 0, + startParam: 0, + endParam: System.Math.PI / 2, + tolerance: tolerance + ); Assert.NotEmpty(result); Assert.All(result, e => Assert.IsType(e)); @@ -169,23 +200,27 @@ public class EllipseConverterTests public void Convert_EndpointContinuity_ArcsConnect() { var result = EllipseConverter.Convert( - new Vector(5, 10), semiMajor: 15, semiMinor: 8, rotation: 0.5, - startParam: 0, endParam: Angle.TwoPI, tolerance: 0.001); + new Vector(5, 10), + semiMajor: 15, + semiMinor: 8, + rotation: 0.5, + startParam: 0, + endParam: Angle.TwoPI, + tolerance: 0.001 + ); for (var i = 0; i < result.Count - 1; i++) { var current = (Arc)result[i]; var next = (Arc)result[i + 1]; var gap = current.EndPoint().DistanceTo(next.StartPoint()); - Assert.True(gap < 1e-6, - $"Gap of {gap:E4} between arc {i} and arc {i + 1}"); + Assert.True(gap < 1e-6, $"Gap of {gap:E4} between arc {i} and arc {i + 1}"); } var lastArc = (Arc)result[^1]; var firstArc = (Arc)result[0]; var closingGap = lastArc.EndPoint().DistanceTo(firstArc.StartPoint()); - Assert.True(closingGap < 1e-6, - $"Closing gap of {closingGap:E4}"); + Assert.True(closingGap < 1e-6, $"Closing gap of {closingGap:E4}"); } [Fact] @@ -197,16 +232,25 @@ public class EllipseConverterTests var b = 6.0; var tolerance = 0.001; - var result = EllipseConverter.Convert(center, a, b, rotation, - startParam: 0, endParam: Angle.TwoPI, tolerance: tolerance); + var result = EllipseConverter.Convert( + center, + a, + b, + rotation, + startParam: 0, + endParam: Angle.TwoPI, + tolerance: tolerance + ); Assert.NotEmpty(result); foreach (var entity in result) { var arc = (Arc)entity; var maxDev = MaxDeviationFromEllipse(arc, center, a, b, rotation, 50); - Assert.True(maxDev <= tolerance, - $"Deviation {maxDev:F6} exceeds tolerance {tolerance}"); + Assert.True( + maxDev <= tolerance, + $"Deviation {maxDev:F6} exceeds tolerance {tolerance}" + ); } } @@ -221,7 +265,7 @@ public class EllipseConverterTests MajorAxisEndPoint = new CSMath.XYZ(10, 0, 0), RadiusRatio = 0.6, StartParameter = 0, - EndParameter = System.Math.PI * 2 + EndParameter = System.Math.PI * 2, }; doc.Entities.Add(ellipse); @@ -253,19 +297,24 @@ public class EllipseConverterTests public void DxfImport_ArcBoundingBoxes_Diagnostic() { var path = @"C:\Users\aisaacs\Desktop\11ga tab.dxf"; - if (!System.IO.File.Exists(path)) return; + if (!System.IO.File.Exists(path)) + return; var result = Dxf.Import(path); var all = (System.Collections.Generic.IEnumerable)result.Entities; var bbox = all.GetBoundingBox(); - _output.WriteLine($"Overall: X={bbox.X:F4} Y={bbox.Y:F4} W={bbox.Length:F4} H={bbox.Width:F4}"); + _output.WriteLine( + $"Overall: X={bbox.X:F4} Y={bbox.Y:F4} W={bbox.Length:F4} H={bbox.Width:F4}" + ); for (var i = 0; i < result.Entities.Count; i++) { var e = result.Entities[i]; var b = e.BoundingBox; var flag = (b.Length > 1 || b.Width > 1) ? " ***" : ""; - _output.WriteLine($"{i + 1,3}. {e.GetType().Name,-8} X={b.X:F4} Y={b.Y:F4} W={b.Length:F4} H={b.Width:F4}{flag}"); + _output.WriteLine( + $"{i + 1, 3}. {e.GetType().Name, -8} X={b.X:F4} Y={b.Y:F4} W={b.Length:F4} H={b.Width:F4}{flag}" + ); } } @@ -279,7 +328,7 @@ public class EllipseConverterTests RadiusRatio = 0.28, StartParameter = 0.017, EndParameter = 1.571, - Normal = new CSMath.XYZ(0, 0, 1) + Normal = new CSMath.XYZ(0, 0, 1), }; var flipped = new ACadSharp.Entities.Ellipse @@ -289,7 +338,7 @@ public class EllipseConverterTests RadiusRatio = 0.28, StartParameter = 0.017, EndParameter = 1.571, - Normal = new CSMath.XYZ(0, 0, -1) + Normal = new CSMath.XYZ(0, 0, -1), }; var normalArcs = normal.ToOpenNest(); @@ -305,13 +354,25 @@ public class EllipseConverterTests var normalStart = GetArcStart(normalFirst); var flippedStart = GetArcStart(flippedFirst); - Assert.True(normalStart.X < 0, $"Normal ellipse start X should be negative, got {normalStart.X}"); - Assert.True(flippedStart.X > 0, $"Flipped ellipse should bulge right, got {flippedStart.X}"); + Assert.True( + normalStart.X < 0, + $"Normal ellipse start X should be negative, got {normalStart.X}" + ); + Assert.True( + flippedStart.X > 0, + $"Flipped ellipse should bulge right, got {flippedStart.X}" + ); var normalBbox = GetBoundingBox(normalArcs.Cast()); var flippedBbox = GetBoundingBox(flippedArcs.Cast()); - Assert.True(flippedBbox.minX > 0, $"Flipped ellipse should stay on positive X side, minX={flippedBbox.minX}"); - Assert.True(normalBbox.maxX < 0, $"Normal ellipse should stay on negative X side, maxX={normalBbox.maxX}"); + Assert.True( + flippedBbox.minX > 0, + $"Flipped ellipse should stay on positive X side, minX={flippedBbox.minX}" + ); + Assert.True( + normalBbox.maxX < 0, + $"Normal ellipse should stay on negative X side, maxX={normalBbox.maxX}" + ); } private static (double minX, double maxX) GetBoundingBox(IEnumerable arcs) @@ -333,7 +394,8 @@ public class EllipseConverterTests var angle = arc.IsReversed ? arc.EndAngle : arc.StartAngle; return new Vector( arc.Center.X + arc.Radius * System.Math.Cos(angle), - arc.Center.Y + arc.Radius * System.Math.Sin(angle)); + arc.Center.Y + arc.Radius * System.Math.Sin(angle) + ); } private static Vector GetArcEnd(Arc arc) @@ -341,11 +403,18 @@ public class EllipseConverterTests var angle = arc.IsReversed ? arc.StartAngle : arc.EndAngle; return new Vector( arc.Center.X + arc.Radius * System.Math.Cos(angle), - arc.Center.Y + arc.Radius * System.Math.Sin(angle)); + arc.Center.Y + arc.Radius * System.Math.Sin(angle) + ); } - private static double MaxDeviationFromEllipse(Arc arc, Vector ellipseCenter, - double semiMajor, double semiMinor, double rotation, int samples) + private static double MaxDeviationFromEllipse( + Arc arc, + Vector ellipseCenter, + double semiMajor, + double semiMinor, + double rotation, + int samples + ) { var maxDev = 0.0; var sweep = arc.SweepAngle(); @@ -367,9 +436,19 @@ public class EllipseConverterTests for (var j = 0; j <= 1000; j++) { var t = (double)j / 1000 * Angle.TwoPI; - var ep2 = EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t); + var ep2 = EllipseConverter.EvaluatePoint( + semiMajor, + semiMinor, + rotation, + ellipseCenter, + t + ); var dist = arcPoint.DistanceTo(ep2); - if (dist < minDist) { minDist = dist; bestT = t; } + if (dist < minDist) + { + minDist = dist; + bestT = t; + } } // Refine with local bisection around bestT @@ -379,12 +458,40 @@ public class EllipseConverterTests { var t1 = lo + (hi - lo) / 3; var t2 = lo + 2 * (hi - lo) / 3; - var d1 = arcPoint.DistanceTo(EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t1)); - var d2 = arcPoint.DistanceTo(EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, t2)); - if (d1 < d2) hi = t2; else lo = t1; + var d1 = arcPoint.DistanceTo( + EllipseConverter.EvaluatePoint( + semiMajor, + semiMinor, + rotation, + ellipseCenter, + t1 + ) + ); + var d2 = arcPoint.DistanceTo( + EllipseConverter.EvaluatePoint( + semiMajor, + semiMinor, + rotation, + ellipseCenter, + t2 + ) + ); + if (d1 < d2) + hi = t2; + else + lo = t1; } - var bestDist = arcPoint.DistanceTo(EllipseConverter.EvaluatePoint(semiMajor, semiMinor, rotation, ellipseCenter, (lo + hi) / 2)); - if (bestDist > maxDev) maxDev = bestDist; + var bestDist = arcPoint.DistanceTo( + EllipseConverter.EvaluatePoint( + semiMajor, + semiMinor, + rotation, + ellipseCenter, + (lo + hi) / 2 + ) + ); + if (bestDist > maxDev) + maxDev = bestDist; } return maxDev; diff --git a/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs b/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs index b8f268b..44ba0b1 100644 --- a/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs +++ b/OpenNest.Tests/Geometry/GeometrySimplifierTests.cs @@ -1,7 +1,7 @@ -using OpenNest.Geometry; -using OpenNest.IO; using System.IO; using System.Linq; +using OpenNest.Geometry; +using OpenNest.IO; using Xunit; namespace OpenNest.Tests.Geometry; @@ -251,7 +251,8 @@ public class GeometrySimplifierTests foreach (var shape in shapes) { var candidates = simplifier.Analyze(shape); - if (candidates.Count == 0) continue; + if (candidates.Count == 0) + continue; var simplified = simplifier.Apply(shape, candidates); @@ -265,20 +266,23 @@ public class GeometrySimplifierTests { Line l => l.EndPoint, Arc a => a.EndPoint(), - _ => Vector.Invalid + _ => Vector.Invalid, }; var nextStart = next switch { Line l => l.StartPoint, Arc a => a.StartPoint(), - _ => Vector.Invalid + _ => Vector.Invalid, }; - if (!currentEnd.IsValid() || !nextStart.IsValid()) continue; + if (!currentEnd.IsValid() || !nextStart.IsValid()) + continue; var gap = currentEnd.DistanceTo(nextStart); - Assert.True(gap < 0.005, - $"Gap of {gap:F4} between entities {i} ({current.GetType().Name}) and {i + 1} ({next.GetType().Name})"); + Assert.True( + gap < 0.005, + $"Gap of {gap:F4} between entities {i} ({current.GetType().Name}) and {i + 1} ({next.GetType().Name})" + ); } } } diff --git a/OpenNest.Tests/Geometry/PolygonHelperTests.cs b/OpenNest.Tests/Geometry/PolygonHelperTests.cs index 2844f02..f3bbd7f 100644 --- a/OpenNest.Tests/Geometry/PolygonHelperTests.cs +++ b/OpenNest.Tests/Geometry/PolygonHelperTests.cs @@ -30,10 +30,13 @@ public class PolygonHelperTests // OffsetSide.Left offsets outward or inward depending on winding, // but either way the result must be a different size. Assert.True( - System.Math.Abs(withSpacing.Polygon.BoundingBox.Width - noSpacing.Polygon.BoundingBox.Width) > 0.5, - $"Expected polygon width to differ by >0.5 with 1mm spacing. " + - $"No-spacing width: {noSpacing.Polygon.BoundingBox.Width:F3}, " + - $"With-spacing width: {withSpacing.Polygon.BoundingBox.Width:F3}"); + System.Math.Abs( + withSpacing.Polygon.BoundingBox.Width - noSpacing.Polygon.BoundingBox.Width + ) > 0.5, + $"Expected polygon width to differ by >0.5 with 1mm spacing. " + + $"No-spacing width: {noSpacing.Polygon.BoundingBox.Width:F3}, " + + $"With-spacing width: {withSpacing.Polygon.BoundingBox.Width:F3}" + ); } [Fact] @@ -47,10 +50,14 @@ public class PolygonHelperTests noSpacing.Polygon.UpdateBounds(); withSpacing.Polygon.UpdateBounds(); - Assert.True(withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width, - $"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}"); - Assert.True(withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length, - $"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}"); + Assert.True( + withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width, + $"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}" + ); + Assert.True( + withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length, + $"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}" + ); } [Fact] @@ -71,10 +78,14 @@ public class PolygonHelperTests noSpacing.Polygon.UpdateBounds(); withSpacing.Polygon.UpdateBounds(); - Assert.True(withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width, - $"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}"); - Assert.True(withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length, - $"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}"); + Assert.True( + withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width, + $"Inflated width {withSpacing.Polygon.BoundingBox.Width:F3} should be > original {noSpacing.Polygon.BoundingBox.Width:F3}" + ); + Assert.True( + withSpacing.Polygon.BoundingBox.Length > noSpacing.Polygon.BoundingBox.Length, + $"Inflated length {withSpacing.Polygon.BoundingBox.Length:F3} should be > original {noSpacing.Polygon.BoundingBox.Length:F3}" + ); } [Fact] diff --git a/OpenNest.Tests/Geometry/SpatialQueryTests.cs b/OpenNest.Tests/Geometry/SpatialQueryTests.cs index dd99fb4..65af921 100644 --- a/OpenNest.Tests/Geometry/SpatialQueryTests.cs +++ b/OpenNest.Tests/Geometry/SpatialQueryTests.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; -using System.Collections.Generic; namespace OpenNest.Tests.Geometry; @@ -45,9 +45,19 @@ public class SpatialQueryTests foreach (var e in entities) { if (e is Line line) - result.Add(new Line(line.pt1.X + dx, line.pt1.Y + dy, line.pt2.X + dx, line.pt2.Y + dy)); + result.Add( + new Line(line.pt1.X + dx, line.pt1.Y + dy, line.pt2.X + dx, line.pt2.Y + dy) + ); else if (e is Arc arc) - result.Add(new Arc(arc.Center.X + dx, arc.Center.Y + dy, arc.Radius, arc.StartAngle, arc.EndAngle)); + result.Add( + new Arc( + arc.Center.X + dx, + arc.Center.Y + dy, + arc.Radius, + arc.StartAngle, + arc.EndAngle + ) + ); else if (e is Circle circle) result.Add(new Circle(circle.Center.X + dx, circle.Center.Y + dy, circle.Radius)); } diff --git a/OpenNest.Tests/Geometry/SplineConverterTests.cs b/OpenNest.Tests/Geometry/SplineConverterTests.cs index 5547c66..752c310 100644 --- a/OpenNest.Tests/Geometry/SplineConverterTests.cs +++ b/OpenNest.Tests/Geometry/SplineConverterTests.cs @@ -63,7 +63,7 @@ public class SplineConverterTests var points = new System.Collections.Generic.List { new Vector(0, 0), - new Vector(10, 5) + new Vector(10, 5), }; var result = SplineConverter.Convert(points, isClosed: false, tolerance: 0.001); @@ -89,16 +89,18 @@ public class SplineConverterTests var endPt = GetEndPoint(result[i]); var startPt = GetStartPoint(result[i + 1]); var gap = endPt.DistanceTo(startPt); - Assert.True(gap < 0.001, - $"Gap of {gap:F6} between entity {i} and {i + 1}"); + Assert.True(gap < 0.001, $"Gap of {gap:F6} between entity {i} and {i + 1}"); } } [Fact] public void Convert_EmptyPoints_ReturnsEmpty() { - var result = SplineConverter.Convert(new System.Collections.Generic.List(), - isClosed: false, tolerance: 0.001); + var result = SplineConverter.Convert( + new System.Collections.Generic.List(), + isClosed: false, + tolerance: 0.001 + ); Assert.Empty(result); } @@ -116,7 +118,7 @@ public class SplineConverterTests { Arc a => a.StartPoint(), Line l => l.StartPoint, - _ => throw new System.Exception("Unexpected entity type") + _ => throw new System.Exception("Unexpected entity type"), }; } @@ -126,7 +128,7 @@ public class SplineConverterTests { Arc a => a.EndPoint(), Line l => l.EndPoint, - _ => throw new System.Exception("Unexpected entity type") + _ => throw new System.Exception("Unexpected entity type"), }; } } diff --git a/OpenNest.Tests/GravographIS/EnvelopeGuardTests.cs b/OpenNest.Tests/GravographIS/EnvelopeGuardTests.cs index fb52061..6753f4c 100644 --- a/OpenNest.Tests/GravographIS/EnvelopeGuardTests.cs +++ b/OpenNest.Tests/GravographIS/EnvelopeGuardTests.cs @@ -150,11 +150,7 @@ public class EnvelopeGuardTests new[] { new Vector(0, 0), new Vector(0, -2) }, }; - var opts = new GravographISWriterOptions - { - WorkEnvelopeXMm = 25.4, - WorkEnvelopeYMm = 25.4, - }; + var opts = new GravographISWriterOptions { WorkEnvelopeXMm = 25.4, WorkEnvelopeYMm = 25.4 }; Assert.Throws(() => { diff --git a/OpenNest.Tests/GravographIS/GravographISWriterTests.cs b/OpenNest.Tests/GravographIS/GravographISWriterTests.cs index 4db5947..74f052d 100644 --- a/OpenNest.Tests/GravographIS/GravographISWriterTests.cs +++ b/OpenNest.Tests/GravographIS/GravographISWriterTests.cs @@ -14,16 +14,16 @@ public class GravographISWriterTests // those frozen deltas send the head to a fixed point regardless of the job. The // writer now emits a job-specific leading DR travel from operator zero instead. private const string PreambleHex = - "21 41 53 20 33 38 3b 01 90 01 f4 01 90 01 f4 01 90 01 f4 00 00 00 00 00 00 00 00 00 00 " + - "00 00 00 09 00 00 03 e8 05 06 00 00 00 00 00 00 ff fd 32 44 00 00 ff fd 4d 43 00 01 ff fd " + - "4f 55 ff fb ff fd 4f 55 ff fa ff fd 50 5a 00 00 ff fd 56 53 00 23 ff fd 56 5a 00 23 ff fd " + - "44 5a 01 fc"; + "21 41 53 20 33 38 3b 01 90 01 f4 01 90 01 f4 01 90 01 f4 00 00 00 00 00 00 00 00 00 00 " + + "00 00 00 09 00 00 03 e8 05 06 00 00 00 00 00 00 ff fd 32 44 00 00 ff fd 4d 43 00 01 ff fd " + + "4f 55 ff fb ff fd 4f 55 ff fa ff fd 50 5a 00 00 ff fd 56 53 00 23 ff fd 56 5a 00 23 ff fd " + + "44 5a 01 fc"; // Legacy 36-byte tail with lift, aux off, motor off, operator beep, job finish. // Byte-exact capture tests disable dynamic return-to-origin to preserve this form. private const string PostambleHex = - "ff fd 50 55 00 01 ff fd 4f 55 ff fa ff fd 4f 55 ff fb ff fd 4d 43 00 00 " + - "ff fd 4f 50 00 00 ff fd 4a 46 00 00"; + "ff fd 50 55 00 01 ff fd 4f 55 ff fa ff fd 4f 55 ff fb ff fd 4d 43 00 00 " + + "ff fd 4f 50 00 00 ff fd 4a 46 00 00"; [Fact] public void TestA_SingleTwoInchVerticalLine_IsByteExact() @@ -33,20 +33,22 @@ public class GravographISWriterTests new[] { new Vector(1, 1), new Vector(1, 3) }, }; - var writer = new GravographISWriter(new GravographISWriterOptions - { - DepthInches = 0.25, - FeedMmPerSec = 35, - EnvelopeGuardEnabled = false, - ReturnToOriginAtEnd = false, - }); + var writer = new GravographISWriter( + new GravographISWriterOptions + { + DepthInches = 0.25, + FeedMmPerSec = 35, + EnvelopeGuardEnabled = false, + ReturnToOriginAtEnd = false, + } + ); using var ms = new MemoryStream(); writer.Write(polylines, ms); const string GeomHex = - "ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " + - "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20"; + "ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " + + "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20"; var expected = HexToBytes(PreambleHex + " " + GeomHex + " " + PostambleHex); Assert.Equal(expected, ms.ToArray()); @@ -63,26 +65,28 @@ public class GravographISWriterTests new[] { new Vector(1, 5), new Vector(1, 7) }, }; - var writer = new GravographISWriter(new GravographISWriterOptions - { - DepthInches = 0.25, - FeedMmPerSec = 35, - EnvelopeGuardEnabled = false, - ReturnToOriginAtEnd = false, - }); + var writer = new GravographISWriter( + new GravographISWriterOptions + { + DepthInches = 0.25, + FeedMmPerSec = 35, + EnvelopeGuardEnabled = false, + ReturnToOriginAtEnd = false, + } + ); using var ms = new MemoryStream(); writer.Write(polylines, ms); const string GeomHex = - "ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " + - "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " + - "ff fd 50 55 00 00 35 40 00 b4 17 d0 0f e0 " + - "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " + - "ff fd 50 55 00 00 40 00 00 b4 00 00 f0 20 " + - "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " + - "ff fd 50 55 00 00 35 40 00 b4 e8 30 0f e0 " + - "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20"; + "ff fd 44 52 00 00 2d 41 00 80 07 f0 f8 10 " + + "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " + + "ff fd 50 55 00 00 35 40 00 b4 17 d0 0f e0 " + + "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " + + "ff fd 50 55 00 00 40 00 00 b4 00 00 f0 20 " + + "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20 " + + "ff fd 50 55 00 00 35 40 00 b4 e8 30 0f e0 " + + "ff fd 50 44 00 00 40 00 00 b4 00 00 f0 20"; var expected = HexToBytes(PreambleHex + " " + GeomHex + " " + PostambleHex); Assert.Equal(expected, ms.ToArray()); @@ -97,7 +101,9 @@ public class GravographISWriterTests }; using var ms = new MemoryStream(); - new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false }).Write(polylines, ms); + new GravographISWriter( + new GravographISWriterOptions { EnvelopeGuardEnabled = false } + ).Write(polylines, ms); var bytes = ms.ToArray(); // First command after the 93-byte preamble must be DR to the first point, @@ -122,7 +128,9 @@ public class GravographISWriterTests }; using var ms = new MemoryStream(); - new GravographISWriter(new GravographISWriterOptions { EnvelopeGuardEnabled = false }).Write(polylines, ms); + new GravographISWriter( + new GravographISWriterOptions { EnvelopeGuardEnabled = false } + ).Write(polylines, ms); var bytes = ms.ToArray(); Assert.Equal((byte)'D', bytes[95]); @@ -140,11 +148,13 @@ public class GravographISWriterTests }; using var ms = new MemoryStream(); - new GravographISWriter(new GravographISWriterOptions - { - DepthInches = 0.125, // 254 steps = 0x00FE - FeedMmPerSec = 50, // 0x0032 - }).Write(polylines, ms); + new GravographISWriter( + new GravographISWriterOptions + { + DepthInches = 0.125, // 254 steps = 0x00FE + FeedMmPerSec = 50, // 0x0032 + } + ).Write(polylines, ms); var bytes = ms.ToArray(); AssertOperand(bytes, (byte)'V', (byte)'S', 0x00, 0x32); @@ -277,7 +287,12 @@ public class GravographISWriterTests { for (var i = 0; i < bytes.Length - 5; i++) { - if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD && bytes[i + 2] == c0 && bytes[i + 3] == c1) + if ( + bytes[i] == 0xFF + && bytes[i + 1] == 0xFD + && bytes[i + 2] == c0 + && bytes[i + 3] == c1 + ) { Assert.Equal(hi, bytes[i + 4]); Assert.Equal(lo, bytes[i + 5]); @@ -291,9 +306,14 @@ public class GravographISWriterTests { for (var i = bytes.Length - 6; i >= 0; i--) { - if (bytes[i] == 0xFF && bytes[i + 1] == 0xFD && - bytes[i + 2] == c0 && bytes[i + 3] == c1 && - bytes[i + 4] == hi && bytes[i + 5] == lo) + if ( + bytes[i] == 0xFF + && bytes[i + 1] == 0xFD + && bytes[i + 2] == c0 + && bytes[i + 3] == c1 + && bytes[i + 4] == hi + && bytes[i + 5] == lo + ) { return i; } @@ -309,7 +329,9 @@ public class GravographISWriterTests internal static byte[] HexToBytes(string hex) { - var clean = hex.Replace(" ", string.Empty).Replace("\n", string.Empty).Replace("\r", string.Empty); + var clean = hex.Replace(" ", string.Empty) + .Replace("\n", string.Empty) + .Replace("\r", string.Empty); var bytes = new byte[clean.Length / 2]; for (var i = 0; i < bytes.Length; i++) bytes[i] = System.Convert.ToByte(clean.Substring(i * 2, 2), 16); diff --git a/OpenNest.Tests/GravographIS/PolylinePrePassTests.cs b/OpenNest.Tests/GravographIS/PolylinePrePassTests.cs index 112d9f2..c3d89e6 100644 --- a/OpenNest.Tests/GravographIS/PolylinePrePassTests.cs +++ b/OpenNest.Tests/GravographIS/PolylinePrePassTests.cs @@ -108,8 +108,10 @@ public class PolylinePrePassTests Assert.Equal(3, reordered.Count); var travelBefore = TotalPenUpTravel(inputs); var travelAfter = TotalPenUpTravel(reordered); - Assert.True(travelAfter < travelBefore, - $"Expected reorder to reduce pen-up travel; before={travelBefore}, after={travelAfter}"); + Assert.True( + travelAfter < travelBefore, + $"Expected reorder to reduce pen-up travel; before={travelBefore}, after={travelAfter}" + ); } [Fact] @@ -150,7 +152,8 @@ public class PolylinePrePassTests Vector? last = null; foreach (var p in polylines) { - if (p == null || p.Count < 2) continue; + if (p == null || p.Count < 2) + continue; if (last.HasValue) { var dx = p[0].X - last.Value.X; diff --git a/OpenNest.Tests/IO/CadImporterTests.cs b/OpenNest.Tests/IO/CadImporterTests.cs index bceb883..4569f0a 100644 --- a/OpenNest.Tests/IO/CadImporterTests.cs +++ b/OpenNest.Tests/IO/CadImporterTests.cs @@ -7,8 +7,7 @@ namespace OpenNest.Tests.IO { public class CadImporterTests { - private static string TestDxf => - Path.Combine("Bending", "TestData", "4526 A14 PT11.dxf"); + private static string TestDxf => Path.Combine("Bending", "TestData", "4526 A14 PT11.dxf"); [Fact] public void Import_LoadsEntitiesAndDetectsBends() @@ -45,8 +44,10 @@ namespace OpenNest.Tests.IO // Exercises the named-detector branch: when BendDetectorName doesn't // match any registered detector, bends should be an empty list // (not a crash, and no fall-through to auto-detect). - var result = CadImporter.Import(TestDxf, - new CadImportOptions { BendDetectorName = "__nonexistent__" }); + var result = CadImporter.Import( + TestDxf, + new CadImportOptions { BendDetectorName = "__nonexistent__" } + ); Assert.Empty(result.Bends); } @@ -62,7 +63,8 @@ namespace OpenNest.Tests.IO result.Bends, quantity: 5, customer: "ACME", - editedProgram: null); + editedProgram: null + ); Assert.NotNull(drawing); Assert.Equal("4526 A14 PT11", drawing.Name); @@ -80,8 +82,14 @@ namespace OpenNest.Tests.IO { var result = CadImporter.Import(TestDxf); - var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends, - quantity: 1, customer: null, editedProgram: null); + var drawing = CadImporter.BuildDrawing( + result, + result.Entities, + result.Bends, + quantity: 1, + customer: null, + editedProgram: null + ); Assert.NotNull(drawing.Source.Offset); // After offset extraction, the program's first rapid must start at origin. @@ -95,15 +103,21 @@ namespace OpenNest.Tests.IO { var result = CadImporter.Import(TestDxf); // Suppress the first non-bend-source entity - var bendSources = result.Bends - .Where(b => b.SourceEntity != null) + var bendSources = result + .Bends.Where(b => b.SourceEntity != null) .Select(b => b.SourceEntity) .ToHashSet(); var hidden = result.Entities.First(e => !bendSources.Contains(e)); hidden.IsVisible = false; - var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends, - quantity: 1, customer: null, editedProgram: null); + var drawing = CadImporter.BuildDrawing( + result, + result.Entities, + result.Bends, + quantity: 1, + customer: null, + editedProgram: null + ); Assert.Contains(hidden.Id, drawing.SuppressedEntityIds); } @@ -115,8 +129,14 @@ namespace OpenNest.Tests.IO var edited = new OpenNest.CNC.Program(); edited.MoveTo(new OpenNest.Geometry.Vector(0, 0)); - var drawing = CadImporter.BuildDrawing(result, result.Entities, result.Bends, - quantity: 1, customer: null, editedProgram: edited); + var drawing = CadImporter.BuildDrawing( + result, + result.Entities, + result.Bends, + quantity: 1, + customer: null, + editedProgram: edited + ); Assert.Same(edited, drawing.Program); } @@ -124,8 +144,10 @@ namespace OpenNest.Tests.IO [Fact] public void ImportDrawing_ComposesImportAndBuild() { - var drawing = CadImporter.ImportDrawing(TestDxf, - new CadImportOptions { Quantity = 3, Customer = "ACME" }); + var drawing = CadImporter.ImportDrawing( + TestDxf, + new CadImportOptions { Quantity = 3, Customer = "ACME" } + ); Assert.NotNull(drawing); Assert.Equal("4526 A14 PT11", drawing.Name); diff --git a/OpenNest.Tests/IO/ChrFontTests.cs b/OpenNest.Tests/IO/ChrFontTests.cs index 9581a2a..e86d99e 100644 --- a/OpenNest.Tests/IO/ChrFontTests.cs +++ b/OpenNest.Tests/IO/ChrFontTests.cs @@ -51,7 +51,10 @@ public class ChrFontTests Assert.NotNull(glyph); var entities = glyph.ToEntities(1.0, 0, 0); - Assert.True(entities.Count >= 2, $"Expected at least 2 entities for 'L', got {entities.Count}"); + Assert.True( + entities.Count >= 2, + $"Expected at least 2 entities for 'L', got {entities.Count}" + ); Assert.All(entities, e => Assert.Equal(EntityType.Line, e.Type)); } @@ -99,8 +102,10 @@ public class ChrFontTests var abBox = abEntities.GetBoundingBox(); var aBox = aEntities.GetBoundingBox(); - Assert.True(abBox.Length > aBox.Length * 1.5, - $"AB width ({abBox.Length:F1}) should be significantly wider than A width ({aBox.Length:F1})"); + Assert.True( + abBox.Length > aBox.Length * 1.5, + $"AB width ({abBox.Length:F1}) should be significantly wider than A width ({aBox.Length:F1})" + ); } [SkippableFact] @@ -129,18 +134,28 @@ public class ChrFontTests var tolerance = 0.5; - Assert.True(System.Math.Abs(box.Left - refLeft) < tolerance, - $"Left: ours={box.Left:F2}, ref={refLeft:F2}, diff={System.Math.Abs(box.Left - refLeft):F2}"); - Assert.True(System.Math.Abs(box.Right - refRight) < tolerance, - $"Right: ours={box.Right:F2}, ref={refRight:F2}, diff={System.Math.Abs(box.Right - refRight):F2}"); - Assert.True(System.Math.Abs(box.Bottom - refBottom) < tolerance, - $"Bottom: ours={box.Bottom:F2}, ref={refBottom:F2}, diff={System.Math.Abs(box.Bottom - refBottom):F2}"); - Assert.True(System.Math.Abs(box.Top - refTop) < tolerance, - $"Top: ours={box.Top:F2}, ref={refTop:F2}, diff={System.Math.Abs(box.Top - refTop):F2}"); + Assert.True( + System.Math.Abs(box.Left - refLeft) < tolerance, + $"Left: ours={box.Left:F2}, ref={refLeft:F2}, diff={System.Math.Abs(box.Left - refLeft):F2}" + ); + Assert.True( + System.Math.Abs(box.Right - refRight) < tolerance, + $"Right: ours={box.Right:F2}, ref={refRight:F2}, diff={System.Math.Abs(box.Right - refRight):F2}" + ); + Assert.True( + System.Math.Abs(box.Bottom - refBottom) < tolerance, + $"Bottom: ours={box.Bottom:F2}, ref={refBottom:F2}, diff={System.Math.Abs(box.Bottom - refBottom):F2}" + ); + Assert.True( + System.Math.Abs(box.Top - refTop) < tolerance, + $"Top: ours={box.Top:F2}, ref={refTop:F2}, diff={System.Math.Abs(box.Top - refTop):F2}" + ); var actualCapHeight = box.Top - box.Bottom; - Assert.True(System.Math.Abs(actualCapHeight - height) < 0.5, - $"Cap height: ours={actualCapHeight:F2}, expected={height:F2}"); + Assert.True( + System.Math.Abs(actualCapHeight - height) < 0.5, + $"Cap height: ours={actualCapHeight:F2}, expected={height:F2}" + ); } [SkippableFact] @@ -152,10 +167,14 @@ public class ChrFontTests var entities = font.RenderText("Text", height, Vector.Zero); var box = entities.GetBoundingBox(); - Assert.True(measuredWidth >= box.Length, - $"Measured={measuredWidth:F2} should be >= rendered={box.Length:F2}"); - Assert.True(measuredWidth - box.Length < 2.0, - $"Measured={measuredWidth:F2}, rendered={box.Length:F2}, diff={measuredWidth - box.Length:F2}"); + Assert.True( + measuredWidth >= box.Length, + $"Measured={measuredWidth:F2} should be >= rendered={box.Length:F2}" + ); + Assert.True( + measuredWidth - box.Length < 2.0, + $"Measured={measuredWidth:F2}, rendered={box.Length:F2}, diff={measuredWidth - box.Length:F2}" + ); } [SkippableFact] @@ -171,10 +190,15 @@ public class ChrFontTests Assert.True(lines.Count >= 10, $"Expected at least 10 entities for 't', got {lines.Count}"); var curveLines = lines.Skip(1).Take(lines.Count - 3).ToList(); - Assert.True(curveLines.Count >= 14, $"Expected at least 14 curve segments, got {curveLines.Count}"); + Assert.True( + curveLines.Count >= 14, + $"Expected at least 14 curve segments, got {curveLines.Count}" + ); var lastCurve = curveLines[^1]; - Assert.True(lastCurve.EndPoint.X > curveLines[0].StartPoint.X, - $"Curve should end to the right of where it starts: start X={curveLines[0].StartPoint.X:F1}, end X={lastCurve.EndPoint.X:F1}"); + Assert.True( + lastCurve.EndPoint.X > curveLines[0].StartPoint.X, + $"Curve should end to the right of where it starts: start X={curveLines[0].StartPoint.X:F1}, end X={lastCurve.EndPoint.X:F1}" + ); } } diff --git a/OpenNest.Tests/IO/DxfRoundtripTests.cs b/OpenNest.Tests/IO/DxfRoundtripTests.cs index 452faf5..68e53fc 100644 --- a/OpenNest.Tests/IO/DxfRoundtripTests.cs +++ b/OpenNest.Tests/IO/DxfRoundtripTests.cs @@ -22,12 +22,10 @@ public class DxfRoundtripTests return reimported; } - private static List FilterByLayer(List entities, string layerName) where T : Entity + private static List FilterByLayer(List entities, string layerName) + where T : Entity { - return entities - .Where(e => e is T && e.Layer?.Name == layerName) - .Cast() - .ToList(); + return entities.Where(e => e is T && e.Layer?.Name == layerName).Cast().ToList(); } [Fact] @@ -38,7 +36,7 @@ public class DxfRoundtripTests { new Line(0, 0, 10, 0), new Line(10, 0, 5, 8), - new Line(5, 8, 0, 0) + new Line(5, 8, 0, 0), }; var reimported = ExportAndReimport(original); @@ -97,7 +95,7 @@ public class DxfRoundtripTests new Line(0, 0, 10, 0), new Line(10, 0, 10, 5), new Circle(20, 20, 3), - new Arc(15, 15, 5, 0.0, System.Math.PI) + new Arc(15, 15, 5, 0.0, System.Math.PI), }; var reimported = ExportAndReimport(original); @@ -120,17 +118,25 @@ public class DxfRoundtripTests new Line(0, 0, 20, 0), new Line(20, 0, 20, 10), new Line(20, 10, 0, 10), - new Line(0, 10, 0, 0) + new Line(0, 10, 0, 0), }; var reimported = ExportAndReimport(original); var cutLines = FilterByLayer(reimported, "Cut"); // Verify bounding box is preserved regardless of line order - var origMinX = original.Cast().Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X)); - var origMaxX = original.Cast().Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X)); - var origMinY = original.Cast().Min(l => System.Math.Min(l.StartPoint.Y, l.EndPoint.Y)); - var origMaxY = original.Cast().Max(l => System.Math.Max(l.StartPoint.Y, l.EndPoint.Y)); + var origMinX = original + .Cast() + .Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X)); + var origMaxX = original + .Cast() + .Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X)); + var origMinY = original + .Cast() + .Min(l => System.Math.Min(l.StartPoint.Y, l.EndPoint.Y)); + var origMaxY = original + .Cast() + .Max(l => System.Math.Max(l.StartPoint.Y, l.EndPoint.Y)); var rtMinX = cutLines.Min(l => System.Math.Min(l.StartPoint.X, l.EndPoint.X)); var rtMaxX = cutLines.Max(l => System.Math.Max(l.StartPoint.X, l.EndPoint.X)); diff --git a/OpenNest.Tests/IO/NestBendSerializationTests.cs b/OpenNest.Tests/IO/NestBendSerializationTests.cs index 6c92ca5..0e920f2 100644 --- a/OpenNest.Tests/IO/NestBendSerializationTests.cs +++ b/OpenNest.Tests/IO/NestBendSerializationTests.cs @@ -1,7 +1,7 @@ +using System.Linq; using OpenNest.Bending; using OpenNest.Geometry; using OpenNest.IO; -using System.Linq; namespace OpenNest.Tests.IO; @@ -11,24 +11,28 @@ public class NestBendSerializationTests public void Bends_SurviveNestRoundtrip() { var drawing = TestHelpers.MakeSquareDrawing(); - drawing.Bends.Add(new Bend - { - StartPoint = new Vector(0, 5), - EndPoint = new Vector(10, 5), - Direction = BendDirection.Up, - Angle = 90, - Radius = 0.06, - NoteText = "UP 90° R0.06" - }); - drawing.Bends.Add(new Bend - { - StartPoint = new Vector(0, 3), - EndPoint = new Vector(10, 3), - Direction = BendDirection.Down, - Angle = 45.5, - Radius = 0.125, - NoteText = "DOWN 45.5° R0.125" - }); + drawing.Bends.Add( + new Bend + { + StartPoint = new Vector(0, 5), + EndPoint = new Vector(10, 5), + Direction = BendDirection.Up, + Angle = 90, + Radius = 0.06, + NoteText = "UP 90° R0.06", + } + ); + drawing.Bends.Add( + new Bend + { + StartPoint = new Vector(0, 3), + EndPoint = new Vector(10, 3), + Direction = BendDirection.Down, + Angle = 45.5, + Radius = 0.125, + NoteText = "DOWN 45.5° R0.125", + } + ); var nest = new Nest(); nest.Drawings.Add(drawing); diff --git a/OpenNest.Tests/IO/NestWriterVariableTests.cs b/OpenNest.Tests/IO/NestWriterVariableTests.cs index e13d756..0f5490d 100644 --- a/OpenNest.Tests/IO/NestWriterVariableTests.cs +++ b/OpenNest.Tests/IO/NestWriterVariableTests.cs @@ -13,7 +13,8 @@ public class NestWriterVariableTests public void RoundTrip_VariableDefinitions_Preserved() { var nest = CreateNestWithVariableProgram( - "width = 48.0 global\ndiameter = 0.3\nG90\nG01X$widthY$diameter"); + "width = 48.0 global\ndiameter = 0.3\nG90\nG01X$widthY$diameter" + ); var loaded = RoundTrip(nest); var pgm = loaded.Drawings.First().Program; @@ -28,8 +29,7 @@ public class NestWriterVariableTests [Fact] public void RoundTrip_VariableRefs_Preserved() { - var nest = CreateNestWithVariableProgram( - "width = 48.0\nG90\nG01X$widthY0"); + var nest = CreateNestWithVariableProgram("width = 48.0\nG90\nG01X$widthY0"); var loaded = RoundTrip(nest); var pgm = loaded.Drawings.First().Program; @@ -43,8 +43,7 @@ public class NestWriterVariableTests [Fact] public void RoundTrip_InlineFlag_Preserved() { - var nest = CreateNestWithVariableProgram( - "kerf = 0.06 inline\nG90\nG01X1Y0"); + var nest = CreateNestWithVariableProgram("kerf = 0.06 inline\nG90\nG01X1Y0"); var loaded = RoundTrip(nest); var pgm = loaded.Drawings.First().Program; diff --git a/OpenNest.Tests/IO/SubProgramSerializationTests.cs b/OpenNest.Tests/IO/SubProgramSerializationTests.cs index 184987b..65679c5 100644 --- a/OpenNest.Tests/IO/SubProgramSerializationTests.cs +++ b/OpenNest.Tests/IO/SubProgramSerializationTests.cs @@ -54,7 +54,14 @@ public class SubProgramSerializationTests var pgm = new Program(Mode.Absolute); pgm.SubPrograms[42] = sub; - pgm.Codes.Add(new SubProgramCall { Id = 42, Program = sub, Offset = new Vector(5, 5) }); + pgm.Codes.Add( + new SubProgramCall + { + Id = 42, + Program = sub, + Offset = new Vector(5, 5), + } + ); // Add perimeter so the drawing has non-zero geometry pgm.Codes.Add(new RapidMove(0, 0)); pgm.Codes.Add(new LinearMove(10, 0)); diff --git a/OpenNest.Tests/Math/ExpressionEvaluatorTests.cs b/OpenNest.Tests/Math/ExpressionEvaluatorTests.cs index 067354d..30b2101 100644 --- a/OpenNest.Tests/Math/ExpressionEvaluatorTests.cs +++ b/OpenNest.Tests/Math/ExpressionEvaluatorTests.cs @@ -87,7 +87,7 @@ public class ExpressionEvaluatorTests { var vars = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "Diameter", 0.3 } + { "Diameter", 0.3 }, }; Assert.Equal(0.3, ExpressionEvaluator.Evaluate("$diameter", vars)); } @@ -95,8 +95,7 @@ public class ExpressionEvaluatorTests [Fact] public void Evaluate_UndefinedVariable_Throws() { - Assert.Throws(() => - ExpressionEvaluator.Evaluate("$missing", Empty)); + Assert.Throws(() => ExpressionEvaluator.Evaluate("$missing", Empty)); } [Fact] diff --git a/OpenNest.Tests/PlateSnapToStandardSizeTests.cs b/OpenNest.Tests/PlateSnapToStandardSizeTests.cs index b4a9cd1..7112c0f 100644 --- a/OpenNest.Tests/PlateSnapToStandardSizeTests.cs +++ b/OpenNest.Tests/PlateSnapToStandardSizeTests.cs @@ -31,7 +31,7 @@ public class PlateSnapToStandardSizeTests // 10x20 is well below 48x48 MinSheet -> snap to integer increment. Assert.Null(result.MatchedLabel); Assert.Equal(10, plate.Size.Length); // X axis - Assert.Equal(20, plate.Size.Width); // Y axis + Assert.Equal(20, plate.Size.Width); // Y axis } [Fact] @@ -58,7 +58,7 @@ public class PlateSnapToStandardSizeTests Assert.Equal("48x96", result.MatchedLabel); Assert.Equal(96, plate.Size.Length); // X axis = long - Assert.Equal(48, plate.Size.Width); // Y axis = short + Assert.Equal(48, plate.Size.Width); // Y axis = short } [Fact] @@ -72,7 +72,7 @@ public class PlateSnapToStandardSizeTests Assert.Equal("48x96", result.MatchedLabel); Assert.Equal(48, plate.Size.Length); // X axis = short - Assert.Equal(96, plate.Size.Width); // Y axis = long + Assert.Equal(96, plate.Size.Width); // Y axis = long } [Fact] @@ -105,8 +105,8 @@ public class PlateSnapToStandardSizeTests { var plate = new Plate(200, 200); plate.Parts.Add(MakeRectPart(0, 0, 30, 40)); - plate.Parts.Add(MakeRectPart(30, 0, 30, 40)); // combined X-extent = 60 - plate.Parts.Add(MakeRectPart(0, 40, 60, 60)); // combined extent = 60 x 100 + plate.Parts.Add(MakeRectPart(30, 0, 30, 40)); // combined X-extent = 60 + plate.Parts.Add(MakeRectPart(0, 40, 60, 60)); // combined extent = 60 x 100 var result = plate.SnapToStandardSize(); diff --git a/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs b/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs index 793876e..e1786cb 100644 --- a/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs +++ b/OpenNest.Tests/RapidPlanning/DirectRapidPlannerTests.cs @@ -27,8 +27,10 @@ public class DirectRapidPlannerTests cutArea.Entities.Add(new Line(new Vector(60, 0), new Vector(50, 0))); var result = planner.Plan( - new Vector(0, 0), new Vector(10, 10), - new List { cutArea }); + new Vector(0, 0), + new Vector(10, 10), + new List { cutArea } + ); Assert.False(result.HeadUp); } @@ -45,8 +47,10 @@ public class DirectRapidPlannerTests cutArea.Entities.Add(new Line(new Vector(6, 0), new Vector(5, 0))); var result = planner.Plan( - new Vector(0, 10), new Vector(10, 10), - new List { cutArea }); + new Vector(0, 10), + new Vector(10, 10), + new List { cutArea } + ); Assert.True(result.HeadUp); Assert.Empty(result.Waypoints); diff --git a/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs b/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs index 1ddf277..397da30 100644 --- a/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs +++ b/OpenNest.Tests/Sequencing/AdvancedSequencerTests.cs @@ -24,7 +24,7 @@ public class AdvancedSequencerTests { Method = SequenceMethod.Advanced, MinDistanceBetweenRowsColumns = 5.0, - AlternateRowsColumns = false + AlternateRowsColumns = false, }; var sequencer = new AdvancedSequencer(parameters); var result = sequencer.Sequence(plate.Parts.ToList(), plate); @@ -52,7 +52,7 @@ public class AdvancedSequencerTests { Method = SequenceMethod.Advanced, MinDistanceBetweenRowsColumns = 5.0, - AlternateRowsColumns = true + AlternateRowsColumns = true, }; var sequencer = new AdvancedSequencer(parameters); var result = sequencer.Sequence(plate.Parts.ToList(), plate); diff --git a/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs b/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs index 13dfc8f..955eb1a 100644 --- a/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs +++ b/OpenNest.Tests/Sequencing/DirectionalSequencerTests.cs @@ -5,6 +5,7 @@ namespace OpenNest.Tests.Sequencing; public class DirectionalSequencerTests { private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y); + private static Plate MakePlate(params Part[] parts) => TestHelpers.MakePlate(60, 120, parts); [Fact] diff --git a/OpenNest.Tests/Shapes/LShapeTests.cs b/OpenNest.Tests/Shapes/LShapeTests.cs index 34616d4..989dee2 100644 --- a/OpenNest.Tests/Shapes/LShapeTests.cs +++ b/OpenNest.Tests/Shapes/LShapeTests.cs @@ -30,7 +30,13 @@ public class LShapeTests [Fact] public void GetDrawing_CustomLegDimensions() { - var shape = new LShape { Width = 10, Height = 20, LegWidth = 3, LegHeight = 5 }; + var shape = new LShape + { + Width = 10, + Height = 20, + LegWidth = 3, + LegHeight = 5, + }; var drawing = shape.GetDrawing(); // Area = Width*Height - (Width - LegWidth) * (Height - LegHeight) diff --git a/OpenNest.Tests/Shapes/NgonShapeTests.cs b/OpenNest.Tests/Shapes/NgonShapeTests.cs index 1610198..3f999db 100644 --- a/OpenNest.Tests/Shapes/NgonShapeTests.cs +++ b/OpenNest.Tests/Shapes/NgonShapeTests.cs @@ -31,9 +31,7 @@ public class NgonShapeTests var shape = new NgonShape { Sides = sides, Width = 20 }; var drawing = shape.GetDrawing(); - var moves = drawing.Program.Codes - .OfType() - .Count(); + var moves = drawing.Program.Codes.OfType().Count(); Assert.Equal(sides, moves); } @@ -43,9 +41,7 @@ public class NgonShapeTests var shape = new NgonShape { Sides = 2, Width = 20 }; var drawing = shape.GetDrawing(); - var moves = drawing.Program.Codes - .OfType() - .Count(); + var moves = drawing.Program.Codes.OfType().Count(); Assert.Equal(3, moves); } } diff --git a/OpenNest.Tests/Shapes/PipeFlangeShapeTests.cs b/OpenNest.Tests/Shapes/PipeFlangeShapeTests.cs index 5d0e950..a2b629f 100644 --- a/OpenNest.Tests/Shapes/PipeFlangeShapeTests.cs +++ b/OpenNest.Tests/Shapes/PipeFlangeShapeTests.cs @@ -14,7 +14,7 @@ public class PipeFlangeShapeTests OD = 10, HoleDiameter = 1, HolePatternDiameter = 7, - HoleCount = 4 + HoleCount = 4, }; var drawing = shape.GetDrawing(); @@ -32,7 +32,7 @@ public class PipeFlangeShapeTests HoleDiameter = 1, HolePatternDiameter = 7, HoleCount = 4, - Blind = true + Blind = true, }; var drawing = shape.GetDrawing(); @@ -48,7 +48,7 @@ public class PipeFlangeShapeTests OD = 10, HoleDiameter = 1, HolePatternDiameter = 7, - HoleCount = 4 + HoleCount = 4, }; var drawing = shape.GetDrawing(); @@ -64,9 +64,9 @@ public class PipeFlangeShapeTests HoleDiameter = 1, HolePatternDiameter = 7, HoleCount = 4, - PipeSize = "2", // OD = 2.375 + PipeSize = "2", // OD = 2.375 PipeClearance = 0.125, - Blind = false + Blind = false, }; var drawing = shape.GetDrawing(); @@ -87,7 +87,7 @@ public class PipeFlangeShapeTests HoleCount = 4, PipeSize = "2", PipeClearance = 0.125, - Blind = true + Blind = true, }; var drawing = shape.GetDrawing(); @@ -107,7 +107,7 @@ public class PipeFlangeShapeTests HoleCount = 4, PipeSize = "not-a-real-pipe", PipeClearance = 0.125, - Blind = false + Blind = false, }; var drawing = shape.GetDrawing(); @@ -128,7 +128,7 @@ public class PipeFlangeShapeTests HolePatternDiameter = 7, HoleCount = 4, PipeSize = pipeSize, - PipeClearance = 0.125 + PipeClearance = 0.125, }; var drawing = shape.GetDrawing(); @@ -140,27 +140,27 @@ public class PipeFlangeShapeTests public void LoadFromJson_ProducesCorrectDrawing() { var json = """ - [ - { - "Name": "2in-150#", - "PipeSize": "2", - "PipeClearance": 0.0625, - "OD": 6.0, - "HoleDiameter": 0.75, - "HolePatternDiameter": 4.75, - "HoleCount": 4 - }, - { - "Name": "2in-300#", - "PipeSize": "2", - "PipeClearance": 0.0625, - "OD": 6.5, - "HoleDiameter": 0.75, - "HolePatternDiameter": 5.0, - "HoleCount": 8 - } - ] - """; + [ + { + "Name": "2in-150#", + "PipeSize": "2", + "PipeClearance": 0.0625, + "OD": 6.0, + "HoleDiameter": 0.75, + "HolePatternDiameter": 4.75, + "HoleCount": 4 + }, + { + "Name": "2in-300#", + "PipeSize": "2", + "PipeClearance": 0.0625, + "OD": 6.5, + "HoleDiameter": 0.75, + "HolePatternDiameter": 5.0, + "HoleCount": 8 + } + ] + """; var tempFile = Path.GetTempFileName(); try @@ -208,8 +208,10 @@ public class PipeFlangeShapeTests foreach (var f in flanges) { Assert.False(string.IsNullOrWhiteSpace(f.PipeSize)); - Assert.True(PipeSizes.TryGetOD(f.PipeSize, out _), - $"Unknown PipeSize '{f.PipeSize}' in entry '{f.Name}'"); + Assert.True( + PipeSizes.TryGetOD(f.PipeSize, out _), + $"Unknown PipeSize '{f.PipeSize}' in entry '{f.Name}'" + ); Assert.Equal(0.0625, f.PipeClearance, 0.0001); } } diff --git a/OpenNest.Tests/Shapes/PlateSizesTests.cs b/OpenNest.Tests/Shapes/PlateSizesTests.cs index 7b3e0a7..e05675c 100644 --- a/OpenNest.Tests/Shapes/PlateSizesTests.cs +++ b/OpenNest.Tests/Shapes/PlateSizesTests.cs @@ -43,12 +43,12 @@ public class PlateSizesTests } [Theory] - [InlineData(40, 40, true)] // small - fits trivially - [InlineData(48, 96, true)] // exact - [InlineData(96, 48, true)] // rotated exact - [InlineData(90, 40, true)] // rotated - [InlineData(49, 97, false)] // just over in both dims - [InlineData(50, 50, false)] // too wide in both orientations + [InlineData(40, 40, true)] // small - fits trivially + [InlineData(48, 96, true)] // exact + [InlineData(96, 48, true)] // rotated exact + [InlineData(90, 40, true)] // rotated + [InlineData(49, 97, false)] // just over in both dims + [InlineData(50, 50, false)] // too wide in both orientations public void Entry_Fits_RespectsRotation(double w, double h, bool expected) { var entry = new PlateSizes.Entry("48x96", 48, 96); @@ -233,11 +233,7 @@ public class PlateSizesTests public void Recommend_BoxEnumerable_CombinesIntoEnvelope() { // Two boxes that together span 0..40 x 0..90 -> fits 48x96 - var boxes = new[] - { - new Box(0, 0, 40, 50), - new Box(0, 40, 30, 50), - }; + var boxes = new[] { new Box(0, 0, 40, 50), new Box(0, 40, 30, 50) }; var result = PlateSizes.Recommend(boxes); @@ -247,8 +243,9 @@ public class PlateSizesTests [Fact] public void Recommend_BoxEnumerable_Empty_Throws() { - Assert.Throws( - () => PlateSizes.Recommend(System.Array.Empty())); + Assert.Throws(() => + PlateSizes.Recommend(System.Array.Empty()) + ); } [Fact] diff --git a/OpenNest.Tests/Shapes/RectangleShapeTests.cs b/OpenNest.Tests/Shapes/RectangleShapeTests.cs index 62ec40c..5a7fb0b 100644 --- a/OpenNest.Tests/Shapes/RectangleShapeTests.cs +++ b/OpenNest.Tests/Shapes/RectangleShapeTests.cs @@ -27,7 +27,12 @@ public class RectangleShapeTests [Fact] public void GetDrawing_CustomName_IsUsed() { - var shape = new RectangleShape { Name = "Plate1", Length = 10, Width = 5 }; + var shape = new RectangleShape + { + Name = "Plate1", + Length = 10, + Width = 5, + }; var drawing = shape.GetDrawing(); Assert.Equal("Plate1", drawing.Name); diff --git a/OpenNest.Tests/Shapes/RoundedRectangleShapeTests.cs b/OpenNest.Tests/Shapes/RoundedRectangleShapeTests.cs index 1046b48..f7765fd 100644 --- a/OpenNest.Tests/Shapes/RoundedRectangleShapeTests.cs +++ b/OpenNest.Tests/Shapes/RoundedRectangleShapeTests.cs @@ -7,7 +7,12 @@ public class RoundedRectangleShapeTests [Fact] public void GetDrawing_BoundingBoxMatchesDimensions() { - var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 2 }; + var shape = new RoundedRectangleShape + { + Length = 20, + Width = 10, + Radius = 2, + }; var drawing = shape.GetDrawing(); var bbox = drawing.Program.BoundingBox(); @@ -18,7 +23,12 @@ public class RoundedRectangleShapeTests [Fact] public void GetDrawing_AreaIsLessThanFullRectangle() { - var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 2 }; + var shape = new RoundedRectangleShape + { + Length = 20, + Width = 10, + Radius = 2, + }; var drawing = shape.GetDrawing(); // Area should be less than 20*10=200 because corners are rounded @@ -30,7 +40,12 @@ public class RoundedRectangleShapeTests [Fact] public void GetDrawing_ZeroRadius_MatchesRectangleArea() { - var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 0 }; + var shape = new RoundedRectangleShape + { + Length = 20, + Width = 10, + Radius = 0, + }; var drawing = shape.GetDrawing(); Assert.Equal(200, drawing.Area, 0.5); diff --git a/OpenNest.Tests/Shapes/TShapeTests.cs b/OpenNest.Tests/Shapes/TShapeTests.cs index 982ec88..764e00e 100644 --- a/OpenNest.Tests/Shapes/TShapeTests.cs +++ b/OpenNest.Tests/Shapes/TShapeTests.cs @@ -30,7 +30,13 @@ public class TShapeTests [Fact] public void GetDrawing_CustomStemAndBarDimensions() { - var shape = new TShape { Width = 12, Height = 18, StemWidth = 6, BarHeight = 4 }; + var shape = new TShape + { + Width = 12, + Height = 18, + StemWidth = 6, + BarHeight = 4, + }; var drawing = shape.GetDrawing(); // Area = Width * BarHeight + StemWidth * (Height - BarHeight) diff --git a/OpenNest.Tests/Shapes/TrapezoidShapeTests.cs b/OpenNest.Tests/Shapes/TrapezoidShapeTests.cs index dee2f06..bfb9d46 100644 --- a/OpenNest.Tests/Shapes/TrapezoidShapeTests.cs +++ b/OpenNest.Tests/Shapes/TrapezoidShapeTests.cs @@ -7,7 +7,12 @@ public class TrapezoidShapeTests [Fact] public void GetDrawing_BoundingBoxMatchesDimensions() { - var shape = new TrapezoidShape { BottomWidth = 20, TopWidth = 10, Height = 8 }; + var shape = new TrapezoidShape + { + BottomWidth = 20, + TopWidth = 10, + Height = 8, + }; var drawing = shape.GetDrawing(); var bbox = drawing.Program.BoundingBox(); @@ -18,7 +23,12 @@ public class TrapezoidShapeTests [Fact] public void GetDrawing_AreaIsCorrect() { - var shape = new TrapezoidShape { BottomWidth = 20, TopWidth = 10, Height = 8 }; + var shape = new TrapezoidShape + { + BottomWidth = 20, + TopWidth = 10, + Height = 8, + }; var drawing = shape.GetDrawing(); // Area = (top + bottom) / 2 * height = (10 + 20) / 2 * 8 = 120 diff --git a/OpenNest.Tests/Splitting/DrawingSplitterTests.cs b/OpenNest.Tests/Splitting/DrawingSplitterTests.cs index 534e7b2..8cde933 100644 --- a/OpenNest.Tests/Splitting/DrawingSplitterTests.cs +++ b/OpenNest.Tests/Splitting/DrawingSplitterTests.cs @@ -9,7 +9,12 @@ public class DrawingSplitterTests [Fact] public void Split_Rectangle_Vertical_ProducesTwoPieces() { - var drawing = new RectangleShape { Name = "RECT", Length = 100, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "RECT", + Length = 100, + Width = 50, + }.GetDrawing(); var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -27,7 +32,12 @@ public class DrawingSplitterTests [Fact] public void Split_Rectangle_Horizontal_ProducesTwoPieces() { - var drawing = new RectangleShape { Name = "RECT", Length = 100, Width = 60 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "RECT", + Length = 100, + Width = 60, + }.GetDrawing(); var splitLines = new List { new SplitLine(30.0, CutOffAxis.Horizontal) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -41,11 +51,16 @@ public class DrawingSplitterTests [Fact] public void Split_ThreePieces_NamesSequentially() { - var drawing = new RectangleShape { Name = "PART", Length = 150, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "PART", + Length = 150, + Width = 50, + }.GetDrawing(); var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical), - new SplitLine(100.0, CutOffAxis.Vertical) + new SplitLine(100.0, CutOffAxis.Vertical), }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -60,28 +75,45 @@ public class DrawingSplitterTests [Fact] public void Split_CopiesDrawingProperties() { - var drawing = new RectangleShape { Name = "PART", Length = 100, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "PART", + Length = 100, + Width = 50, + }.GetDrawing(); drawing.Color = System.Drawing.Color.Red; drawing.Priority = 5; - var results = DrawingSplitter.Split(drawing, + var results = DrawingSplitter.Split( + drawing, new List { new SplitLine(50.0, CutOffAxis.Vertical) }, - new SplitParameters()); + new SplitParameters() + ); - Assert.All(results, d => - { - Assert.Equal(System.Drawing.Color.Red, d.Color); - Assert.Equal(5, d.Priority); - }); + Assert.All( + results, + d => + { + Assert.Equal(System.Drawing.Color.Red, d.Color); + Assert.Equal(5, d.Priority); + } + ); } [Fact] public void Split_PiecesNormalizedToOrigin() { - var drawing = new RectangleShape { Name = "PART", Length = 100, Width = 50 }.GetDrawing(); - var results = DrawingSplitter.Split(drawing, + var drawing = new RectangleShape + { + Name = "PART", + Length = 100, + Width = 50, + }.GetDrawing(); + var results = DrawingSplitter.Split( + drawing, new List { new SplitLine(50.0, CutOffAxis.Vertical) }, - new SplitParameters()); + new SplitParameters() + ); // Each piece's program bounding box should start near (0,0) foreach (var d in results) @@ -101,14 +133,14 @@ public class DrawingSplitterTests new Line(new Vector(0, 0), new Vector(100, 0)), new Line(new Vector(100, 0), new Vector(100, 50)), new Line(new Vector(100, 50), new Vector(0, 50)), - new Line(new Vector(0, 50), new Vector(0, 0)) + new Line(new Vector(0, 50), new Vector(0, 0)), }; var cutoutEntities = new List { new Line(new Vector(20, 20), new Vector(30, 20)), new Line(new Vector(30, 20), new Vector(30, 30)), new Line(new Vector(30, 30), new Vector(20, 30)), - new Line(new Vector(20, 30), new Vector(20, 20)) + new Line(new Vector(20, 30), new Vector(20, 20)), }; var allEntities = new List(); allEntities.AddRange(perimeterEntities); @@ -118,24 +150,33 @@ public class DrawingSplitterTests var drawing = new Drawing("HOLE", pgm); // Split at X=50 — cutout is in the left half - var results = DrawingSplitter.Split(drawing, + var results = DrawingSplitter.Split( + drawing, new List { new SplitLine(50.0, CutOffAxis.Vertical) }, - new SplitParameters()); + new SplitParameters() + ); Assert.Equal(2, results.Count); // Left piece should have smaller area (has the cutout) - Assert.True(results[0].Area < results[1].Area, - "Left piece should have less area due to cutout"); + Assert.True( + results[0].Area < results[1].Area, + "Left piece should have less area due to cutout" + ); } [Fact] public void Split_GridSplit_ProducesFourPieces() { - var drawing = new RectangleShape { Name = "GRID", Length = 100, Width = 100 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "GRID", + Length = 100, + Width = 100, + }.GetDrawing(); var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical), - new SplitLine(50.0, CutOffAxis.Horizontal) + new SplitLine(50.0, CutOffAxis.Horizontal), }; var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters()); @@ -149,7 +190,12 @@ public class DrawingSplitterTests [Fact] public void Split_Square_Vertical_PieceWidthsSumToOriginal() { - var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "SQ", + Length = 100, + Width = 100, + }.GetDrawing(); var splitLines = new List { new SplitLine(40.0, CutOffAxis.Vertical) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -171,7 +217,12 @@ public class DrawingSplitterTests [Fact] public void Split_Square_Horizontal_PieceHeightsSumToOriginal() { - var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "SQ", + Length = 100, + Width = 100, + }.GetDrawing(); var splitLines = new List { new SplitLine(60.0, CutOffAxis.Horizontal) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -193,7 +244,12 @@ public class DrawingSplitterTests [Fact] public void Split_Square_Vertical_AreaPreserved() { - var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "SQ", + Length = 100, + Width = 100, + }.GetDrawing(); var originalArea = drawing.Area; var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -207,7 +263,12 @@ public class DrawingSplitterTests [Fact] public void Split_Square_Vertical_PiecesAreClosedPerimeters() { - var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "SQ", + Length = 100, + Width = 100, + }.GetDrawing(); var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -215,17 +276,24 @@ public class DrawingSplitterTests foreach (var piece in results) { - var entities = ConvertProgram.ToGeometry(piece.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var entities = ConvertProgram + .ToGeometry(piece.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); - Assert.True(entities.Count >= 4, $"{piece.Name} should have at least 4 entities for a rectangle"); + Assert.True( + entities.Count >= 4, + $"{piece.Name} should have at least 4 entities for a rectangle" + ); // First entity start should connect to last entity end (closed shape) var firstStart = GetStartPoint(entities[0]); var lastEnd = GetEndPoint(entities[^1]); var closingGap = firstStart.DistanceTo(lastEnd); - Assert.True(closingGap < 0.01, - $"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start"); + Assert.True( + closingGap < 0.01, + $"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start" + ); // Consecutive entities should connect for (var i = 0; i < entities.Count - 1; i++) @@ -233,8 +301,10 @@ public class DrawingSplitterTests var end = GetEndPoint(entities[i]); var start = GetStartPoint(entities[i + 1]); var gap = end.DistanceTo(start); - Assert.True(gap < 0.01, - $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"); + Assert.True( + gap < 0.01, + $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}" + ); } } } @@ -242,7 +312,12 @@ public class DrawingSplitterTests [Fact] public void Split_Square_Horizontal_PiecesAreClosedPerimeters() { - var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "SQ", + Length = 100, + Width = 100, + }.GetDrawing(); var splitLines = new List { new SplitLine(50.0, CutOffAxis.Horizontal) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -250,24 +325,33 @@ public class DrawingSplitterTests foreach (var piece in results) { - var entities = ConvertProgram.ToGeometry(piece.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var entities = ConvertProgram + .ToGeometry(piece.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); - Assert.True(entities.Count >= 4, $"{piece.Name} should have at least 4 entities for a rectangle"); + Assert.True( + entities.Count >= 4, + $"{piece.Name} should have at least 4 entities for a rectangle" + ); var firstStart = GetStartPoint(entities[0]); var lastEnd = GetEndPoint(entities[^1]); var closingGap = firstStart.DistanceTo(lastEnd); - Assert.True(closingGap < 0.01, - $"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start"); + Assert.True( + closingGap < 0.01, + $"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start" + ); for (var i = 0; i < entities.Count - 1; i++) { var end = GetEndPoint(entities[i]); var start = GetStartPoint(entities[i + 1]); var gap = end.DistanceTo(start); - Assert.True(gap < 0.01, - $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"); + Assert.True( + gap < 0.01, + $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}" + ); } } } @@ -275,7 +359,12 @@ public class DrawingSplitterTests [Fact] public void Split_Square_AsymmetricSplit_PieceDimensionsMatchSplitPosition() { - var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "SQ", + Length = 100, + Width = 100, + }.GetDrawing(); var splitLines = new List { new SplitLine(30.0, CutOffAxis.Vertical) }; var parameters = new SplitParameters { Type = SplitType.Straight }; @@ -301,7 +390,7 @@ public class DrawingSplitterTests new Line(new Vector(0, 0), new Vector(100, 0)), new Line(new Vector(100, 0), new Vector(100, 50)), new Line(new Vector(100, 50), new Vector(0, 50)), - new Line(new Vector(0, 50), new Vector(0, 0)) + new Line(new Vector(0, 50), new Vector(0, 0)), }; var hole = new Circle(new Vector(20, 25), 3); var allEntities = new List(); @@ -311,24 +400,32 @@ public class DrawingSplitterTests var pgm = ConvertGeometry.ToProgram(allEntities); var drawing = new Drawing("CIRC", pgm); - var results = DrawingSplitter.Split(drawing, + var results = DrawingSplitter.Split( + drawing, new List { new SplitLine(50.0, CutOffAxis.Vertical) }, - new SplitParameters()); + new SplitParameters() + ); Assert.Equal(2, results.Count); // Left piece should have the hole — verify by checking it has arc entities - var leftEntities = ConvertProgram.ToGeometry(results[0].Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var leftEntities = ConvertProgram + .ToGeometry(results[0].Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); var leftArcs = leftEntities.OfType().ToList(); // Decomposed circle = 2 arcs. Both should be present. - Assert.True(leftArcs.Count >= 2, - $"Left piece should have at least 2 arcs (full circle), but has {leftArcs.Count}"); + Assert.True( + leftArcs.Count >= 2, + $"Left piece should have at least 2 arcs (full circle), but has {leftArcs.Count}" + ); // Right piece should have no arcs (hole is on the left) - var rightEntities = ConvertProgram.ToGeometry(results[1].Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var rightEntities = ConvertProgram + .ToGeometry(results[1].Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); var rightArcs = rightEntities.OfType().ToList(); Assert.Equal(0, rightArcs.Count); } @@ -344,7 +441,7 @@ public class DrawingSplitterTests new Line(new Vector(0, 0), new Vector(100, 0)), new Line(new Vector(100, 0), new Vector(100, 50)), new Line(new Vector(100, 50), new Vector(0, 50)), - new Line(new Vector(0, 50), new Vector(0, 0)) + new Line(new Vector(0, 50), new Vector(0, 0)), }; var hole = new Circle(new Vector(20, 25), 3); var allEntities = new List(); @@ -356,14 +453,19 @@ public class DrawingSplitterTests drawing.Bends = new List(); // Split — the circle gets decomposed into two arcs - var results = DrawingSplitter.Split(drawing, + var results = DrawingSplitter.Split( + drawing, new List { new SplitLine(50.0, CutOffAxis.Vertical) }, - new SplitParameters()); + new SplitParameters() + ); Assert.Equal(2, results.Count); // Write left piece to DXF and re-import - var tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "split_roundtrip_test.dxf"); + var tempPath = System.IO.Path.Combine( + System.IO.Path.GetTempPath(), + "split_roundtrip_test.dxf" + ); try { var writer = new OpenNest.IO.SplitDxfWriter(); @@ -374,8 +476,10 @@ public class DrawingSplitterTests var afterArcs = reimportResult.Entities.OfType().Count(); var afterCircles = reimportResult.Entities.OfType().Count(); - Assert.True(afterArcs + afterCircles * 2 >= 2, - $"After DXF round-trip: {afterArcs} arcs, {afterCircles} circles (expected 2+ for full hole)"); + Assert.True( + afterArcs + afterCircles * 2 >= 2, + $"After DXF round-trip: {afterArcs} arcs, {afterCircles} circles (expected 2+ for full hole)" + ); } finally { @@ -401,14 +505,14 @@ public class DrawingSplitterTests new Line(new Vector(0, 0), new Vector(255, 0)), new Line(new Vector(255, 0), new Vector(255, 55)), new Line(new Vector(255, 55), new Vector(0, 55)), - new Line(new Vector(0, 55), new Vector(0, 0)) + new Line(new Vector(0, 55), new Vector(0, 0)), }; var slotEntities = new List { new Line(new Vector(10, 10), new Vector(245, 10)), new Line(new Vector(245, 10), new Vector(245, 45)), new Line(new Vector(245, 45), new Vector(10, 45)), - new Line(new Vector(10, 45), new Vector(10, 10)) + new Line(new Vector(10, 45), new Vector(10, 10)), }; var allEntities = new List(); allEntities.AddRange(outerEntities); @@ -422,10 +526,14 @@ public class DrawingSplitterTests new SplitLine(55.0, CutOffAxis.Vertical), new SplitLine(110.0, CutOffAxis.Vertical), new SplitLine(165.0, CutOffAxis.Vertical), - new SplitLine(220.0, CutOffAxis.Vertical) + new SplitLine(220.0, CutOffAxis.Vertical), }; - var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters { Type = SplitType.Straight }); + var results = DrawingSplitter.Split( + drawing, + splitLines, + new SplitParameters { Type = SplitType.Straight } + ); // R1 (0..55) → 1 notched piece, height 55 // R2 (55..110) → upper strip + lower strip, each height 10 @@ -454,8 +562,10 @@ public class DrawingSplitterTests // Each piece should form a closed perimeter (no dangling edges, no gaps). foreach (var piece in results) { - var entities = ConvertProgram.ToGeometry(piece.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var entities = ConvertProgram + .ToGeometry(piece.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); Assert.True(entities.Count >= 3, $"{piece.Name} must have at least 3 edges"); @@ -464,8 +574,10 @@ public class DrawingSplitterTests var end = GetEndPoint(entities[i]); var nextStart = GetStartPoint(entities[(i + 1) % entities.Count]); var gap = end.DistanceTo(nextStart); - Assert.True(gap < 0.01, - $"{piece.Name} gap of {gap:F4} between edge {i} end and edge {(i + 1) % entities.Count} start"); + Assert.True( + gap < 0.01, + $"{piece.Name} gap of {gap:F4} between edge {i} end and edge {(i + 1) % entities.Count} start" + ); } } } @@ -477,7 +589,12 @@ public class DrawingSplitterTests // five columns. Exercises the same path as the synthetic // Split_RectangleWithSpanningSlot_ProducesDisconnectedStrips test but through // the full DXF import pipeline. - var path = Path.Combine(AppContext.BaseDirectory, "Splitting", "TestData", "split_test.dxf"); + var path = Path.Combine( + AppContext.BaseDirectory, + "Splitting", + "TestData", + "split_test.dxf" + ); Assert.True(File.Exists(path), $"Test DXF not found: {path}"); var imported = OpenNest.IO.Dxf.Import(path); @@ -487,13 +604,16 @@ public class DrawingSplitterTests var bb = profile.Perimeter.BoundingBox; var offsetX = -bb.X; var offsetY = -bb.Y; - foreach (var e in profile.Perimeter.Entities) e.Offset(offsetX, offsetY); + foreach (var e in profile.Perimeter.Entities) + e.Offset(offsetX, offsetY); foreach (var cutout in profile.Cutouts) - foreach (var e in cutout.Entities) e.Offset(offsetX, offsetY); + foreach (var e in cutout.Entities) + e.Offset(offsetX, offsetY); var allEntities = new List(); allEntities.AddRange(profile.Perimeter.Entities); - foreach (var cutout in profile.Cutouts) allEntities.AddRange(cutout.Entities); + foreach (var cutout in profile.Cutouts) + allEntities.AddRange(cutout.Entities); var drawing = new Drawing("SPLITTEST", ConvertGeometry.ToProgram(allEntities)); var originalArea = drawing.Area; @@ -504,10 +624,14 @@ public class DrawingSplitterTests new SplitLine(55.0, CutOffAxis.Vertical), new SplitLine(110.0, CutOffAxis.Vertical), new SplitLine(165.0, CutOffAxis.Vertical), - new SplitLine(220.0, CutOffAxis.Vertical) + new SplitLine(220.0, CutOffAxis.Vertical), }; - var results = DrawingSplitter.Split(drawing, splitLines, new SplitParameters { Type = SplitType.Straight }); + var results = DrawingSplitter.Split( + drawing, + splitLines, + new SplitParameters { Type = SplitType.Straight } + ); // Area must be preserved within tolerance (floating-point coords in the DXF). var totalArea = results.Sum(d => d.Area); @@ -515,16 +639,20 @@ public class DrawingSplitterTests // At least one region must yield more than one physical strip — that's the // whole point of the fix: a cutout that spans a region disconnects it. - Assert.True(results.Count > splitLines.Count + 1, - $"Expected more than {splitLines.Count + 1} pieces (some regions split into strips), got {results.Count}"); + Assert.True( + results.Count > splitLines.Count + 1, + $"Expected more than {splitLines.Count + 1} pieces (some regions split into strips), got {results.Count}" + ); // Every output drawing must resolve into fully-closed shapes (outer loop // and any hole loops), with no dangling geometry. A piece that contains // a cutout will have its entities span more than one connected loop. foreach (var piece in results) { - var entities = ConvertProgram.ToGeometry(piece.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var entities = ConvertProgram + .ToGeometry(piece.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); Assert.True(entities.Count >= 3, $"{piece.Name} has only {entities.Count} entities"); @@ -533,8 +661,10 @@ public class DrawingSplitterTests foreach (var shape in shapes) { - Assert.True(shape.IsClosed(), - $"{piece.Name} contains an open chain of {shape.Entities.Count} entities"); + Assert.True( + shape.IsClosed(), + $"{piece.Name} contains an open chain of {shape.Entities.Count} entities" + ); } } } @@ -545,7 +675,7 @@ public class DrawingSplitterTests { Line l => l.StartPoint, Arc a => a.StartPoint(), - _ => new Vector(0, 0) + _ => new Vector(0, 0), }; } @@ -555,7 +685,7 @@ public class DrawingSplitterTests { Line l => l.EndPoint, Arc a => a.EndPoint(), - _ => new Vector(0, 0) + _ => new Vector(0, 0), }; } } diff --git a/OpenNest.Tests/Splitting/EntitySplitTests.cs b/OpenNest.Tests/Splitting/EntitySplitTests.cs index b027873..9faffb4 100644 --- a/OpenNest.Tests/Splitting/EntitySplitTests.cs +++ b/OpenNest.Tests/Splitting/EntitySplitTests.cs @@ -98,7 +98,8 @@ public class EntitySplitTests var expectedHigh = 50.0 + System.Math.Sqrt(300); Assert.True( System.Math.Abs(y - expectedLow) < 0.1 || System.Math.Abs(y - expectedHigh) < 0.1, - $"Expected Y near {expectedLow:F2} or {expectedHigh:F2}, got {y:F2}"); + $"Expected Y near {expectedLow:F2} or {expectedHigh:F2}, got {y:F2}" + ); } // --- CrossesSplitLine --- diff --git a/OpenNest.Tests/Splitting/SplitDxfWriterEtchLayerTests.cs b/OpenNest.Tests/Splitting/SplitDxfWriterEtchLayerTests.cs index 484c0e1..4d2be65 100644 --- a/OpenNest.Tests/Splitting/SplitDxfWriterEtchLayerTests.cs +++ b/OpenNest.Tests/Splitting/SplitDxfWriterEtchLayerTests.cs @@ -12,7 +12,12 @@ public class SplitDxfWriterEtchLayerTests public void Write_DrawingWithUpBend_EtchLinesHaveEtchLayer() { // Create a simple rectangular drawing with an up bend - var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "TEST", + Length = 100, + Width = 50, + }.GetDrawing(); drawing.Bends = new List { new Bend @@ -22,8 +27,8 @@ public class SplitDxfWriterEtchLayerTests Direction = BendDirection.Up, Angle = 90, Radius = 0.06, - NoteText = "UP 90° R0.06" - } + NoteText = "UP 90° R0.06", + }, }; var tempPath = Path.Combine(Path.GetTempPath(), $"etch_layer_test_{Guid.NewGuid()}.dxf"); @@ -50,8 +55,9 @@ public class SplitDxfWriterEtchLayerTests // Check if this line is an etch mark (short, near the bend Y=25) var midY = (line.StartPoint.Y + line.EndPoint.Y) / 2; var length = System.Math.Sqrt( - System.Math.Pow(line.EndPoint.X - line.StartPoint.X, 2) + - System.Math.Pow(line.EndPoint.Y - line.StartPoint.Y, 2)); + System.Math.Pow(line.EndPoint.X - line.StartPoint.X, 2) + + System.Math.Pow(line.EndPoint.Y - line.StartPoint.Y, 2) + ); if (System.Math.Abs(midY - 25) < 0.1 && length <= 1.5 && layerName != "BEND") { @@ -61,9 +67,11 @@ public class SplitDxfWriterEtchLayerTests } // Should have etch lines (up bend with length 100 > 3*EtchLength, so 2 etch dashes) - Assert.True(etchEntities.Count >= 2, - $"Expected at least 2 etch lines, found {etchEntities.Count}. " + - $"All entities: {string.Join(", ", allEntities.Select(e => $"{e.Type}@{e.LayerName}"))}"); + Assert.True( + etchEntities.Count >= 2, + $"Expected at least 2 etch lines, found {etchEntities.Count}. " + + $"All entities: {string.Join(", ", allEntities.Select(e => $"{e.Type}@{e.LayerName}"))}" + ); // ALL etch lines should be on the ETCH layer, not layer 0 foreach (var etch in etchEntities) @@ -83,7 +91,12 @@ public class SplitDxfWriterEtchLayerTests public void Write_SplitDrawingWithUpBend_EtchLinesHaveEtchLayer() { // Create a drawing, split it, then verify etch layers in the split DXFs - var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "TEST", + Length = 100, + Width = 50, + }.GetDrawing(); drawing.Bends = new List { new Bend @@ -93,8 +106,8 @@ public class SplitDxfWriterEtchLayerTests Direction = BendDirection.Up, Angle = 90, Radius = 0.06, - NoteText = "UP 90° R0.06" - } + NoteText = "UP 90° R0.06", + }, }; var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical) }; @@ -109,7 +122,10 @@ public class SplitDxfWriterEtchLayerTests Assert.NotNull(splitDrawing.Bends); Assert.True(splitDrawing.Bends.Count > 0, $"{splitDrawing.Name} should have bends"); - var tempPath = Path.Combine(Path.GetTempPath(), $"split_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf"); + var tempPath = Path.Combine( + Path.GetTempPath(), + $"split_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf" + ); try { var writer = new SplitDxfWriter(); @@ -135,15 +151,19 @@ public class SplitDxfWriterEtchLayerTests } // Should have etch entities - Assert.True(etchLayerEntities.Count > 0, - $"{splitDrawing.Name}: No entities on ETCH layer. " + - $"All: {string.Join(", ", entitySummary)}"); + Assert.True( + etchLayerEntities.Count > 0, + $"{splitDrawing.Name}: No entities on ETCH layer. " + + $"All: {string.Join(", ", entitySummary)}" + ); // No entities should be on layer 0 - Assert.True(layer0Entities.Count == 0, - $"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 " + - $"(expected all on CUT/BEND/ETCH). " + - $"All: {string.Join(", ", entitySummary)}"); + Assert.True( + layer0Entities.Count == 0, + $"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 " + + $"(expected all on CUT/BEND/ETCH). " + + $"All: {string.Join(", ", entitySummary)}" + ); } finally { @@ -158,7 +178,12 @@ public class SplitDxfWriterEtchLayerTests { // After re-import, ETCH entities should be filtered (like BEND) since // etch marks are generated from bends, not treated as cut geometry. - var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "TEST", + Length = 100, + Width = 50, + }.GetDrawing(); drawing.Bends = new List { new Bend @@ -168,8 +193,8 @@ public class SplitDxfWriterEtchLayerTests Direction = BendDirection.Up, Angle = 90, Radius = 0.06, - NoteText = "UP 90° R0.06" - } + NoteText = "UP 90° R0.06", + }, }; var splitLines = new List { new SplitLine(50.0, CutOffAxis.Vertical) }; @@ -178,7 +203,10 @@ public class SplitDxfWriterEtchLayerTests foreach (var splitDrawing in results) { - var tempPath = Path.Combine(Path.GetTempPath(), $"reimport_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf"); + var tempPath = Path.Combine( + Path.GetTempPath(), + $"reimport_etch_test_{splitDrawing.Name}_{Guid.NewGuid()}.dxf" + ); try { var writer = new SplitDxfWriter(); @@ -188,25 +216,40 @@ public class SplitDxfWriterEtchLayerTests var result = Dxf.Import(tempPath); // ETCH entities should be filtered during import (like BEND) - var etchEntities = result.Entities - .Where(e => string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase)) + var etchEntities = result + .Entities.Where(e => + string.Equals(e.Layer?.Name, "ETCH", StringComparison.OrdinalIgnoreCase) + ) .ToList(); - var layer0Entities = result.Entities - .Where(e => string.Equals(e.Layer?.Name, "0", StringComparison.OrdinalIgnoreCase)) + var layer0Entities = result + .Entities.Where(e => + string.Equals(e.Layer?.Name, "0", StringComparison.OrdinalIgnoreCase) + ) .ToList(); - Assert.True(etchEntities.Count == 0, - $"{splitDrawing.Name}: ETCH entities should be filtered during import, found {etchEntities.Count}"); + Assert.True( + etchEntities.Count == 0, + $"{splitDrawing.Name}: ETCH entities should be filtered during import, found {etchEntities.Count}" + ); - Assert.True(layer0Entities.Count == 0, - $"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 after re-import"); + Assert.True( + layer0Entities.Count == 0, + $"{splitDrawing.Name}: {layer0Entities.Count} entities on layer 0 after re-import" + ); // All imported entities should be on CUT layer (cut geometry only) - Assert.True(result.Entities.Count > 0, $"{splitDrawing.Name}: Should have cut geometry"); - Assert.True(result.Entities.All(e => string.Equals(e.Layer?.Name, "CUT", StringComparison.OrdinalIgnoreCase)), - $"{splitDrawing.Name}: All imported entities should be on CUT layer. " + - $"Found: {string.Join(", ", result.Entities.Select(e => e.Layer?.Name ?? "(null)").Distinct())}"); + Assert.True( + result.Entities.Count > 0, + $"{splitDrawing.Name}: Should have cut geometry" + ); + Assert.True( + result.Entities.All(e => + string.Equals(e.Layer?.Name, "CUT", StringComparison.OrdinalIgnoreCase) + ), + $"{splitDrawing.Name}: All imported entities should be on CUT layer. " + + $"Found: {string.Join(", ", result.Entities.Select(e => e.Layer?.Name ?? "(null)").Distinct())}" + ); } finally { diff --git a/OpenNest.Tests/Splitting/SplitFeatureTests.cs b/OpenNest.Tests/Splitting/SplitFeatureTests.cs index 9fcd8f0..d65ebe7 100644 --- a/OpenNest.Tests/Splitting/SplitFeatureTests.cs +++ b/OpenNest.Tests/Splitting/SplitFeatureTests.cs @@ -15,7 +15,7 @@ public class SplitFeatureTests Type = SplitType.WeldGapTabs, TabWidth = 2.0, TabHeight = 0.25, - TabCount = 2 + TabCount = 2, }; var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters); @@ -104,7 +104,7 @@ public class SplitFeatureTests Type = SplitType.SpikeGroove, SpikeDepth = 1.0, SpikeAngle = 60.0, - SpikePairCount = 2 + SpikePairCount = 2, }; var result = feature.GenerateFeatures(line, 0.0, 100.0, parameters); diff --git a/OpenNest.Tests/Splitting/SplitIntegrationTest.cs b/OpenNest.Tests/Splitting/SplitIntegrationTest.cs index 44e2e90..a38aeef 100644 --- a/OpenNest.Tests/Splitting/SplitIntegrationTest.cs +++ b/OpenNest.Tests/Splitting/SplitIntegrationTest.cs @@ -9,7 +9,12 @@ public class SplitIntegrationTest [Fact] public void Split_SpikeGroove_NoContinuityGaps() { - var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "TEST", + Length = 100, + Width = 50, + }.GetDrawing(); var sl = new SplitLine(50.0, CutOffAxis.Vertical); sl.FeaturePositions.Add(12.5); @@ -22,7 +27,7 @@ public class SplitIntegrationTest SpikeDepth = 0.75, SpikeWeldGap = 0.125, SpikeAngle = 45, - SpikePairCount = 2 + SpikePairCount = 2, }; var results = DrawingSplitter.Split(drawing, new List { sl }, parameters); @@ -31,8 +36,10 @@ public class SplitIntegrationTest foreach (var piece in results) { // Get cut entities only (no rapids) - var pieceEntities = ConvertProgram.ToGeometry(piece.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var pieceEntities = ConvertProgram + .ToGeometry(piece.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); // Check that consecutive entity endpoints connect (no gaps) for (var i = 0; i < pieceEntities.Count - 1; i++) @@ -40,8 +47,10 @@ public class SplitIntegrationTest var end = GetEndPoint(pieceEntities[i]); var start = GetStartPoint(pieceEntities[i + 1]); var gap = end.DistanceTo(start); - Assert.True(gap < 0.01, - $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"); + Assert.True( + gap < 0.01, + $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}" + ); } // Area should be non-zero @@ -52,7 +61,12 @@ public class SplitIntegrationTest [Fact] public void Split_SpikeGroove_Horizontal_NoContinuityGaps() { - var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing(); + var drawing = new RectangleShape + { + Name = "TEST", + Length = 100, + Width = 50, + }.GetDrawing(); var sl = new SplitLine(25.0, CutOffAxis.Horizontal); sl.FeaturePositions.Add(25.0); @@ -65,7 +79,7 @@ public class SplitIntegrationTest SpikeDepth = 0.75, SpikeWeldGap = 0.125, SpikeAngle = 45, - SpikePairCount = 2 + SpikePairCount = 2, }; var results = DrawingSplitter.Split(drawing, new List { sl }, parameters); @@ -73,16 +87,20 @@ public class SplitIntegrationTest foreach (var piece in results) { - var pieceEntities = ConvertProgram.ToGeometry(piece.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + var pieceEntities = ConvertProgram + .ToGeometry(piece.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); for (var i = 0; i < pieceEntities.Count - 1; i++) { var end = GetEndPoint(pieceEntities[i]); var start = GetStartPoint(pieceEntities[i + 1]); var gap = end.DistanceTo(start); - Assert.True(gap < 0.01, - $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}"); + Assert.True( + gap < 0.01, + $"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}" + ); } Assert.True(piece.Area > 0, $"{piece.Name} has zero area"); @@ -95,7 +113,7 @@ public class SplitIntegrationTest { Line l => l.StartPoint, Arc a => a.StartPoint(), - _ => new Vector(0, 0) + _ => new Vector(0, 0), }; } @@ -105,7 +123,7 @@ public class SplitIntegrationTest { Line l => l.EndPoint, Arc a => a.EndPoint(), - _ => new Vector(0, 0) + _ => new Vector(0, 0), }; } } diff --git a/OpenNest.Tests/Splitting/SplitLineTests.cs b/OpenNest.Tests/Splitting/SplitLineTests.cs index 1256930..c2bc16e 100644 --- a/OpenNest.Tests/Splitting/SplitLineTests.cs +++ b/OpenNest.Tests/Splitting/SplitLineTests.cs @@ -69,7 +69,11 @@ public class AutoSplitCalculatorTests public void SplitByCount_SingleAxis_EvenlySpaced() { var partBounds = new Box(0, 0, 100, 50); - var lines = AutoSplitCalculator.SplitByCount(partBounds, horizontalPieces: 1, verticalPieces: 3); + var lines = AutoSplitCalculator.SplitByCount( + partBounds, + horizontalPieces: 1, + verticalPieces: 3 + ); Assert.Equal(2, lines.Count); Assert.All(lines, l => Assert.Equal(CutOffAxis.Vertical, l.Axis)); diff --git a/OpenNest.Tests/Strategies/FillPipelineTests.cs b/OpenNest.Tests/Strategies/FillPipelineTests.cs index 6f6bb70..2c11ae8 100644 --- a/OpenNest.Tests/Strategies/FillPipelineTests.cs +++ b/OpenNest.Tests/Strategies/FillPipelineTests.cs @@ -25,8 +25,10 @@ public class FillPipelineTests engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); - Assert.True(engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count, - $"Expected phase results from all active strategies, got {engine.PhaseResults.Count}"); + Assert.True( + engine.PhaseResults.Count >= FillStrategyRegistry.Strategies.Count, + $"Expected phase results from all active strategies, got {engine.PhaseResults.Count}" + ); } [Fact] @@ -36,14 +38,21 @@ public class FillPipelineTests var engine = new DefaultNestEngine(plate); var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; - var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None); + var parts = engine.Fill( + item, + plate.WorkArea(), + null, + System.Threading.CancellationToken.None + ); Assert.True(parts.Count > 0); - Assert.True(engine.WinnerPhase == NestPhase.Pairs || - engine.WinnerPhase == NestPhase.Linear || - engine.WinnerPhase == NestPhase.RectBestFit || - engine.WinnerPhase == NestPhase.Extents || - engine.WinnerPhase == NestPhase.Custom); + Assert.True( + engine.WinnerPhase == NestPhase.Pairs + || engine.WinnerPhase == NestPhase.Linear + || engine.WinnerPhase == NestPhase.RectBestFit + || engine.WinnerPhase == NestPhase.Extents + || engine.WinnerPhase == NestPhase.Custom + ); } [Fact] diff --git a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs index a317d9e..d152e06 100644 --- a/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs +++ b/OpenNest.Tests/Strategies/FillStrategyRegistryTests.cs @@ -10,7 +10,10 @@ public class FillStrategyRegistryTests { var strategies = FillStrategyRegistry.Strategies; - Assert.True(strategies.Count >= 6, $"Expected at least 6 built-in strategies, got {strategies.Count}"); + Assert.True( + strategies.Count >= 6, + $"Expected at least 6 built-in strategies, got {strategies.Count}" + ); Assert.Contains(strategies, s => s.Name == "Pairs"); Assert.Contains(strategies, s => s.Name == "RectBestFit"); Assert.Contains(strategies, s => s.Name == "Extents"); @@ -25,8 +28,10 @@ public class FillStrategyRegistryTests var strategies = FillStrategyRegistry.Strategies; for (var i = 1; i < strategies.Count; i++) - Assert.True(strategies[i].Order >= strategies[i - 1].Order, - $"Strategy '{strategies[i].Name}' (Order={strategies[i].Order}) should not precede '{strategies[i - 1].Name}' (Order={strategies[i - 1].Order})"); + Assert.True( + strategies[i].Order >= strategies[i - 1].Order, + $"Strategy '{strategies[i].Name}' (Order={strategies[i].Order}) should not precede '{strategies[i - 1].Name}' (Order={strategies[i - 1].Order})" + ); } [Fact] diff --git a/OpenNest.Tests/Strategies/StrategyOverlapTests.cs b/OpenNest.Tests/Strategies/StrategyOverlapTests.cs index 8cfa906..a5f01b1 100644 --- a/OpenNest.Tests/Strategies/StrategyOverlapTests.cs +++ b/OpenNest.Tests/Strategies/StrategyOverlapTests.cs @@ -35,7 +35,9 @@ public class StrategyOverlapTests if (drawing is null) return; // Skip if test DXF not available - _output.WriteLine($"Drawing bbox: {drawing.Program.BoundingBox().Width:F2} x {drawing.Program.BoundingBox().Length:F2}"); + _output.WriteLine( + $"Drawing bbox: {drawing.Program.BoundingBox().Width:F2} x {drawing.Program.BoundingBox().Length:F2}" + ); var strategies = FillStrategyRegistry.Strategies.ToList(); var item = new NestItem { Drawing = drawing }; @@ -59,12 +61,17 @@ public class StrategyOverlapTests context.SharedState["BestRotation"] = classification.PrimaryAngle; context.SharedState["Classification"] = classification; context.SharedState["AngleCandidates"] = new AngleCandidateBuilder().Build( - item, classification, context.WorkArea); + item, + classification, + context.WorkArea + ); var parts = strategy.Fill(context); var count = parts?.Count ?? 0; - _output.WriteLine($"\n{strategy.GetType().Name} (Phase: {strategy.Phase}, Order: {strategy.Order}): {count} parts"); + _output.WriteLine( + $"\n{strategy.GetType().Name} (Phase: {strategy.Phase}, Order: {strategy.Order}): {count} parts" + ); if (count == 0) continue; @@ -83,7 +90,9 @@ public class StrategyOverlapTests if (hasOverlaps) { - failures.Add($"{strategy.GetType().Name} ({strategy.Phase}): {pts.Count} collision pts, {count} parts"); + failures.Add( + $"{strategy.GetType().Name} ({strategy.Phase}): {pts.Count} collision pts, {count} parts" + ); // Show overlapping pair details for (var a = 0; a < parts.Count; a++) @@ -92,16 +101,27 @@ public class StrategyOverlapTests { var ba = parts[a].BoundingBox; var bb = parts[b].BoundingBox; - var oX = System.Math.Min(ba.Right, bb.Right) - System.Math.Max(ba.Left, bb.Left); - var oY = System.Math.Min(ba.Top, bb.Top) - System.Math.Max(ba.Bottom, bb.Bottom); - if (oX <= OpenNest.Math.Tolerance.Epsilon || oY <= OpenNest.Math.Tolerance.Epsilon) + var oX = + System.Math.Min(ba.Right, bb.Right) - System.Math.Max(ba.Left, bb.Left); + var oY = + System.Math.Min(ba.Top, bb.Top) - System.Math.Max(ba.Bottom, bb.Bottom); + if ( + oX <= OpenNest.Math.Tolerance.Epsilon + || oY <= OpenNest.Math.Tolerance.Epsilon + ) continue; if (parts[a].Intersects(parts[b], out var pairPts) && pairPts.Count > 0) { - _output.WriteLine($" [{a}] vs [{b}]: {pairPts.Count} pts, bbox overlap: {oX:F4} x {oY:F4}"); - _output.WriteLine($" [{a}]: loc=({parts[a].Location.X:F4},{parts[a].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[a].Rotation):F2}°"); - _output.WriteLine($" [{b}]: loc=({parts[b].Location.X:F4},{parts[b].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[b].Rotation):F2}°"); + _output.WriteLine( + $" [{a}] vs [{b}]: {pairPts.Count} pts, bbox overlap: {oX:F4} x {oY:F4}" + ); + _output.WriteLine( + $" [{a}]: loc=({parts[a].Location.X:F4},{parts[a].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[a].Rotation):F2}°" + ); + _output.WriteLine( + $" [{b}]: loc=({parts[b].Location.X:F4},{parts[b].Location.Y:F4}) rot={OpenNest.Math.Angle.ToDegrees(parts[b].Rotation):F2}°" + ); } } } @@ -114,5 +134,4 @@ public class StrategyOverlapTests Assert.Empty(failures); } - } diff --git a/OpenNest.Tests/Strategies/StripeFillerTests.cs b/OpenNest.Tests/Strategies/StripeFillerTests.cs index 3a471ca..63b933d 100644 --- a/OpenNest.Tests/Strategies/StripeFillerTests.cs +++ b/OpenNest.Tests/Strategies/StripeFillerTests.cs @@ -33,8 +33,7 @@ public class StripeFillerTests /// Builds a simple side-by-side pair BestFitResult for a rectangular drawing. /// Places two copies next to each other along the X axis with the given spacing. /// - private static List MakeSideBySideBestFits( - Drawing drawing, double spacing) + private static List MakeSideBySideBestFits(Drawing drawing, double spacing) { var bb = drawing.Program.BoundingBox(); var w = bb.Length; @@ -71,10 +70,15 @@ public class StripeFillerTests { var pattern = MakeRectPattern(20, 10); var angle = StripeFiller.FindAngleForTargetSpan( - pattern.Parts, 20.0, NestDirection.Horizontal); + pattern.Parts, + 20.0, + NestDirection.Horizontal + ); - Assert.True(System.Math.Abs(angle) < 0.05, - $"Expected angle near 0, got {OpenNest.Math.Angle.ToDegrees(angle):F1}°"); + Assert.True( + System.Math.Abs(angle) < 0.05, + $"Expected angle near 0, got {OpenNest.Math.Angle.ToDegrees(angle):F1}°" + ); } [Fact] @@ -82,12 +86,17 @@ public class StripeFillerTests { var pattern = MakeRectPattern(20, 10); var angle = StripeFiller.FindAngleForTargetSpan( - pattern.Parts, 22.0, NestDirection.Horizontal); + pattern.Parts, + 22.0, + NestDirection.Horizontal + ); var rotated = FillHelpers.BuildRotatedPattern(pattern.Parts, angle); var span = rotated.BoundingBox.Length; - Assert.True(System.Math.Abs(span - 22.0) < 0.5, - $"Expected span ~22, got {span:F2} at {OpenNest.Math.Angle.ToDegrees(angle):F1}°"); + Assert.True( + System.Math.Abs(span - 22.0) < 0.5, + $"Expected span ~22, got {span:F2} at {OpenNest.Math.Angle.ToDegrees(angle):F1}°" + ); } [Fact] @@ -95,7 +104,10 @@ public class StripeFillerTests { var pattern = MakeRectPattern(20, 10); var angle = StripeFiller.FindAngleForTargetSpan( - pattern.Parts, 30.0, NestDirection.Horizontal); + pattern.Parts, + 30.0, + NestDirection.Horizontal + ); Assert.True(angle >= 0 && angle <= System.Math.PI / 2); } @@ -105,7 +117,11 @@ public class StripeFillerTests { var pattern = MakeRectPattern(20, 10); var (angle, waste, count) = StripeFiller.ConvergeStripeAngle( - pattern.Parts, 120.0, 0.5, NestDirection.Horizontal); + pattern.Parts, + 120.0, + 0.5, + NestDirection.Horizontal + ); Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}"); Assert.True(waste < 18.0, $"Expected waste < 18, got {waste:F2}"); @@ -117,7 +133,11 @@ public class StripeFillerTests // 10x5 pattern: short side (5) oriented along axis, so more pairs fit var pattern = MakeRectPattern(10, 5); var (angle, waste, count) = StripeFiller.ConvergeStripeAngle( - pattern.Parts, 100.0, 0.0, NestDirection.Horizontal); + pattern.Parts, + 100.0, + 0.0, + NestDirection.Horizontal + ); Assert.True(count >= 10, $"Expected at least 10 pairs, got {count}"); Assert.True(waste < 1.0, $"Expected low waste, got {waste:F2}"); @@ -128,7 +148,11 @@ public class StripeFillerTests { var pattern = MakeRectPattern(10, 20); var (angle, waste, count) = StripeFiller.ConvergeStripeAngle( - pattern.Parts, 120.0, 0.5, NestDirection.Vertical); + pattern.Parts, + 120.0, + 0.5, + NestDirection.Vertical + ); Assert.True(count >= 5, $"Expected at least 5 pairs, got {count}"); } diff --git a/OpenNest.Training/Data/TrainingDbContext.cs b/OpenNest.Training/Data/TrainingDbContext.cs index 5ffcaaa..6dcb95f 100644 --- a/OpenNest.Training/Data/TrainingDbContext.cs +++ b/OpenNest.Training/Data/TrainingDbContext.cs @@ -30,17 +30,13 @@ namespace OpenNest.Training.Data modelBuilder.Entity(e => { e.HasIndex(r => r.PartId).HasDatabaseName("idx_runs_partid"); - e.HasOne(r => r.Part) - .WithMany(p => p.Runs) - .HasForeignKey(r => r.PartId); + e.HasOne(r => r.Part).WithMany(p => p.Runs).HasForeignKey(r => r.PartId); }); modelBuilder.Entity(e => { e.HasIndex(a => a.RunId).HasDatabaseName("idx_angleresults_runid"); - e.HasOne(a => a.Run) - .WithMany(r => r.AngleResults) - .HasForeignKey(a => a.RunId); + e.HasOne(a => a.Run).WithMany(r => r.AngleResults).HasForeignKey(a => a.RunId); }); } } diff --git a/OpenNest.Training/Program.cs b/OpenNest.Training/Program.cs index b288a38..6005726 100644 --- a/OpenNest.Training/Program.cs +++ b/OpenNest.Training/Program.cs @@ -1,14 +1,14 @@ -using OpenNest; -using OpenNest.Engine.BestFit; -using OpenNest.Engine.ML; -using OpenNest.Gpu; -using OpenNest.Geometry; -using OpenNest.IO; -using OpenNest.Training; using System; using System.Diagnostics; using System.IO; using System.Linq; +using OpenNest; +using OpenNest.Engine.BestFit; +using OpenNest.Engine.ML; +using OpenNest.Geometry; +using OpenNest.Gpu; +using OpenNest.IO; +using OpenNest.Training; using Color = System.Drawing.Color; // Parse arguments. @@ -83,21 +83,32 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin var sheetSuite = new[] { - new Size(96, 48), new Size(120, 48), new Size(144, 48), - new Size(96, 60), new Size(120, 60), new Size(144, 60), - new Size(96, 72), new Size(120, 72), new Size(144, 72), - new Size(48, 24), new Size(120, 10) + new Size(96, 48), + new Size(120, 48), + new Size(144, 48), + new Size(96, 60), + new Size(120, 60), + new Size(144, 60), + new Size(96, 72), + new Size(120, 72), + new Size(144, 72), + new Size(48, 24), + new Size(120, 10), }; - var dxfFiles = Directory.GetFiles(dir, "*.dxf", SearchOption.AllDirectories) + var dxfFiles = Directory + .GetFiles(dir, "*.dxf", SearchOption.AllDirectories) .Concat(Directory.GetFiles(dir, "*.dwg", SearchOption.AllDirectories)) .ToArray(); Console.WriteLine($"Found {dxfFiles.Length} CAD files"); - var resolvedDb = dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) ? dbPath : dbPath + ".db"; + var resolvedDb = dbPath.EndsWith(".db", StringComparison.OrdinalIgnoreCase) + ? dbPath + : dbPath + ".db"; Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}"); Console.WriteLine($"Sheet sizes: {sheetSuite.Length} configurations"); Console.WriteLine($"Spacing: {s:F2}"); - if (saveDir != null) Console.WriteLine($"Saving nests to: {saveDir}"); + if (saveDir != null) + Console.WriteLine($"Saving nests to: {saveDir}"); Console.WriteLine("---"); using var db = new TrainingDatabase(dbPath); @@ -133,8 +144,10 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin Drawing drawing; try { - drawing = CadImporter.ImportDrawing(file, - new CadImportOptions { DetectBends = false, Name = Path.GetFileName(file) }); + drawing = CadImporter.ImportDrawing( + file, + new CadImportOptions { DetectBends = false, Name = Path.GetFileName(file) } + ); } catch (System.Exception ex) { @@ -171,7 +184,11 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin bfSw.Stop(); Console.WriteLine($" Best-fits computed in {bfSw.ElapsedMilliseconds}ms"); - var partId = db.GetOrAddPart(Path.GetFileName(file), features, drawing.Program.ToString()); + var partId = db.GetOrAddPart( + Path.GetFileName(file), + features, + drawing.Program.ToString() + ); var partSw = Stopwatch.StartNew(); var runsThisPart = 0; var bestUtil = 0.0; @@ -215,17 +232,22 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin var engineInfo = $"{result.WinnerEngine}({result.WinnerTimeMs}ms)"; if (!string.IsNullOrEmpty(result.RunnerUpEngine)) - engineInfo += $", 2nd={result.RunnerUpEngine}({result.RunnerUpPartCount}pcs/{result.RunnerUpTimeMs}ms)"; + engineInfo += + $", 2nd={result.RunnerUpEngine}({result.RunnerUpPartCount}pcs/{result.RunnerUpTimeMs}ms)"; if (!string.IsNullOrEmpty(result.ThirdPlaceEngine)) - engineInfo += $", 3rd={result.ThirdPlaceEngine}({result.ThirdPlacePartCount}pcs/{result.ThirdPlaceTimeMs}ms)"; - Console.WriteLine($" {size.Length}x{size.Width} - {result.PartCount}pcs, {result.Utilization:P1}, {sizeSw.ElapsedMilliseconds}ms [{engineInfo}] angles={result.AngleResults.Count}"); + engineInfo += + $", 3rd={result.ThirdPlaceEngine}({result.ThirdPlacePartCount}pcs/{result.ThirdPlaceTimeMs}ms)"; + Console.WriteLine( + $" {size.Length}x{size.Width} - {result.PartCount}pcs, {result.Utilization:P1}, {sizeSw.ElapsedMilliseconds}ms [{engineInfo}] angles={result.AngleResults.Count}" + ); string savedFilePath = null; if (saveDir != null) { // Deterministic bucket (00-FF) based on filename hash uint hash = 0; - foreach (char c in partNo) hash = (hash * 31) + c; + foreach (char c in partNo) + hash = (hash * 31) + c; var bucket = (hash % 256).ToString("X2"); var partDir = Path.Combine(saveDir, bucket, partNo); @@ -242,13 +264,19 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin nestObj = new Nest(nestName) { Units = templateNest.Units, - DateCreated = DateTime.Now + DateCreated = DateTime.Now, }; - nestObj.PlateDefaults.SetFromExisting(templateNest.PlateDefaults.CreateNew()); + nestObj.PlateDefaults.SetFromExisting( + templateNest.PlateDefaults.CreateNew() + ); } else { - nestObj = new Nest(nestName) { Units = Units.Inches, DateCreated = DateTime.Now }; + nestObj = new Nest(nestName) + { + Units = Units.Inches, + DateCreated = DateTime.Now, + }; } nestObj.Drawings.Add(drawing); @@ -261,7 +289,15 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin writer.Write(savedFilePath); } - db.AddRun(partId, size.Width, size.Length, s, result, savedFilePath, result.AngleResults); + db.AddRun( + partId, + size.Width, + size.Length, + s, + result, + savedFilePath, + result.AngleResults + ); runsThisPart++; totalRuns++; } @@ -269,7 +305,9 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin BestFitCache.Invalidate(drawing); partSw.Stop(); processed++; - Console.WriteLine($" Total: {runsThisPart} runs, best={bestCount}pcs @ {bestUtil:P1}, {partSw.ElapsedMilliseconds}ms"); + Console.WriteLine( + $" Total: {runsThisPart} runs, best={bestCount}pcs @ {bestUtil:P1}, {partSw.ElapsedMilliseconds}ms" + ); } catch (Exception ex) { @@ -281,7 +319,9 @@ int RunDataCollection(string dir, string dbPath, string saveDir, double s, strin totalSw.Stop(); Console.WriteLine("---"); Console.WriteLine($"Processed: {processed} parts, {totalRuns} total runs"); - Console.WriteLine($"Skipped: {skippedExisting} (existing) + {skippedGeometry} (no geometry) + {skippedFeatures} (no features)"); + Console.WriteLine( + $"Skipped: {skippedExisting} (existing) + {skippedGeometry} (no geometry) + {skippedFeatures} (no features)" + ); Console.WriteLine($"Time: {totalSw.Elapsed:h\\:mm\\:ss}"); Console.WriteLine($"Database: {Path.GetFullPath(resolvedDb)}"); return 0; @@ -296,8 +336,12 @@ void PrintUsage() Console.Error.WriteLine(); Console.Error.WriteLine("Options:"); Console.Error.WriteLine(" --spacing Part spacing (default: 0.5)"); - Console.Error.WriteLine(" --db SQLite database path (default: OpenNestTraining.db)"); - Console.Error.WriteLine(" --save-nests Directory to save individual .nest nests for each winner"); + Console.Error.WriteLine( + " --db SQLite database path (default: OpenNestTraining.db)" + ); + Console.Error.WriteLine( + " --save-nests Directory to save individual .nest nests for each winner" + ); Console.Error.WriteLine(" --template Nest template (.nstdot) for plate defaults"); Console.Error.WriteLine(" -h, --help Show this help"); } diff --git a/OpenNest.Training/TrainingDatabase.cs b/OpenNest.Training/TrainingDatabase.cs index a89e025..837aa3d 100644 --- a/OpenNest.Training/TrainingDatabase.cs +++ b/OpenNest.Training/TrainingDatabase.cs @@ -1,12 +1,12 @@ -using Microsoft.EntityFrameworkCore; -using OpenNest.Engine.ML; -using OpenNest.IO; -using OpenNest.Training.Data; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Microsoft.EntityFrameworkCore; +using OpenNest.Engine.ML; +using OpenNest.IO; +using OpenNest.Training.Data; namespace OpenNest.Training { @@ -27,7 +27,8 @@ namespace OpenNest.Training public long GetOrAddPart(string fileName, PartFeatures features, string geometryData) { var existing = _db.Parts.FirstOrDefault(p => p.FileName == fileName); - if (existing != null) return existing.Id; + if (existing != null) + return existing.Id; var part = new TrainingPart { @@ -40,7 +41,7 @@ namespace OpenNest.Training PerimeterToAreaRatio = features.PerimeterToAreaRatio, VertexCount = features.VertexCount, Bitmask = features.Bitmask, - GeometryData = geometryData + GeometryData = geometryData, }; _db.Parts.Add(part); @@ -51,10 +52,11 @@ namespace OpenNest.Training public bool HasRun(string fileName, double sheetWidth, double sheetHeight, double spacing) { return _db.Runs.Any(r => - r.Part.FileName == fileName && - r.SheetWidth == sheetWidth && - r.SheetHeight == sheetHeight && - r.Spacing == spacing); + r.Part.FileName == fileName + && r.SheetWidth == sheetWidth + && r.SheetHeight == sheetHeight + && r.Spacing == spacing + ); } public int RunCount(string fileName) @@ -62,7 +64,15 @@ namespace OpenNest.Training return _db.Runs.Count(r => r.Part.FileName == fileName); } - public void AddRun(long partId, double w, double h, double s, BruteForceResult result, string filePath, List angleResults = null) + public void AddRun( + long partId, + double w, + double h, + double s, + BruteForceResult result, + string filePath, + List angleResults = null + ) { var run = new TrainingRun { @@ -82,7 +92,7 @@ namespace OpenNest.Training RunnerUpTimeMs = result.RunnerUpTimeMs, ThirdPlaceEngine = result.ThirdPlaceEngine ?? "", ThirdPlacePartCount = result.ThirdPlacePartCount, - ThirdPlaceTimeMs = result.ThirdPlaceTimeMs + ThirdPlaceTimeMs = result.ThirdPlaceTimeMs, }; _db.Runs.Add(run); @@ -91,13 +101,15 @@ namespace OpenNest.Training { foreach (var ar in angleResults) { - _db.AngleResults.Add(new Data.TrainingAngleResult - { - Run = run, - AngleDeg = ar.AngleDeg, - Direction = ar.Direction.ToString(), - PartCount = ar.PartCount - }); + _db.AngleResults.Add( + new Data.TrainingAngleResult + { + Run = run, + AngleDeg = ar.AngleDeg, + Direction = ar.Direction.ToString(), + PartCount = ar.PartCount, + } + ); } } @@ -106,12 +118,13 @@ namespace OpenNest.Training public int BackfillPerimeterToAreaRatio() { - var partsToFix = _db.Parts - .Where(p => p.PerimeterToAreaRatio == 0) + var partsToFix = _db + .Parts.Where(p => p.PerimeterToAreaRatio == 0) .Select(p => new { p.Id, p.GeometryData }) .ToList(); - if (partsToFix.Count == 0) return 0; + if (partsToFix.Count == 0) + return 0; var updated = 0; foreach (var item in partsToFix) @@ -126,7 +139,8 @@ namespace OpenNest.Training drawing.UpdateArea(); var features = FeatureExtractor.Extract(drawing); - if (features == null) continue; + if (features == null) + continue; var part = _db.Parts.Find(item.Id); part.PerimeterToAreaRatio = features.PerimeterToAreaRatio; @@ -170,7 +184,8 @@ namespace OpenNest.Training try { - _db.Database.ExecuteSqlRaw(@" + _db.Database.ExecuteSqlRaw( + @" CREATE TABLE IF NOT EXISTS AngleResults ( Id INTEGER PRIMARY KEY AUTOINCREMENT, RunId INTEGER NOT NULL, @@ -178,9 +193,11 @@ namespace OpenNest.Training Direction TEXT NOT NULL, PartCount INTEGER NOT NULL, FOREIGN KEY (RunId) REFERENCES Runs(Id) - )"); + )" + ); _db.Database.ExecuteSqlRaw( - "CREATE INDEX IF NOT EXISTS idx_angleresults_runid ON AngleResults (RunId)"); + "CREATE INDEX IF NOT EXISTS idx_angleresults_runid ON AngleResults (RunId)" + ); } catch { diff --git a/OpenNest/Actions/Action.cs b/OpenNest/Actions/Action.cs index 16bbcdd..52a9ede 100644 --- a/OpenNest/Actions/Action.cs +++ b/OpenNest/Actions/Action.cs @@ -1,6 +1,6 @@ -using OpenNest.Controls; +using System.Drawing; +using OpenNest.Controls; using OpenNest.Geometry; -using System.Drawing; namespace OpenNest.Actions { diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index ebd51b1..baabd13 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -1,10 +1,10 @@ -using OpenNest.Controls; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Actions { @@ -17,9 +17,7 @@ namespace OpenNest.Actions private double lastScale; public ActionClone(PlateView plateView, Drawing drawing) - : this(plateView, new List { new Part(drawing) }) - { - } + : this(plateView, new List { new Part(drawing) }) { } public ActionClone(PlateView plateView, List partsToClone) : base(plateView) @@ -141,9 +139,7 @@ namespace OpenNest.Actions plateView.Invalidate(); } - public override void CancelAction() - { - } + public override void CancelAction() { } public override bool IsBusy() { @@ -156,14 +152,30 @@ namespace OpenNest.Actions { var movingParts = parts.Select(p => p.BasePart).ToList(); - PushDirection hDir, vDir; + PushDirection hDir, + vDir; switch (plateView.Plate.Quadrant) { - case 1: hDir = PushDirection.Left; vDir = PushDirection.Down; break; - case 2: hDir = PushDirection.Right; vDir = PushDirection.Down; break; - case 3: hDir = PushDirection.Right; vDir = PushDirection.Up; break; - case 4: hDir = PushDirection.Left; vDir = PushDirection.Up; break; - default: hDir = PushDirection.Left; vDir = PushDirection.Down; break; + case 1: + hDir = PushDirection.Left; + vDir = PushDirection.Down; + break; + case 2: + hDir = PushDirection.Right; + vDir = PushDirection.Down; + break; + case 3: + hDir = PushDirection.Right; + vDir = PushDirection.Up; + break; + case 4: + hDir = PushDirection.Left; + vDir = PushDirection.Up; + break; + default: + hDir = PushDirection.Left; + vDir = PushDirection.Down; + break; } Compactor.PushBoundingBox(movingParts, plateView.Plate, hDir); diff --git a/OpenNest/Actions/ActionCutOff.cs b/OpenNest/Actions/ActionCutOff.cs index 95cf12a..50708cb 100644 --- a/OpenNest/Actions/ActionCutOff.cs +++ b/OpenNest/Actions/ActionCutOff.cs @@ -1,11 +1,11 @@ -using OpenNest.CNC; -using OpenNest.Controls; -using OpenNest.Geometry; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; +using OpenNest.CNC; +using OpenNest.Controls; +using OpenNest.Geometry; namespace OpenNest.Actions { @@ -75,9 +75,8 @@ namespace OpenNest.Actions { if (e.KeyCode == Keys.Space) { - lockedAxis = lockedAxis == CutOffAxis.Vertical - ? CutOffAxis.Horizontal - : CutOffAxis.Vertical; + lockedAxis = + lockedAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical; if (previewCutOff != null) { @@ -103,13 +102,15 @@ namespace OpenNest.Actions using var pen = new Pen(Color.FromArgb(128, 64, 64, 64), 1.5f / plateView.ViewScale) { - DashStyle = DashStyle.Dash + DashStyle = DashStyle.Dash, }; for (var i = 0; i < program.Codes.Count - 1; i += 2) { - if (program.Codes[i] is RapidMove rapid && - program.Codes[i + 1] is LinearMove linear) + if ( + program.Codes[i] is RapidMove rapid + && program.Codes[i + 1] is LinearMove linear + ) { var pt1 = plateView.PointWorldToGraph(rapid.EndPoint); var pt2 = plateView.PointWorldToGraph(linear.EndPoint); diff --git a/OpenNest/Actions/ActionFillArea.cs b/OpenNest/Actions/ActionFillArea.cs index 8307184..ec9fcf1 100644 --- a/OpenNest/Actions/ActionFillArea.cs +++ b/OpenNest/Actions/ActionFillArea.cs @@ -1,10 +1,10 @@ -using OpenNest.Controls; using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using OpenNest.Controls; namespace OpenNest.Actions { @@ -17,13 +17,15 @@ namespace OpenNest.Actions private Action> onFillComplete; public ActionFillArea(PlateView plateView, Drawing drawing) - : this(plateView, drawing, null, null, null) - { - } + : this(plateView, drawing, null, null, null) { } - public ActionFillArea(PlateView plateView, Drawing drawing, - IProgress progress, CancellationTokenSource cts, - Action> onFillComplete) + public ActionFillArea( + PlateView plateView, + Drawing drawing, + IProgress progress, + CancellationTokenSource cts, + Action> onFillComplete + ) : base(plateView) { plateView.PreviewKeyDown += plateView_PreviewKeyDown; @@ -49,8 +51,13 @@ namespace OpenNest.Actions { var engine = NestEngineRegistry.Create(plateView.Plate); var parts = await Task.Run(() => - engine.Fill(new NestItem { Drawing = drawing }, - SelectedArea, progress, cts.Token)); + engine.Fill( + new NestItem { Drawing = drawing }, + SelectedArea, + progress, + cts.Token + ) + ); onFillComplete?.Invoke(parts); } diff --git a/OpenNest/Actions/ActionLeadIn.cs b/OpenNest/Actions/ActionLeadIn.cs index c5260ff..c327b45 100644 --- a/OpenNest/Actions/ActionLeadIn.cs +++ b/OpenNest/Actions/ActionLeadIn.cs @@ -1,22 +1,27 @@ -using OpenNest.CNC.CuttingStrategy; -using OpenNest.Controls; -using OpenNest.Converters; -using OpenNest.Forms; -using OpenNest.Geometry; -using OpenNest.Math; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Controls; +using OpenNest.Converters; +using OpenNest.Forms; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Actions { [DisplayName("Place Lead-in")] public class ActionLeadIn : Action { - private enum SnapType { None, Endpoint, Midpoint } + private enum SnapType + { + None, + Endpoint, + Midpoint, + } private const double SnapCapturePixels = 10.0; @@ -34,7 +39,9 @@ namespace OpenNest.Actions private ShapeInfo lockedContour; private ContextMenuStrip contextMenu; private CuttingPanel cuttingPanel; - private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180)); + private static readonly Brush grayOverlay = new SolidBrush( + Color.FromArgb(160, 180, 180, 180) + ); private static readonly Pen highlightPen = new Pen(Color.Cyan, 2.5f); private static readonly Pen lockedPen = new Pen(Color.Yellow, 3.0f); @@ -106,7 +113,9 @@ namespace OpenNest.Actions var saved = CuttingParametersSerializer.Deserialize(json); cuttingPanel.LoadFromParameters(saved); } - catch { /* use defaults */ } + catch + { /* use defaults */ + } } } @@ -131,7 +140,9 @@ namespace OpenNest.Actions private CuttingParameters GetCurrentParameters() { - return cuttingPanel?.BuildParameters() ?? plateView.Plate?.CuttingParameters ?? new CuttingParameters(); + return cuttingPanel?.BuildParameters() + ?? plateView.Plate?.CuttingParameters + ?? new CuttingParameters(); } private void SaveParameters() @@ -163,8 +174,10 @@ namespace OpenNest.Actions // Transform world point into program-local space by subtracting the // part's location. The contour shapes are already in the program's // rotated coordinate system, so no additional un-rotation is needed. - var localPt = new Vector(worldPt.X - selectedPart.Location.X, - worldPt.Y - selectedPart.Location.Y); + var localPt = new Vector( + worldPt.X - selectedPart.Location.X, + worldPt.Y - selectedPart.Location.Y + ); // Find closest contour and point var bestDist = double.MaxValue; @@ -173,9 +186,8 @@ namespace OpenNest.Actions hoveredContour = null; // When a contour is locked, only snap within that contour - var searchContours = lockedContour != null - ? new List { lockedContour } - : contours; + var searchContours = + lockedContour != null ? new List { lockedContour } : contours; foreach (var info in searchContours) { @@ -188,7 +200,12 @@ namespace OpenNest.Actions snapPoint = closest; snapEntity = entity; snapContourType = info.ContourType; - snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType, info.Winding); + snapNormal = ContourCuttingStrategy.ComputeNormal( + closest, + entity, + info.ContourType, + info.Winding + ); hasSnap = true; hoveredContour = info; } @@ -353,9 +370,11 @@ namespace OpenNest.Actions private LeadIn ClampLeadInForCircle(LeadIn leadIn, CuttingParameters parameters) { - if (snapContourType != ContourType.ArcCircle + if ( + snapContourType != ContourType.ArcCircle || !(snapEntity is Circle snapCircle) - || parameters.PierceClearance <= 0) + || parameters.PierceClearance <= 0 + ) return leadIn; var pierceCheck = leadIn.GetPiercePoint(snapPoint, snapNormal); @@ -412,7 +431,12 @@ namespace OpenNest.Actions { snapPoint = bestPoint; snapEntity = bestEntity; - snapNormal = ContourCuttingStrategy.ComputeNormal(bestPoint, bestEntity, snapContourType, hoveredContour.Winding); + snapNormal = ContourCuttingStrategy.ComputeNormal( + bestPoint, + bestEntity, + snapContourType, + hoveredContour.Winding + ); activeSnapType = bestType; } @@ -472,7 +496,8 @@ namespace OpenNest.Actions cleanProgram = selectedPart.Program; } - var entities = ConvertProgram.ToGeometry(cleanProgram) + var entities = ConvertProgram + .ToGeometry(cleanProgram) .Where(e => e.Layer == SpecialLayers.Cut) .ToList(); @@ -483,23 +508,27 @@ namespace OpenNest.Actions // Perimeter is always External if (profile.Perimeter != null) { - contours.Add(new ShapeInfo - { - Shape = profile.Perimeter, - ContourType = ContourType.External, - Winding = ContourCuttingStrategy.DetermineWinding(profile.Perimeter) - }); + contours.Add( + new ShapeInfo + { + Shape = profile.Perimeter, + ContourType = ContourType.External, + Winding = ContourCuttingStrategy.DetermineWinding(profile.Perimeter), + } + ); } // Cutouts foreach (var cutout in profile.Cutouts) { - contours.Add(new ShapeInfo - { - Shape = cutout, - ContourType = ContourCuttingStrategy.DetectContourType(cutout), - Winding = ContourCuttingStrategy.DetermineWinding(cutout) - }); + contours.Add( + new ShapeInfo + { + Shape = cutout, + ContourType = ContourCuttingStrategy.DetectContourType(cutout), + Winding = ContourCuttingStrategy.DetermineWinding(cutout), + } + ); } } @@ -601,7 +630,7 @@ namespace OpenNest.Actions new PointF(pt.X, pt.Y - size), new PointF(pt.X + size, pt.Y), new PointF(pt.X, pt.Y + size), - new PointF(pt.X - size, pt.Y) + new PointF(pt.X - size, pt.Y), }; g.FillPolygon(Brushes.Red, points); } @@ -612,7 +641,7 @@ namespace OpenNest.Actions { new PointF(pt.X, pt.Y - size), new PointF(pt.X + size, pt.Y + size), - new PointF(pt.X - size, pt.Y + size) + new PointF(pt.X - size, pt.Y + size), }; g.FillPolygon(Brushes.Red, points); } @@ -622,8 +651,10 @@ namespace OpenNest.Actions { // The contours are already in rotated local space (we rotated the program // before building the profile), so just add the part location offset - return new Vector(localPt.X + selectedPart.Location.X, - localPt.Y + selectedPart.Location.Y); + return new Vector( + localPt.X + selectedPart.Location.X, + localPt.Y + selectedPart.Location.Y + ); } private static LeadIn SelectLeadIn(CuttingParameters parameters, ContourType contourType) @@ -632,7 +663,7 @@ namespace OpenNest.Actions { ContourType.ArcCircle => parameters.ArcCircleLeadIn ?? parameters.InternalLeadIn, ContourType.Internal => parameters.InternalLeadIn, - _ => parameters.ExternalLeadIn + _ => parameters.ExternalLeadIn, }; } diff --git a/OpenNest/Actions/ActionSelect.cs b/OpenNest/Actions/ActionSelect.cs index 4a789b8..1446d51 100644 --- a/OpenNest/Actions/ActionSelect.cs +++ b/OpenNest/Actions/ActionSelect.cs @@ -1,8 +1,8 @@ -using OpenNest.Controls; -using OpenNest.Geometry; -using System.ComponentModel; +using System.ComponentModel; using System.Drawing; using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Geometry; namespace OpenNest.Actions { @@ -150,11 +150,7 @@ namespace OpenNest.Actions e.Graphics.FillRectangle(fillBrush, rect); - e.Graphics.DrawRectangle(borderPen, - rect.X, - rect.Y, - rect.Width, - rect.Height); + e.Graphics.DrawRectangle(borderPen, rect.X, rect.Y, rect.Width, rect.Height); } private bool SelectPartAtCurrentPoint() @@ -168,7 +164,10 @@ namespace OpenNest.Actions return false; } - if (Control.ModifierKeys != Keys.Control && plateView.SelectedParts.Contains(part) == false) + if ( + Control.ModifierKeys != Keys.Control + && plateView.SelectedParts.Contains(part) == false + ) plateView.DeselectAll(); if (plateView.SelectedParts.Contains(part) == false) @@ -216,18 +215,13 @@ namespace OpenNest.Actions public SelectionType SelectionType { - get - { - return Point1.X < Point2.X - ? SelectionType.Contains - : SelectionType.Intersect; - } + get { return Point1.X < Point2.X ? SelectionType.Contains : SelectionType.Intersect; } } public enum Status { SetFirstPoint, - SetSecondPoint + SetSecondPoint, } } } diff --git a/OpenNest/Actions/ActionSelectArea.cs b/OpenNest/Actions/ActionSelectArea.cs index 36735b7..6e8f417 100644 --- a/OpenNest/Actions/ActionSelectArea.cs +++ b/OpenNest/Actions/ActionSelectArea.cs @@ -1,9 +1,9 @@ -using OpenNest.Controls; -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Geometry; namespace OpenNest.Actions { @@ -34,7 +34,7 @@ namespace OpenNest.Actions stringFormat = new StringFormat { Alignment = StringAlignment.Center, - LineAlignment = StringAlignment.Center + LineAlignment = StringAlignment.Center, }; SelectedArea = Box.Empty; @@ -93,15 +93,17 @@ namespace OpenNest.Actions var location = plateView.PointWorldToGraph(SelectedArea.Location); var size = new SizeF( plateView.LengthWorldToGui(SelectedArea.Length), - plateView.LengthWorldToGui(SelectedArea.Width)); + plateView.LengthWorldToGui(SelectedArea.Width) + ); - var rect = new System.Drawing.RectangleF(location.X, location.Y - size.Height, size.Width, size.Height); + var rect = new System.Drawing.RectangleF( + location.X, + location.Y - size.Height, + size.Width, + size.Height + ); - e.Graphics.DrawRectangle(pen, - rect.X, - rect.Y, - rect.Width, - rect.Height); + e.Graphics.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); e.Graphics.FillRectangle(brush, rect); @@ -110,7 +112,8 @@ namespace OpenNest.Actions font, Brushes.Green, rect, - stringFormat); + stringFormat + ); } private void plateView_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e) diff --git a/OpenNest/Actions/ActionSetSequence.cs b/OpenNest/Actions/ActionSetSequence.cs index eaf87d0..73e37c5 100644 --- a/OpenNest/Actions/ActionSetSequence.cs +++ b/OpenNest/Actions/ActionSetSequence.cs @@ -1,12 +1,12 @@ -using OpenNest.Controls; -using OpenNest.Converters; -using OpenNest.Forms; -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Linq; using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Converters; +using OpenNest.Forms; +using OpenNest.Geometry; namespace OpenNest.Actions { @@ -26,7 +26,10 @@ namespace OpenNest.Actions get { return sequenceNumber; } set { - if (value <= SequenceForm.numericUpDown1.Maximum && value >= SequenceForm.numericUpDown1.Minimum) + if ( + value <= SequenceForm.numericUpDown1.Maximum + && value >= SequenceForm.numericUpDown1.Minimum + ) { sequenceNumber = value; SequenceForm.numericUpDown1.Value = sequenceNumber; @@ -38,7 +41,13 @@ namespace OpenNest.Actions : base(plateView) { SequenceForm = new Forms.SequenceForm(); - SequenceForm.numericUpDown1.DataBindings.Add("Value", this, "SequenceNumber", false, DataSourceUpdateMode.OnPropertyChanged); + SequenceForm.numericUpDown1.DataBindings.Add( + "Value", + this, + "SequenceNumber", + false, + DataSourceUpdateMode.OnPropertyChanged + ); SequenceForm.numericUpDown1.Maximum = plateView.Plate.Parts.Count; SequenceForm.Owner = Application.OpenForms[0]; SequenceForm.Show(); @@ -50,7 +59,10 @@ namespace OpenNest.Actions foreach (var part in plateView.Plate.Parts) { - var entities = ConvertProgram.ToGeometry(part.Program).Where(e => e.Layer == SpecialLayers.Cut).ToList(); + var entities = ConvertProgram + .ToGeometry(part.Program) + .Where(e => e.Layer == SpecialLayers.Cut) + .ToList(); entities.ForEach(entity => entity.Offset(part.Location)); var shapes = ShapeBuilder.GetShapes(entities); var shape = new Shape(); @@ -97,7 +109,8 @@ namespace OpenNest.Actions private void plateView_Paint(object sender, PaintEventArgs e) { - if (ClosestShape == null) return; + if (ClosestShape == null) + return; var path = ClosestShape.GetGraphicsPath(); path.Transform(plateView.Matrix); diff --git a/OpenNest/Actions/ActionZoomWindow.cs b/OpenNest/Actions/ActionZoomWindow.cs index a279f15..e2bf210 100644 --- a/OpenNest/Actions/ActionZoomWindow.cs +++ b/OpenNest/Actions/ActionZoomWindow.cs @@ -1,8 +1,8 @@ -using OpenNest.Controls; -using OpenNest.Geometry; -using System.ComponentModel; +using System.ComponentModel; using System.Drawing; using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Geometry; namespace OpenNest.Actions { @@ -121,24 +121,35 @@ namespace OpenNest.Actions e.Graphics.FillRectangle(fillBrush, rect); - e.Graphics.DrawRectangle(borderPen, - rect.X, - rect.Y, - rect.Width, - rect.Height); + e.Graphics.DrawRectangle(borderPen, rect.X, rect.Y, rect.Width, rect.Height); var centerX = rect.X + rect.Width * 0.5f; var centerY = rect.Y + rect.Height * 0.5f; const float halfWidth = 10; - e.Graphics.DrawLine(borderPen, centerX, centerY - halfWidth, centerX, centerY + halfWidth); - e.Graphics.DrawLine(borderPen, centerX - halfWidth, centerY, centerX + halfWidth, centerY); + e.Graphics.DrawLine( + borderPen, + centerX, + centerY - halfWidth, + centerX, + centerY + halfWidth + ); + e.Graphics.DrawLine( + borderPen, + centerX - halfWidth, + centerY, + centerX + halfWidth, + centerY + ); } private void ZoomWindow() { - double x, y, w, h; + double x, + y, + w, + h; if (Point1.X < Point2.X) { @@ -171,7 +182,7 @@ namespace OpenNest.Actions public enum Status { SetFirstPoint, - SetSecondPoint + SetSecondPoint, } } } diff --git a/OpenNest/ArchUnits.cs b/OpenNest/ArchUnits.cs index 30304ad..5d2a6da 100644 --- a/OpenNest/ArchUnits.cs +++ b/OpenNest/ArchUnits.cs @@ -1,16 +1,17 @@ -using OpenNest.Math; using System; using System.Drawing; using System.Text; using System.Text.RegularExpressions; using System.Windows.Forms; +using OpenNest.Math; namespace OpenNest { public static class ArchUnits { - private static readonly Regex UnitRegex = - new Regex("^(?\\d+\\.?\\d*\\s*')?\\s*(?\\d+\\.?\\d*\\s*\")?$"); + private static readonly Regex UnitRegex = new Regex( + "^(?\\d+\\.?\\d*\\s*')?\\s*(?\\d+\\.?\\d*\\s*\")?$" + ); public static double ParseToInches(string input) { diff --git a/OpenNest/ColorScheme.cs b/OpenNest/ColorScheme.cs index 6f5e62d..02385d7 100644 --- a/OpenNest/ColorScheme.cs +++ b/OpenNest/ColorScheme.cs @@ -113,7 +113,7 @@ namespace OpenNest RapidPen = new Pen(value) { DashPattern = new float[] { 10, 10 }, - DashCap = DashCap.Flat + DashCap = DashCap.Flat, }; } } @@ -145,7 +145,7 @@ namespace OpenNest EdgeSpacingPen = new Pen(value) { DashPattern = new float[] { 3, 3 }, - DashCap = DashCap.Flat + DashCap = DashCap.Flat, }; } } diff --git a/OpenNest/ColorSchemeRegistry.cs b/OpenNest/ColorSchemeRegistry.cs index 841080b..1e9083e 100644 --- a/OpenNest/ColorSchemeRegistry.cs +++ b/OpenNest/ColorSchemeRegistry.cs @@ -1,23 +1,24 @@ -using OpenNest.Forms; -using OpenNest.Properties; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms; +using OpenNest.Forms; +using OpenNest.Properties; namespace OpenNest { public static class ColorSchemeRegistry { - private static readonly Dictionary builtIns = - new(StringComparer.OrdinalIgnoreCase) - { - ["Classic"] = BuildClassic(), - ["Pastel"] = BuildPastel(), - ["Dark"] = BuildDark() - }; + private static readonly Dictionary builtIns = new( + StringComparer.OrdinalIgnoreCase + ) + { + ["Classic"] = BuildClassic(), + ["Pastel"] = BuildPastel(), + ["Dark"] = BuildDark(), + }; private static List diskCache; @@ -37,8 +38,9 @@ namespace OpenNest if (string.IsNullOrWhiteSpace(name)) return builtIns["Classic"]; - var hit = AllSchemes.FirstOrDefault( - s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase)); + var hit = AllSchemes.FirstOrDefault(s => + string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase) + ); return hit ?? builtIns["Classic"]; } @@ -108,96 +110,119 @@ namespace OpenNest } } - private static ColorScheme BuildClassic() => new ColorScheme - { - Name = "Classic", - BackgroundColor = Color.DarkGray, - LayoutOutlineColor = Color.Gray, - LayoutFillColor = Color.WhiteSmoke, - BoundingBoxColor = Color.FromArgb(128, 128, 255), - RapidColor = Color.DodgerBlue, - OriginColor = Color.Gray, - EdgeSpacingColor = Color.FromArgb(180, 180, 180), - PreviewPartColor = Color.FromArgb(255, 140, 0), - PartColors = new[] + private static ColorScheme BuildClassic() => + new ColorScheme { - Color.FromArgb(205, 92, 92), - Color.FromArgb(148, 103, 189), - Color.FromArgb(75, 180, 175), - Color.FromArgb(210, 190, 75), - Color.FromArgb(190, 85, 175), - Color.FromArgb(185, 115, 85), - Color.FromArgb(120, 100, 190), - Color.FromArgb(200, 100, 140), - Color.FromArgb(80, 175, 155), - Color.FromArgb(195, 160, 85), - Color.FromArgb(175, 95, 160), - Color.FromArgb(215, 130, 130), - } - }; + Name = "Classic", + BackgroundColor = Color.DarkGray, + LayoutOutlineColor = Color.Gray, + LayoutFillColor = Color.WhiteSmoke, + BoundingBoxColor = Color.FromArgb(128, 128, 255), + RapidColor = Color.DodgerBlue, + OriginColor = Color.Gray, + EdgeSpacingColor = Color.FromArgb(180, 180, 180), + PreviewPartColor = Color.FromArgb(255, 140, 0), + PartColors = new[] + { + Color.FromArgb(205, 92, 92), + Color.FromArgb(148, 103, 189), + Color.FromArgb(75, 180, 175), + Color.FromArgb(210, 190, 75), + Color.FromArgb(190, 85, 175), + Color.FromArgb(185, 115, 85), + Color.FromArgb(120, 100, 190), + Color.FromArgb(200, 100, 140), + Color.FromArgb(80, 175, 155), + Color.FromArgb(195, 160, 85), + Color.FromArgb(175, 95, 160), + Color.FromArgb(215, 130, 130), + }, + }; - private static ColorScheme BuildPastel() => new ColorScheme - { - Name = "Pastel", - BackgroundColor = Color.FromArgb(70, 75, 85), - LayoutOutlineColor = Color.FromArgb(180, 180, 190), - LayoutFillColor = Color.FromArgb(245, 245, 248), - BoundingBoxColor = Color.FromArgb(128, 128, 255), - RapidColor = Color.DodgerBlue, - OriginColor = Color.FromArgb(160, 160, 160), - EdgeSpacingColor = Color.FromArgb(200, 200, 210), - PreviewPartColor = Color.FromArgb(255, 140, 0), - PartColors = new[] + private static ColorScheme BuildPastel() => + new ColorScheme { - Color.FromArgb(122, 179, 209), Color.FromArgb(254, 229, 174), - Color.FromArgb(143, 177, 229), Color.FromArgb(167, 172, 227), - Color.FromArgb(216, 249, 195), Color.FromArgb(209, 168, 216), - Color.FromArgb(222, 157, 190), Color.FromArgb(176, 255, 240), - Color.FromArgb(235, 205, 153), Color.FromArgb(177, 225, 180), - Color.FromArgb(125, 202, 241), Color.FromArgb(187, 206, 151), - Color.FromArgb(251, 175, 190), Color.FromArgb(129, 226, 227), - Color.FromArgb(255, 253, 207), Color.FromArgb(235, 205, 255), - Color.FromArgb(255, 197, 168), Color.FromArgb(116, 213, 234), - Color.FromArgb(190, 169, 122), Color.FromArgb(213, 159, 135), - Color.FromArgb(124, 184, 155), Color.FromArgb(255, 189, 214), - Color.FromArgb(146, 222, 255), Color.FromArgb(177, 173, 125), - Color.FromArgb(177, 166, 202), Color.FromArgb(197, 208, 255), - Color.FromArgb(255, 209, 243), Color.FromArgb(210, 255, 237), - Color.FromArgb(255, 237, 204), Color.FromArgb(167, 233, 255), - Color.FromArgb(182, 220, 255), Color.FromArgb(159, 177, 142), - Color.FromArgb(190, 248, 255), Color.FromArgb(187, 169, 136), - Color.FromArgb(199, 162, 168), Color.FromArgb(250, 255, 239), - Color.FromArgb(222, 233, 255), Color.FromArgb(255, 234, 225), - Color.FromArgb(240, 249, 255), Color.FromArgb(152, 176, 176), - } - }; + Name = "Pastel", + BackgroundColor = Color.FromArgb(70, 75, 85), + LayoutOutlineColor = Color.FromArgb(180, 180, 190), + LayoutFillColor = Color.FromArgb(245, 245, 248), + BoundingBoxColor = Color.FromArgb(128, 128, 255), + RapidColor = Color.DodgerBlue, + OriginColor = Color.FromArgb(160, 160, 160), + EdgeSpacingColor = Color.FromArgb(200, 200, 210), + PreviewPartColor = Color.FromArgb(255, 140, 0), + PartColors = new[] + { + Color.FromArgb(122, 179, 209), + Color.FromArgb(254, 229, 174), + Color.FromArgb(143, 177, 229), + Color.FromArgb(167, 172, 227), + Color.FromArgb(216, 249, 195), + Color.FromArgb(209, 168, 216), + Color.FromArgb(222, 157, 190), + Color.FromArgb(176, 255, 240), + Color.FromArgb(235, 205, 153), + Color.FromArgb(177, 225, 180), + Color.FromArgb(125, 202, 241), + Color.FromArgb(187, 206, 151), + Color.FromArgb(251, 175, 190), + Color.FromArgb(129, 226, 227), + Color.FromArgb(255, 253, 207), + Color.FromArgb(235, 205, 255), + Color.FromArgb(255, 197, 168), + Color.FromArgb(116, 213, 234), + Color.FromArgb(190, 169, 122), + Color.FromArgb(213, 159, 135), + Color.FromArgb(124, 184, 155), + Color.FromArgb(255, 189, 214), + Color.FromArgb(146, 222, 255), + Color.FromArgb(177, 173, 125), + Color.FromArgb(177, 166, 202), + Color.FromArgb(197, 208, 255), + Color.FromArgb(255, 209, 243), + Color.FromArgb(210, 255, 237), + Color.FromArgb(255, 237, 204), + Color.FromArgb(167, 233, 255), + Color.FromArgb(182, 220, 255), + Color.FromArgb(159, 177, 142), + Color.FromArgb(190, 248, 255), + Color.FromArgb(187, 169, 136), + Color.FromArgb(199, 162, 168), + Color.FromArgb(250, 255, 239), + Color.FromArgb(222, 233, 255), + Color.FromArgb(255, 234, 225), + Color.FromArgb(240, 249, 255), + Color.FromArgb(152, 176, 176), + }, + }; - private static ColorScheme BuildDark() => new ColorScheme - { - Name = "Dark", - BackgroundColor = Color.FromArgb(30, 30, 34), - LayoutOutlineColor = Color.FromArgb(90, 90, 95), - LayoutFillColor = Color.FromArgb(50, 50, 55), - BoundingBoxColor = Color.FromArgb(100, 160, 220), - RapidColor = Color.FromArgb(255, 200, 50), - OriginColor = Color.FromArgb(120, 120, 130), - EdgeSpacingColor = Color.FromArgb(90, 90, 100), - PreviewPartColor = Color.FromArgb(255, 170, 60), - PartColors = new[] + private static ColorScheme BuildDark() => + new ColorScheme { - Color.FromArgb(255, 85, 85), // Neon Red - Color.FromArgb(80, 220, 255), // Electric Cyan - Color.FromArgb(255, 200, 50), // Amber - Color.FromArgb(130, 255, 130), // Lime Green - Color.FromArgb(255, 130, 220), // Hot Pink - Color.FromArgb(255, 165, 70), // Tangerine - Color.FromArgb(100, 180, 255), // Sky Blue - Color.FromArgb(200, 160, 255), // Lavender - Color.FromArgb(50, 230, 180), // Mint - Color.FromArgb(255, 255, 100), // Lemon - Color.FromArgb(255, 120, 120), // Salmon - Color.FromArgb(140, 230, 255), // Ice Blue - } - }; + Name = "Dark", + BackgroundColor = Color.FromArgb(30, 30, 34), + LayoutOutlineColor = Color.FromArgb(90, 90, 95), + LayoutFillColor = Color.FromArgb(50, 50, 55), + BoundingBoxColor = Color.FromArgb(100, 160, 220), + RapidColor = Color.FromArgb(255, 200, 50), + OriginColor = Color.FromArgb(120, 120, 130), + EdgeSpacingColor = Color.FromArgb(90, 90, 100), + PreviewPartColor = Color.FromArgb(255, 170, 60), + PartColors = new[] + { + Color.FromArgb(255, 85, 85), // Neon Red + Color.FromArgb(80, 220, 255), // Electric Cyan + Color.FromArgb(255, 200, 50), // Amber + Color.FromArgb(130, 255, 130), // Lime Green + Color.FromArgb(255, 130, 220), // Hot Pink + Color.FromArgb(255, 165, 70), // Tangerine + Color.FromArgb(100, 180, 255), // Sky Blue + Color.FromArgb(200, 160, 255), // Lavender + Color.FromArgb(50, 230, 180), // Mint + Color.FromArgb(255, 255, 100), // Lemon + Color.FromArgb(255, 120, 120), // Salmon + Color.FromArgb(140, 230, 255), // Ice Blue + }, + }; } } diff --git a/OpenNest/ColorSchemeSerializer.cs b/OpenNest/ColorSchemeSerializer.cs index 2c7b7ae..ea8de67 100644 --- a/OpenNest/ColorSchemeSerializer.cs +++ b/OpenNest/ColorSchemeSerializer.cs @@ -10,7 +10,7 @@ namespace OpenNest private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; public static string Serialize(ColorScheme scheme) @@ -26,14 +26,15 @@ namespace OpenNest OriginColor = ToHex(scheme.OriginColor), EdgeSpacingColor = ToHex(scheme.EdgeSpacingColor), PreviewPartColor = ToHex(scheme.PreviewPartColor), - PartColors = scheme.PartColors.Select(ToHex).ToArray() + PartColors = scheme.PartColors.Select(ToHex).ToArray(), }; return JsonSerializer.Serialize(dto, JsonOptions); } public static ColorScheme Deserialize(string json) { - var dto = JsonSerializer.Deserialize(json, JsonOptions) + var dto = + JsonSerializer.Deserialize(json, JsonOptions) ?? throw new JsonException("ColorScheme JSON was null"); return new ColorScheme @@ -47,7 +48,7 @@ namespace OpenNest OriginColor = FromHex(dto.OriginColor), EdgeSpacingColor = FromHex(dto.EdgeSpacingColor), PreviewPartColor = FromHex(dto.PreviewPartColor), - PartColors = (dto.PartColors ?? new string[0]).Select(FromHex).ToArray() + PartColors = (dto.PartColors ?? new string[0]).Select(FromHex).ToArray(), }; } @@ -61,9 +62,21 @@ namespace OpenNest var h = hex.TrimStart('#'); if (h.Length < 6) return Color.Black; - var r = byte.Parse(h.Substring(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - var g = byte.Parse(h.Substring(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); - var b = byte.Parse(h.Substring(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + var r = byte.Parse( + h.Substring(0, 2), + NumberStyles.HexNumber, + CultureInfo.InvariantCulture + ); + var g = byte.Parse( + h.Substring(2, 2), + NumberStyles.HexNumber, + CultureInfo.InvariantCulture + ); + var b = byte.Parse( + h.Substring(4, 2), + NumberStyles.HexNumber, + CultureInfo.InvariantCulture + ); return Color.FromArgb(r, g, b); } diff --git a/OpenNest/Controls/ActionManager.cs b/OpenNest/Controls/ActionManager.cs index b7f8dde..cda7afa 100644 --- a/OpenNest/Controls/ActionManager.cs +++ b/OpenNest/Controls/ActionManager.cs @@ -25,7 +25,10 @@ namespace OpenNest.Controls if (currentAction != null) { - if (type == typeof(Actions.ActionSelect) && !(currentAction is Actions.ActionSelect)) + if ( + type == typeof(Actions.ActionSelect) + && !(currentAction is Actions.ActionSelect) + ) previousAction = currentAction; else previousAction = null; diff --git a/OpenNest/Controls/BestFitCell.cs b/OpenNest/Controls/BestFitCell.cs index c6ec08a..c3b41e0 100644 --- a/OpenNest/Controls/BestFitCell.cs +++ b/OpenNest/Controls/BestFitCell.cs @@ -1,8 +1,8 @@ -using OpenNest.Engine.BestFit; -using OpenNest.Math; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; +using OpenNest.Engine.BestFit; +using OpenNest.Math; namespace OpenNest.Controls { @@ -39,12 +39,19 @@ namespace OpenNest.Controls metadataLines = new[] { - string.Format("#{0} {1:F1}x{2:F1} Area={3:F1}", - rank, result.BoundingHeight, result.BoundingWidth, result.RotatedArea), - string.Format("Util={0:P1} Rot={1:F1}\u00b0", + string.Format( + "#{0} {1:F1}x{2:F1} Area={3:F1}", + rank, + result.BoundingHeight, + result.BoundingWidth, + result.RotatedArea + ), + string.Format( + "Util={0:P1} Rot={1:F1}\u00b0", result.Utilization, - Angle.ToDegrees(result.OptimalRotation)), - result.Keep ? "" : result.Reason + Angle.ToDegrees(result.OptimalRotation) + ), + result.Keep ? "" : result.Reason, }; } diff --git a/OpenNest/Controls/CadText.cs b/OpenNest/Controls/CadText.cs index 8ae4d81..c92b25d 100644 --- a/OpenNest/Controls/CadText.cs +++ b/OpenNest/Controls/CadText.cs @@ -1,5 +1,5 @@ -using System.Drawing; using System.Collections.Generic; +using System.Drawing; using System.Linq; using OpenNest.Bending; using OpenNest.Geometry; @@ -11,8 +11,11 @@ namespace OpenNest.Controls public ulong? SourceHandle { get; set; } public bool IsReplacedByBendNote(IEnumerable bends) => - SourceHandle.HasValue && bends != null && bends.Any(b => - b.SourceNoteHandle == SourceHandle && !string.IsNullOrEmpty(b.NoteText)); + SourceHandle.HasValue + && bends != null + && bends.Any(b => + b.SourceNoteHandle == SourceHandle && !string.IsNullOrEmpty(b.NoteText) + ); public Vector Position { get; set; } public string Value { get; set; } diff --git a/OpenNest/Controls/CollapsiblePanel.cs b/OpenNest/Controls/CollapsiblePanel.cs index 11e9d6d..45e875b 100644 --- a/OpenNest/Controls/CollapsiblePanel.cs +++ b/OpenNest/Controls/CollapsiblePanel.cs @@ -23,7 +23,7 @@ namespace OpenNest.Controls Dock = DockStyle.Top, Height = 28, BackColor = Color.FromArgb(240, 240, 240), - Cursor = Cursors.Hand + Cursor = Cursors.Hand, }; chevronLabel = new Label @@ -33,7 +33,7 @@ namespace OpenNest.Controls Size = new Size(20, 28), Dock = DockStyle.Left, TextAlign = ContentAlignment.MiddleCenter, - Font = new Font("Segoe UI", 9f) + Font = new Font("Segoe UI", 9f), }; headerLabel = new Label @@ -42,7 +42,7 @@ namespace OpenNest.Controls AutoSize = false, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft, - Font = new Font("Segoe UI", 9f, FontStyle.Bold) + Font = new Font("Segoe UI", 9f, FontStyle.Bold), }; headerPanel.Controls.Add(headerLabel); @@ -51,10 +51,7 @@ namespace OpenNest.Controls headerLabel.Click += (s, e) => Toggle(); chevronLabel.Click += (s, e) => Toggle(); - contentPanel = new Panel - { - Dock = DockStyle.Fill - }; + contentPanel = new Panel { Dock = DockStyle.Fill }; Controls.Add(contentPanel); Controls.Add(headerPanel); @@ -82,7 +79,8 @@ namespace OpenNest.Controls set { expandedHeight = value; - if (isExpanded) Height = value; + if (isExpanded) + Height = value; } } diff --git a/OpenNest/Controls/CutDirectionArrows.cs b/OpenNest/Controls/CutDirectionArrows.cs index 4953452..9418cee 100644 --- a/OpenNest/Controls/CutDirectionArrows.cs +++ b/OpenNest/Controls/CutDirectionArrows.cs @@ -1,20 +1,35 @@ +using System.Drawing; using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; -using System.Drawing; namespace OpenNest.Controls { internal static class CutDirectionArrows { - public static void DrawProgram(Graphics g, DrawControl view, Program pgm, ref Vector pos, - Pen pen, double spacing, float arrowSize) + public static void DrawProgram( + Graphics g, + DrawControl view, + Program pgm, + ref Vector pos, + Pen pen, + double spacing, + float arrowSize + ) { DrawProgram(g, view, pgm, pos, ref pos, pen, spacing, arrowSize); } - private static void DrawProgram(Graphics g, DrawControl view, Program pgm, Vector basePos, ref Vector pos, - Pen pen, double spacing, float arrowSize) + private static void DrawProgram( + Graphics g, + DrawControl view, + Program pgm, + Vector basePos, + ref Vector pos, + Pen pen, + double spacing, + float arrowSize + ) { for (var i = 0; i < pgm.Length; ++i) { @@ -27,16 +42,27 @@ namespace OpenNest.Controls { var holeBase = basePos + subpgm.Offset; pos = holeBase; - DrawProgram(g, view, subpgm.Program, holeBase, ref pos, pen, spacing, arrowSize); + DrawProgram( + g, + view, + subpgm.Program, + holeBase, + ref pos, + pen, + spacing, + arrowSize + ); } continue; } - if (code is not Motion motion) continue; + if (code is not Motion motion) + continue; - var endpt = pgm.Mode == Mode.Incremental - ? motion.EndPoint + pos - : motion.EndPoint + basePos; + var endpt = + pgm.Mode == Mode.Incremental + ? motion.EndPoint + pos + : motion.EndPoint + basePos; if (code.Type == CodeType.LinearMove) { @@ -49,10 +75,21 @@ namespace OpenNest.Controls var arc = (ArcMove)code; if (!arc.Suppressed) { - var center = pgm.Mode == Mode.Incremental - ? arc.CenterPoint + pos - : arc.CenterPoint + basePos; - DrawArcArrows(g, view, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize); + var center = + pgm.Mode == Mode.Incremental + ? arc.CenterPoint + pos + : arc.CenterPoint + basePos; + DrawArcArrows( + g, + view, + pos, + endpt, + center, + arc.Rotation, + pen, + spacing, + arrowSize + ); } } @@ -60,13 +97,21 @@ namespace OpenNest.Controls } } - private static void DrawLineArrows(Graphics g, DrawControl view, Vector start, Vector end, - Pen pen, double spacing, float arrowSize) + private static void DrawLineArrows( + Graphics g, + DrawControl view, + Vector start, + Vector end, + Pen pen, + double spacing, + float arrowSize + ) { var dx = end.X - start.X; var dy = end.Y - start.Y; var length = System.Math.Sqrt(dx * dx + dy * dy); - if (length < spacing * 0.5) return; + if (length < spacing * 0.5) + return; var dirX = dx / length; var dirY = dy / length; @@ -84,11 +129,21 @@ namespace OpenNest.Controls } } - private static void DrawArcArrows(Graphics g, DrawControl view, Vector start, Vector end, Vector center, - RotationType rotation, Pen pen, double spacing, float arrowSize) + private static void DrawArcArrows( + Graphics g, + DrawControl view, + Vector start, + Vector end, + Vector center, + RotationType rotation, + Pen pen, + double spacing, + float arrowSize + ) { var radius = center.DistanceTo(start); - if (radius < Tolerance.Epsilon) return; + if (radius < Tolerance.Epsilon) + return; var startAngle = System.Math.Atan2(start.Y - center.Y, start.X - center.X); var endAngle = System.Math.Atan2(end.Y - center.Y, end.X - center.X); @@ -97,16 +152,19 @@ namespace OpenNest.Controls if (rotation == RotationType.CCW) { sweep = endAngle - startAngle; - if (sweep <= 0) sweep += 2 * System.Math.PI; + if (sweep <= 0) + sweep += 2 * System.Math.PI; } else { sweep = startAngle - endAngle; - if (sweep <= 0) sweep += 2 * System.Math.PI; + if (sweep <= 0) + sweep += 2 * System.Math.PI; } var arcLength = radius * System.Math.Abs(sweep); - if (arcLength < spacing * 0.5) return; + if (arcLength < spacing * 0.5) + return; var count = System.Math.Max(1, (int)(arcLength / spacing)); var stepAngle = sweep / (count + 1); @@ -121,7 +179,8 @@ namespace OpenNest.Controls var pt = new Vector( center.X + radius * System.Math.Cos(angle), - center.Y + radius * System.Math.Sin(angle)); + center.Y + radius * System.Math.Sin(angle) + ); var screenPt = view.PointWorldToGraph(pt); double tangent; @@ -130,7 +189,10 @@ namespace OpenNest.Controls else tangent = angle - System.Math.PI / 2; - var screenAngle = System.Math.Atan2(-System.Math.Sin(tangent), System.Math.Cos(tangent)); + var screenAngle = System.Math.Atan2( + -System.Math.Sin(tangent), + System.Math.Cos(tangent) + ); DrawArrowHead(g, pen, screenPt, screenAngle, arrowSize); } } @@ -142,10 +204,12 @@ namespace OpenNest.Controls var left = new PointF( tip.X + size * (float)System.Math.Cos(leftAngle), - tip.Y + size * (float)System.Math.Sin(leftAngle)); + tip.Y + size * (float)System.Math.Sin(leftAngle) + ); var right = new PointF( tip.X + size * (float)System.Math.Cos(rightAngle), - tip.Y + size * (float)System.Math.Sin(rightAngle)); + tip.Y + size * (float)System.Math.Sin(rightAngle) + ); g.DrawLine(pen, left, tip); g.DrawLine(pen, right, tip); diff --git a/OpenNest/Controls/CutOffHandler.cs b/OpenNest/Controls/CutOffHandler.cs index dc5b720..b52edba 100644 --- a/OpenNest/Controls/CutOffHandler.cs +++ b/OpenNest/Controls/CutOffHandler.cs @@ -1,6 +1,6 @@ +using System.Collections.Generic; using OpenNest.CNC; using OpenNest.Geometry; -using System.Collections.Generic; namespace OpenNest.Controls { @@ -65,8 +65,10 @@ namespace OpenNest.Controls for (var i = 0; i < program.Codes.Count - 1; i += 2) { - if (program.Codes[i] is RapidMove rapid && - program.Codes[i + 1] is LinearMove linear) + if ( + program.Codes[i] is RapidMove rapid + && program.Codes[i + 1] is LinearMove linear + ) { var line = new Line(rapid.EndPoint, linear.EndPoint); if (line.ClosestPointTo(point).DistanceTo(point) <= tolerance) diff --git a/OpenNest/Controls/CuttingPanel.cs b/OpenNest/Controls/CuttingPanel.cs index 9dc6b54..f9f2112 100644 --- a/OpenNest/Controls/CuttingPanel.cs +++ b/OpenNest/Controls/CuttingPanel.cs @@ -1,26 +1,38 @@ -using OpenNest.CNC.CuttingStrategy; using System; using System.Drawing; using System.Windows.Forms; +using OpenNest.CNC.CuttingStrategy; namespace OpenNest.Controls { public class CuttingPanel : Panel { private static readonly string[] LeadInTypes = - { "None", "Line", "Arc", "Line + Arc", "Clean Hole", "Line + Line" }; + { + "None", + "Line", + "Arc", + "Line + Arc", + "Clean Hole", + "Line + Line", + }; - private static readonly string[] LeadOutTypes = - { "None", "Line", "Arc" }; + private static readonly string[] LeadOutTypes = { "None", "Line", "Arc" }; private readonly TabControl tabControl; - private readonly ComboBox cboExternalLeadIn, cboExternalLeadOut; - private readonly ComboBox cboInternalLeadIn, cboInternalLeadOut; - private readonly ComboBox cboArcCircleLeadIn, cboArcCircleLeadOut; + private readonly ComboBox cboExternalLeadIn, + cboExternalLeadOut; + private readonly ComboBox cboInternalLeadIn, + cboInternalLeadOut; + private readonly ComboBox cboArcCircleLeadIn, + cboArcCircleLeadOut; - private readonly Panel pnlExternalLeadIn, pnlExternalLeadOut; - private readonly Panel pnlInternalLeadIn, pnlInternalLeadOut; - private readonly Panel pnlArcCircleLeadIn, pnlArcCircleLeadOut; + private readonly Panel pnlExternalLeadIn, + pnlExternalLeadOut; + private readonly Panel pnlInternalLeadIn, + pnlInternalLeadOut; + private readonly Panel pnlArcCircleLeadIn, + pnlArcCircleLeadOut; private readonly CheckBox chkTabsEnabled; private readonly NumericUpDown nudTabWidth; @@ -55,7 +67,7 @@ namespace OpenNest.Controls 0 => ContourType.External, 1 => ContourType.Internal, 2 => ContourType.ArcCircle, - _ => null + _ => null, }; } set @@ -68,7 +80,7 @@ namespace OpenNest.Controls ContourType.External => 0, ContourType.Internal => 1, ContourType.ArcCircle => 2, - _ => -1 + _ => -1, }; if (index >= 0 && tabControl.SelectedIndex != index) @@ -82,31 +94,39 @@ namespace OpenNest.Controls BackColor = Color.White; // Tab control for contour types — wrapped in a fixed-height panel for Dock.Top - tabControl = new TabControl - { - Dock = DockStyle.Fill - }; + tabControl = new TabControl { Dock = DockStyle.Fill }; var tabExternal = new TabPage("External") { Padding = new Padding(4) }; var tabInternal = new TabPage("Internal") { Padding = new Padding(4) }; var tabArcCircle = new TabPage("Arc / Circle") { Padding = new Padding(4) }; - SetupTab(tabExternal, out cboExternalLeadIn, out pnlExternalLeadIn, - out cboExternalLeadOut, out pnlExternalLeadOut); - SetupTab(tabInternal, out cboInternalLeadIn, out pnlInternalLeadIn, - out cboInternalLeadOut, out pnlInternalLeadOut); - SetupTab(tabArcCircle, out cboArcCircleLeadIn, out pnlArcCircleLeadIn, - out cboArcCircleLeadOut, out pnlArcCircleLeadOut); + SetupTab( + tabExternal, + out cboExternalLeadIn, + out pnlExternalLeadIn, + out cboExternalLeadOut, + out pnlExternalLeadOut + ); + SetupTab( + tabInternal, + out cboInternalLeadIn, + out pnlInternalLeadIn, + out cboInternalLeadOut, + out pnlInternalLeadOut + ); + SetupTab( + tabArcCircle, + out cboArcCircleLeadIn, + out pnlArcCircleLeadIn, + out cboArcCircleLeadOut, + out pnlArcCircleLeadOut + ); tabControl.Controls.Add(tabExternal); tabControl.Controls.Add(tabInternal); tabControl.Controls.Add(tabArcCircle); - var tabWrapper = new Panel - { - Dock = DockStyle.Top, - Height = 340 - }; + var tabWrapper = new Panel { Dock = DockStyle.Top, Height = 340 }; tabWrapper.Controls.Add(tabControl); // Tabs section @@ -115,23 +135,25 @@ namespace OpenNest.Controls HeaderText = "Tabs", Dock = DockStyle.Top, ExpandedHeight = 160, - IsExpanded = false + IsExpanded = false, }; chkTabsEnabled = new CheckBox { Text = "Enable Tabs", Location = new Point(12, 4), - AutoSize = true + AutoSize = true, }; tabsPanel.ContentPanel.Controls.Add(chkTabsEnabled); - tabsPanel.ContentPanel.Controls.Add(new Label - { - Text = "Tab Size:", - Location = new Point(160, 6), - AutoSize = true - }); + tabsPanel.ContentPanel.Controls.Add( + new Label + { + Text = "Tab Size:", + Location = new Point(160, 6), + AutoSize = true, + } + ); nudTabWidth = CreateNumeric(225, 3, 0.25, 0.0625); nudTabWidth.Enabled = false; @@ -143,7 +165,7 @@ namespace OpenNest.Controls Location = new Point(28, 28), AutoSize = true, Enabled = false, - Checked = true + Checked = true, }; tabsPanel.ContentPanel.Controls.Add(rbTabAll); @@ -152,27 +174,31 @@ namespace OpenNest.Controls Text = "Auto-tab when smallest part dimension is between:", Location = new Point(28, 50), AutoSize = true, - Enabled = false + Enabled = false, }; tabsPanel.ContentPanel.Controls.Add(rbAutoTab); - tabsPanel.ContentPanel.Controls.Add(new Label - { - Text = "Min:", - Location = new Point(44, 76), - AutoSize = true - }); + tabsPanel.ContentPanel.Controls.Add( + new Label + { + Text = "Min:", + Location = new Point(44, 76), + AutoSize = true, + } + ); nudAutoTabMin = CreateNumeric(77, 73, 0, 0.0625); nudAutoTabMin.Enabled = false; tabsPanel.ContentPanel.Controls.Add(nudAutoTabMin); - tabsPanel.ContentPanel.Controls.Add(new Label - { - Text = "Max:", - Location = new Point(210, 76), - AutoSize = true - }); + tabsPanel.ContentPanel.Controls.Add( + new Label + { + Text = "Max:", + Location = new Point(210, 76), + AutoSize = true, + } + ); nudAutoTabMax = CreateNumeric(245, 73, 0, 0.0625); nudAutoTabMax.Enabled = false; @@ -202,15 +228,17 @@ namespace OpenNest.Controls HeaderText = "Pierce", Dock = DockStyle.Top, ExpandedHeight = 90, - IsExpanded = true + IsExpanded = true, }; - piercePanel.ContentPanel.Controls.Add(new Label - { - Text = "Pierce Clearance:", - Location = new Point(12, 6), - AutoSize = true - }); + piercePanel.ContentPanel.Controls.Add( + new Label + { + Text = "Pierce Clearance:", + Location = new Point(12, 6), + AutoSize = true, + } + ); nudPierceClearance = CreateNumeric(130, 3, 0.0625, 0.0625); piercePanel.ContentPanel.Controls.Add(nudPierceClearance); @@ -219,7 +247,7 @@ namespace OpenNest.Controls { Text = "Round Lead-In Angles", Location = new Point(12, 32), - AutoSize = true + AutoSize = true, }; chkRoundLeadInAngles.CheckedChanged += (s, e) => { @@ -228,12 +256,14 @@ namespace OpenNest.Controls }; piercePanel.ContentPanel.Controls.Add(chkRoundLeadInAngles); - piercePanel.ContentPanel.Controls.Add(new Label - { - Text = "Increment:", - Location = new Point(175, 34), - AutoSize = true - }); + piercePanel.ContentPanel.Controls.Add( + new Label + { + Text = "Increment:", + Location = new Point(175, 34), + AutoSize = true, + } + ); nudLeadInAngleIncrement = CreateNumeric(245, 31, 5, 1); nudLeadInAngleIncrement.DecimalPlaces = 0; @@ -249,7 +279,7 @@ namespace OpenNest.Controls Text = "Auto-Assign Lead-ins", Dock = DockStyle.Top, Height = 32, - Visible = false + Visible = false, }; btnAutoAssign.Click += (s, e) => AutoAssignClicked?.Invoke(this, EventArgs.Empty); @@ -257,7 +287,7 @@ namespace OpenNest.Controls { Dock = DockStyle.Top, Height = 36, - Padding = new Padding(4, 2, 4, 2) + Padding = new Padding(4, 2, 4, 2), }; btnWrapper.Controls.Add(btnAutoAssign); @@ -287,8 +317,10 @@ namespace OpenNest.Controls PierceClearance = (double)nudPierceClearance.Value, RoundLeadInAngles = chkRoundLeadInAngles.Checked, LeadInAngleIncrement = (double)nudLeadInAngleIncrement.Value, - AutoTabMinSize = chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMin.Value : 0, - AutoTabMaxSize = chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMax.Value : 0 + AutoTabMinSize = + chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMin.Value : 0, + AutoTabMaxSize = + chkTabsEnabled.Checked && rbAutoTab.Checked ? (double)nudAutoTabMax.Value : 0, }; } @@ -325,30 +357,36 @@ namespace OpenNest.Controls ParametersChanged?.Invoke(this, EventArgs.Empty); } - private static void SetupTab(TabPage tab, - out ComboBox leadInCombo, out Panel leadInPanel, - out ComboBox leadOutCombo, out Panel leadOutPanel) + private static void SetupTab( + TabPage tab, + out ComboBox leadInCombo, + out Panel leadInPanel, + out ComboBox leadOutCombo, + out Panel leadOutPanel + ) { var grpLeadIn = new GroupBox { Text = "Lead-In", Location = new Point(4, 4), - Size = new Size(340, 148) + Size = new Size(340, 148), }; tab.Controls.Add(grpLeadIn); - grpLeadIn.Controls.Add(new Label - { - Text = "Type:", - Location = new Point(8, 22), - AutoSize = true - }); + grpLeadIn.Controls.Add( + new Label + { + Text = "Type:", + Location = new Point(8, 22), + AutoSize = true, + } + ); leadInCombo = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Location = new Point(90, 19), - Size = new Size(230, 24) + Size = new Size(230, 24), }; grpLeadIn.Controls.Add(leadInCombo); @@ -356,7 +394,7 @@ namespace OpenNest.Controls { Location = new Point(8, 48), Size = new Size(320, 92), - AutoScroll = true + AutoScroll = true, }; grpLeadIn.Controls.Add(leadInPanel); @@ -364,22 +402,24 @@ namespace OpenNest.Controls { Text = "Lead-Out", Location = new Point(4, 156), - Size = new Size(340, 132) + Size = new Size(340, 132), }; tab.Controls.Add(grpLeadOut); - grpLeadOut.Controls.Add(new Label - { - Text = "Type:", - Location = new Point(8, 22), - AutoSize = true - }); + grpLeadOut.Controls.Add( + new Label + { + Text = "Type:", + Location = new Point(8, 22), + AutoSize = true, + } + ); leadOutCombo = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Location = new Point(90, 19), - Size = new Size(230, 24) + Size = new Size(230, 24), }; grpLeadOut.Controls.Add(leadOutCombo); @@ -387,20 +427,24 @@ namespace OpenNest.Controls { Location = new Point(8, 48), Size = new Size(320, 76), - AutoScroll = true + AutoScroll = true, }; grpLeadOut.Controls.Add(leadOutPanel); } private void PopulateDropdowns() { - foreach (var combo in new[] { cboExternalLeadIn, cboInternalLeadIn, cboArcCircleLeadIn }) + foreach ( + var combo in new[] { cboExternalLeadIn, cboInternalLeadIn, cboArcCircleLeadIn } + ) { combo.Items.AddRange(LeadInTypes); combo.SelectedIndex = 0; } - foreach (var combo in new[] { cboExternalLeadOut, cboInternalLeadOut, cboArcCircleLeadOut }) + foreach ( + var combo in new[] { cboExternalLeadOut, cboInternalLeadOut, cboArcCircleLeadOut } + ) { combo.Items.AddRange(LeadOutTypes); combo.SelectedIndex = 0; @@ -438,17 +482,23 @@ namespace OpenNest.Controls private Panel GetLeadInPanel(ComboBox combo) { - if (combo == cboExternalLeadIn) return pnlExternalLeadIn; - if (combo == cboInternalLeadIn) return pnlInternalLeadIn; - if (combo == cboArcCircleLeadIn) return pnlArcCircleLeadIn; + if (combo == cboExternalLeadIn) + return pnlExternalLeadIn; + if (combo == cboInternalLeadIn) + return pnlInternalLeadIn; + if (combo == cboArcCircleLeadIn) + return pnlArcCircleLeadIn; return null; } private Panel GetLeadOutPanel(ComboBox combo) { - if (combo == cboExternalLeadOut) return pnlExternalLeadOut; - if (combo == cboInternalLeadOut) return pnlInternalLeadOut; - if (combo == cboArcCircleLeadOut) return pnlArcCircleLeadOut; + if (combo == cboExternalLeadOut) + return pnlExternalLeadOut; + if (combo == cboInternalLeadOut) + return pnlInternalLeadOut; + if (combo == cboArcCircleLeadOut) + return pnlArcCircleLeadOut; return null; } @@ -502,15 +552,22 @@ namespace OpenNest.Controls } } - private void AddNumericField(Panel panel, string label, double defaultValue, - ref int y, string tag) + private void AddNumericField( + Panel panel, + string label, + double defaultValue, + ref int y, + string tag + ) { - panel.Controls.Add(new Label - { - Text = label, - Location = new Point(0, y + 3), - AutoSize = true - }); + panel.Controls.Add( + new Label + { + Text = label, + Location = new Point(0, y + 3), + AutoSize = true, + } + ); var nud = CreateNumeric(130, y, defaultValue, 0.0625); nud.Tag = tag; @@ -520,7 +577,12 @@ namespace OpenNest.Controls y += 30; } - private static NumericUpDown CreateNumeric(int x, int y, double defaultValue, double increment) + private static NumericUpDown CreateNumeric( + int x, + int y, + double defaultValue, + double increment + ) { return new NumericUpDown { @@ -530,7 +592,7 @@ namespace OpenNest.Controls Increment = (decimal)increment, Minimum = 0, Maximum = 9999, - Value = (decimal)defaultValue + Value = (decimal)defaultValue, }; } @@ -598,32 +660,29 @@ namespace OpenNest.Controls 1 => new LineLeadIn { Length = GetParam(panel, "Length", 0.25), - ApproachAngle = GetParam(panel, "ApproachAngle", 90) - }, - 2 => new ArcLeadIn - { - Radius = GetParam(panel, "Radius", 0.25) + ApproachAngle = GetParam(panel, "ApproachAngle", 90), }, + 2 => new ArcLeadIn { Radius = GetParam(panel, "Radius", 0.25) }, 3 => new LineArcLeadIn { LineLength = GetParam(panel, "LineLength", 0.25), ArcRadius = GetParam(panel, "ArcRadius", 0.125), - ApproachAngle = GetParam(panel, "ApproachAngle", 135) + ApproachAngle = GetParam(panel, "ApproachAngle", 135), }, 4 => new CleanHoleLeadIn { LineLength = GetParam(panel, "LineLength", 0.25), ArcRadius = GetParam(panel, "ArcRadius", 0.125), - Kerf = GetParam(panel, "Kerf", 0.06) + Kerf = GetParam(panel, "Kerf", 0.06), }, 5 => new LineLineLeadIn { Length1 = GetParam(panel, "Length1", 0.25), ApproachAngle1 = GetParam(panel, "Angle1", 90), Length2 = GetParam(panel, "Length2", 0.25), - ApproachAngle2 = GetParam(panel, "Angle2", 90) + ApproachAngle2 = GetParam(panel, "Angle2", 90), }, - _ => new NoLeadIn() + _ => new NoLeadIn(), }; } @@ -634,13 +693,10 @@ namespace OpenNest.Controls 1 => new LineLeadOut { Length = GetParam(panel, "Length", 0.25), - ApproachAngle = GetParam(panel, "ApproachAngle", 90) + ApproachAngle = GetParam(panel, "ApproachAngle", 90), }, - 2 => new ArcLeadOut - { - Radius = GetParam(panel, "Radius", 0.25) - }, - _ => new NoLeadOut() + 2 => new ArcLeadOut { Radius = GetParam(panel, "Radius", 0.25) }, + _ => new NoLeadOut(), }; } diff --git a/OpenNest/Controls/DensityBar.cs b/OpenNest/Controls/DensityBar.cs index 88eb476..3db455a 100644 --- a/OpenNest/Controls/DensityBar.cs +++ b/OpenNest/Controls/DensityBar.cs @@ -15,7 +15,12 @@ namespace OpenNest.Controls public DensityBar() { DoubleBuffered = true; - SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + SetStyle( + ControlStyles.OptimizedDoubleBuffer + | ControlStyles.AllPaintingInWmPaint + | ControlStyles.UserPaint, + true + ); Size = new Size(60, 8); } @@ -50,8 +55,11 @@ namespace OpenNest.Controls var fillRect = new Rectangle(rect.X, rect.Y, fillWidth, rect.Height); using var fillPath = CreateRoundedRect(fillRect, fillRadius); using var gradientBrush = new LinearGradientBrush( - new Point(rect.X, 0), new Point(rect.Right, 0), - LowColor, HighColor); + new Point(rect.X, 0), + new Point(rect.Right, 0), + LowColor, + HighColor + ); g.FillPath(gradientBrush, fillPath); } } diff --git a/OpenNest/Controls/DrawControl.cs b/OpenNest/Controls/DrawControl.cs index ebaf59c..ce6cfc4 100644 --- a/OpenNest/Controls/DrawControl.cs +++ b/OpenNest/Controls/DrawControl.cs @@ -1,8 +1,8 @@ -using OpenNest.Geometry; -using System; +using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; +using OpenNest.Geometry; namespace OpenNest.Controls { @@ -201,7 +201,8 @@ namespace OpenNest.Controls ViewScale *= zoomFactor; UpdateMatrix(); - if (redraw) Invalidate(); + if (redraw) + Invalidate(); } public virtual void ZoomToArea(Box box, bool redraw = true) @@ -209,7 +210,13 @@ namespace OpenNest.Controls ZoomToArea(box.X, box.Y, box.Length, box.Width, redraw); } - public virtual void ZoomToArea(double x, double y, double width, double height, bool redraw = true) + public virtual void ZoomToArea( + double x, + double y, + double width, + double height, + bool redraw = true + ) { if (width <= 0 || height <= 0) return; @@ -234,7 +241,8 @@ namespace OpenNest.Controls UpdateMatrix(); - if (redraw) Invalidate(); + if (redraw) + Invalidate(); } protected virtual void UpdateMatrix() diff --git a/OpenNest/Controls/DrawingListBox.cs b/OpenNest/Controls/DrawingListBox.cs index 1993fa2..92b11dd 100644 --- a/OpenNest/Controls/DrawingListBox.cs +++ b/OpenNest/Controls/DrawingListBox.cs @@ -68,7 +68,12 @@ namespace OpenNest.Controls if (isSelected) { - var borderRect = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width - 1, e.Bounds.Height - 1); + var borderRect = new Rectangle( + e.Bounds.X, + e.Bounds.Y, + e.Bounds.Width - 1, + e.Bounds.Height - 1 + ); using var borderPen = new Pen(SystemColors.Highlight, 2); e.Graphics.DrawRectangle(borderPen, borderRect); } @@ -76,11 +81,18 @@ namespace OpenNest.Controls if (!HideQuantity && dwg.Quantity.Required > 0) { var barWidth = 4; - var barColor = dwg.Quantity.Nested >= dwg.Quantity.Required - ? Color.FromArgb(76, 175, 80) - : Color.FromArgb(255, 152, 0); + var barColor = + dwg.Quantity.Nested >= dwg.Quantity.Required + ? Color.FromArgb(76, 175, 80) + : Color.FromArgb(255, 152, 0); using var barBrush = new SolidBrush(barColor); - e.Graphics.FillRectangle(barBrush, e.Bounds.X, e.Bounds.Y, barWidth, e.Bounds.Height); + e.Graphics.FillRectangle( + barBrush, + e.Bounds.X, + e.Bounds.Y, + barWidth, + e.Bounds.Height + ); } var pt = new PointF(5, e.Bounds.Y + 5); @@ -104,7 +116,11 @@ namespace OpenNest.Controls var bounds = dwg.Program.BoundingBox(); var text2 = bounds.Size.ToString(4); - var text3 = string.Format("{0} sq/{1}", System.Math.Round(dwg.Area, 4), UnitsHelper.GetShortString(Units)); + var text3 = string.Format( + "{0} sq/{1}", + System.Math.Round(dwg.Area, 4), + UnitsHelper.GetShortString(Units) + ); if (HideQuantity) { @@ -115,7 +131,11 @@ namespace OpenNest.Controls } else { - var text1 = string.Format("{0} of {1} nested", dwg.Quantity.Nested, dwg.Quantity.Required); + var text1 = string.Format( + "{0} of {1} nested", + dwg.Quantity.Nested, + dwg.Quantity.Required + ); pt.Y += 22; e.Graphics.DrawString(text1, Font, detailBrush, pt); pt.Y += 18; @@ -156,14 +176,23 @@ namespace OpenNest.Controls if (Items.Count > 0) { - var lastVisible = System.Math.Min(TopIndex + (ClientSize.Height / ItemHeight), Items.Count - 1); + var lastVisible = System.Math.Min( + TopIndex + (ClientSize.Height / ItemHeight), + Items.Count - 1 + ); itemBottom = GetItemRectangle(lastVisible).Bottom; } if (itemBottom < ClientSize.Height) { using var g = Graphics.FromHdc(m.WParam); - g.FillRectangle(Brushes.White, 0, itemBottom, ClientSize.Width, ClientSize.Height - itemBottom); + g.FillRectangle( + Brushes.White, + 0, + itemBottom, + ClientSize.Width, + ClientSize.Height - itemBottom + ); } m.Result = (IntPtr)1; diff --git a/OpenNest/Controls/EntityView.cs b/OpenNest/Controls/EntityView.cs index b73bcb5..4870a0e 100644 --- a/OpenNest/Controls/EntityView.cs +++ b/OpenNest/Controls/EntityView.cs @@ -1,12 +1,12 @@ -using OpenNest.Bending; -using OpenNest.Geometry; -using OpenNest.Math; -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; +using OpenNest.Bending; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Controls { @@ -61,15 +61,18 @@ namespace OpenNest.Controls this.Cursor = Cursors.Cross; SetStyle( - ControlStyles.AllPaintingInWmPaint | - ControlStyles.OptimizedDoubleBuffer | - ControlStyles.UserPaint, true); + ControlStyles.AllPaintingInWmPaint + | ControlStyles.OptimizedDoubleBuffer + | ControlStyles.UserPaint, + true + ); } protected override void OnMouseClick(MouseEventArgs e) { base.OnMouseClick(e); - if (!Focused) Focus(); + if (!Focused) + Focus(); if (IsPickingBendLine && e.Button == MouseButtons.Left) { @@ -109,8 +112,10 @@ namespace OpenNest.Controls foreach (var entity in Entities) { - if (IsEtchLayer(entity.Layer)) continue; - var isHighlighted = simplifierHighlightSet != null && simplifierHighlightSet.Contains(entity); + if (IsEtchLayer(entity.Layer)) + continue; + var isHighlighted = + simplifierHighlightSet != null && simplifierHighlightSet.Contains(entity); var pen = isHighlighted ? GetEntityPen(Color.FromArgb(60, entity.Color)) : GetEntityPen(entity.Color); @@ -121,7 +126,8 @@ namespace OpenNest.Controls foreach (var entity in Entities) { - if (!IsEtchLayer(entity.Layer)) continue; + if (!IsEtchLayer(entity.Layer)) + continue; var pen = GetEntityPen(entity.Color); DrawEntity(e.Graphics, entity, pen); } @@ -146,7 +152,10 @@ namespace OpenNest.Controls // Draw old geometry (highlighted lines) in orange dashed if (simplifierHighlightSet != null) { - using var oldPen = new Pen(Color.FromArgb(180, 255, 160, 50), 1f / ViewScale) { DashPattern = new float[] { 6, 3 } }; + using var oldPen = new Pen(Color.FromArgb(180, 255, 160, 50), 1f / ViewScale) + { + DashPattern = new float[] { 6, 3 }, + }; foreach (var entity in simplifierHighlightSet) DrawEntity(e.Graphics, entity, oldPen); } @@ -161,7 +170,9 @@ namespace OpenNest.Controls var offsetShape = new Shape(); offsetShape.Entities.AddRange(Entities); - foreach (var entity in ((Shape)offsetShape.OffsetEntity(0.25, OffsetSide.Left)).Entities) + foreach ( + var entity in ((Shape)offsetShape.OffsetEntity(0.25, OffsetSide.Left)).Entities + ) DrawEntity(e.Graphics, entity, Pens.RoyalBlue); #endif @@ -237,10 +248,12 @@ namespace OpenNest.Controls // Clamp dark colors to ensure visibility on dark background var brightness = (color.R * 299 + color.G * 587 + color.B * 114) / 1000; if (brightness < 80) - color = Color.FromArgb(color.A, + color = Color.FromArgb( + color.A, System.Math.Max(color.R, (byte)80), System.Math.Max(color.G, (byte)80), - System.Math.Max(color.B, (byte)80)); + System.Math.Max(color.B, (byte)80) + ); var argb = color.ToArgb(); if (!penCache.TryGetValue(argb, out var pen)) @@ -266,13 +279,10 @@ namespace OpenNest.Controls if (Bends == null || Bends.Count == 0) return; - using var bendPen = new Pen(Color.Yellow, 1.5f) - { - DashPattern = new float[] { 8, 6 } - }; + using var bendPen = new Pen(Color.Yellow, 1.5f) { DashPattern = new float[] { 8, 6 } }; using var glowPen = new Pen(Color.OrangeRed, 2.0f) { - DashPattern = new float[] { 6, 4 } + DashPattern = new float[] { 6, 4 }, }; using var noteFont = new Font("Segoe UI", 9f); using var noteBrush = new SolidBrush(Color.FromArgb(220, 255, 255, 200)); @@ -293,18 +303,27 @@ namespace OpenNest.Controls if (!string.IsNullOrEmpty(bend.NoteText)) { var mid = new PointF((pt1.X + pt2.X) / 2f, (pt1.Y + pt2.Y) / 2f); - var angle = (float)(System.Math.Atan2(pt2.Y - pt1.Y, pt2.X - pt1.X) * 180.0 / System.Math.PI); + var angle = (float)( + System.Math.Atan2(pt2.Y - pt1.Y, pt2.X - pt1.X) * 180.0 / System.Math.PI + ); // Keep text readable (not upside-down) - if (angle > 90f) angle -= 180f; - else if (angle < -90f) angle += 180f; + if (angle > 90f) + angle -= 180f; + else if (angle < -90f) + angle += 180f; var textSize = g.MeasureString(bend.NoteText, noteFont); var state = g.Save(); g.TranslateTransform(mid.X, mid.Y); g.RotateTransform(angle); - g.DrawString(bend.NoteText, noteFont, isSelected ? selectedNoteBrush : noteBrush, - -textSize.Width / 2f, -textSize.Height); + g.DrawString( + bend.NoteText, + noteFont, + isSelected ? selectedNoteBrush : noteBrush, + -textSize.Width / 2f, + -textSize.Height + ); g.Restore(state); } } @@ -355,8 +374,12 @@ namespace OpenNest.Controls var minY = text.Position.Y - tolerance; var maxY = text.Position.Y + text.Height + tolerance; - if (worldPoint.X >= minX && worldPoint.X <= maxX && - worldPoint.Y >= minY && worldPoint.Y <= maxY) + if ( + worldPoint.X >= minX + && worldPoint.X <= maxX + && worldPoint.Y >= minY + && worldPoint.Y <= maxY + ) return text; } @@ -380,7 +403,8 @@ namespace OpenNest.Controls continue; var mid = GetEntityMidPoint(entity, i); - if (!mid.HasValue) continue; + if (!mid.HasValue) + continue; var screenExtent = GetEntityScreenExtent(entity); var text = i.ToString(); @@ -394,7 +418,13 @@ namespace OpenNest.Controls var pt = PointWorldToGraph(mid.Value); var cx = pt.X - size.Width / 2f; var cy = pt.Y - size.Height / 2f; - g.FillEllipse(labelBackBrush, pt.X - radius, pt.Y - radius, radius * 2f, radius * 2f); + g.FillEllipse( + labelBackBrush, + pt.X - radius, + pt.Y - radius, + radius * 2f, + radius * 2f + ); g.DrawString(text, labelFont, labelBrush, cx, cy); } } @@ -432,14 +462,16 @@ namespace OpenNest.Controls : arc.StartAngle + arc.SweepAngle() / 2.0; return new Vector( arc.Center.X + arc.Radius * System.Math.Cos(midAngle), - arc.Center.Y + arc.Radius * System.Math.Sin(midAngle)); + arc.Center.Y + arc.Radius * System.Math.Sin(midAngle) + ); case Circle circle: // Use golden angle (~137.5°) per index so concentric circles spread labels apart var circleAngle = index * 2.399; return new Vector( circle.Center.X + circle.Radius * System.Math.Cos(circleAngle), - circle.Center.Y + circle.Radius * System.Math.Sin(circleAngle)); + circle.Center.Y + circle.Radius * System.Math.Sin(circleAngle) + ); default: return null; @@ -506,7 +538,8 @@ namespace OpenNest.Controls diameter, diameter, startAngle, - -(float)Angle.ToDegrees(arc.SweepAngle())); + -(float)Angle.ToDegrees(arc.SweepAngle()) + ); } private void DrawCircle(Graphics g, Circle circle, Pen pen) @@ -515,11 +548,7 @@ namespace OpenNest.Controls var radius = LengthWorldToGui(circle.Radius); var diameter = radius * 2.0f; - g.DrawEllipse(pen, - center.X - radius, - center.Y - radius, - diameter, - diameter); + g.DrawEllipse(pen, center.X - radius, center.Y - radius, diameter, diameter); } private void DrawTexts(Graphics g) @@ -532,11 +561,13 @@ namespace OpenNest.Controls foreach (var text in Texts) { // The bend overlay already renders this source annotation. - if (text.IsReplacedByBendNote(Bends)) continue; + if (text.IsReplacedByBendNote(Bends)) + continue; var pos = PointWorldToGraph(text.Position); var fontSize = LengthWorldToGui(text.Height); - if (fontSize < 2f) continue; + if (fontSize < 2f) + continue; var state = g.Save(); g.TranslateTransform(pos.X, pos.Y); diff --git a/OpenNest/Controls/FileListControl.cs b/OpenNest/Controls/FileListControl.cs index 49e9d9c..75496b4 100644 --- a/OpenNest/Controls/FileListControl.cs +++ b/OpenNest/Controls/FileListControl.cs @@ -1,12 +1,12 @@ // OpenNest/Controls/FileListControl.cs -using OpenNest.Bending; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; +using OpenNest.Bending; +using OpenNest.Geometry; namespace OpenNest.Controls { @@ -48,18 +48,24 @@ namespace OpenNest.Controls public FileListControl() { SetStyle( - ControlStyles.AllPaintingInWmPaint | - ControlStyles.OptimizedDoubleBuffer | - ControlStyles.UserPaint | - ControlStyles.ResizeRedraw | - ControlStyles.Selectable, true); + ControlStyles.AllPaintingInWmPaint + | ControlStyles.OptimizedDoubleBuffer + | ControlStyles.UserPaint + | ControlStyles.ResizeRedraw + | ControlStyles.Selectable, + true + ); BackColor = Color.White; Font = new Font("Segoe UI", 9f); scrollBar.Dock = DockStyle.Right; scrollBar.Visible = false; - scrollBar.Scroll += (s, e) => { scrollOffset = e.NewValue; Invalidate(); }; + scrollBar.Scroll += (s, e) => + { + scrollOffset = e.NewValue; + Invalidate(); + }; Controls.Add(scrollBar); } @@ -67,15 +73,18 @@ namespace OpenNest.Controls public int SelectedIndex => selectedIndex; public FileListItem SelectedItem => - selectedIndex >= 0 && selectedIndex < items.Count - ? items[selectedIndex] - : null; + selectedIndex >= 0 && selectedIndex < items.Count ? items[selectedIndex] : null; public void AddItem(FileListItem item) { - var index = items.BinarySearch(item, Comparer.Create( - (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase))); - if (index < 0) index = ~index; + var index = items.BinarySearch( + item, + Comparer.Create( + (a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase) + ) + ); + if (index < 0) + index = ~index; items.Insert(index, item); if (items.Count == 1) @@ -135,7 +144,8 @@ namespace OpenNest.Controls private void EnsureVisible(int index) { - if (index < 0 || index >= items.Count) return; + if (index < 0 || index >= items.Count) + return; var itemTop = index * ItemHeight; var itemBottom = itemTop + ItemHeight; @@ -145,7 +155,10 @@ namespace OpenNest.Controls scrollOffset = itemBottom - Height; if (scrollBar.Visible) - scrollBar.Value = System.Math.Min(scrollOffset, scrollBar.Maximum - scrollBar.LargeChange + 1); + scrollBar.Value = System.Math.Min( + scrollOffset, + scrollBar.Maximum - scrollBar.LargeChange + 1 + ); } protected override void OnResize(EventArgs e) @@ -156,7 +169,8 @@ namespace OpenNest.Controls public void ProcessArrowKey(Keys keyData) { - if (items.Count == 0) return; + if (items.Count == 0) + return; var newIndex = selectedIndex; if (keyData == Keys.Down) @@ -214,7 +228,8 @@ namespace OpenNest.Controls for (var i = 0; i < items.Count; i++) { var y = i * ItemHeight - scrollOffset; - if (y + ItemHeight < 0 || y > Height) continue; + if (y + ItemHeight < 0 || y > Height) + continue; var item = items[i]; var rect = new Rectangle(0, y, contentWidth, ItemHeight); @@ -231,24 +246,52 @@ namespace OpenNest.Controls // Name var nameRect = new Rectangle(AccentBarWidth + 8, y + 6, contentWidth - 70, 20); - TextRenderer.DrawText(g, item.Name, boldFont, nameRect, ForeColor, TextFormatFlags.Left | TextFormatFlags.EndEllipsis); + TextRenderer.DrawText( + g, + item.Name, + boldFont, + nameRect, + ForeColor, + TextFormatFlags.Left | TextFormatFlags.EndEllipsis + ); // Dimensions + entity count var bounds = item.Bounds; - var dimText = bounds != null - ? $"{bounds.Width:0.#} x {bounds.Length:0.#} — {item.EntityCount} entities" - : $"{item.EntityCount} entities"; + var dimText = + bounds != null + ? $"{bounds.Width:0.#} x {bounds.Length:0.#} — {item.EntityCount} entities" + : $"{item.EntityCount} entities"; var dimRect = new Rectangle(AccentBarWidth + 8, y + 26, contentWidth - 70, 16); - TextRenderer.DrawText(g, dimText, smallFont, dimRect, Color.FromArgb(130, 130, 130), TextFormatFlags.Left); + TextRenderer.DrawText( + g, + dimText, + smallFont, + dimRect, + Color.FromArgb(130, 130, 130), + TextFormatFlags.Left + ); // Quantity badge var qtyText = $"x{item.Quantity}"; var qtyRect = new Rectangle(contentWidth - 50, y + 12, 40, 24); - TextRenderer.DrawText(g, qtyText, Font, qtyRect, Color.FromArgb(100, 100, 100), TextFormatFlags.Right | TextFormatFlags.VerticalCenter); + TextRenderer.DrawText( + g, + qtyText, + Font, + qtyRect, + Color.FromArgb(100, 100, 100), + TextFormatFlags.Right | TextFormatFlags.VerticalCenter + ); // Separator if (i < items.Count - 1) - g.DrawLine(separatorPen, AccentBarWidth + 8, y + ItemHeight - 1, contentWidth - 8, y + ItemHeight - 1); + g.DrawLine( + separatorPen, + AccentBarWidth + 8, + y + ItemHeight - 1, + contentWidth - 8, + y + ItemHeight - 1 + ); } boldFont.Dispose(); @@ -263,15 +306,22 @@ namespace OpenNest.Controls var size = g.MeasureString(text, Font); var x = (Width - size.Width) / 2; var y = (Height - size.Height) / 2; - g.DrawString(text, Font, emptyStateBrush, x, y, - new StringFormat { Alignment = StringAlignment.Center }); + g.DrawString( + text, + Font, + emptyStateBrush, + x, + y, + new StringFormat { Alignment = StringAlignment.Center } + ); } protected override void OnMouseClick(MouseEventArgs e) { base.OnMouseClick(e); var index = GetIndexAt(e.Y); - if (index < 0 || index >= items.Count) return; + if (index < 0 || index >= items.Count) + return; if (e.Button == MouseButtons.Right) { @@ -314,7 +364,10 @@ namespace OpenNest.Controls var maxScroll = System.Math.Max(0, items.Count * ItemHeight - Height); scrollOffset = System.Math.Max(0, System.Math.Min(maxScroll, scrollOffset - e.Delta)); if (scrollBar.Visible) - scrollBar.Value = System.Math.Min(scrollOffset, scrollBar.Maximum - scrollBar.LargeChange + 1); + scrollBar.Value = System.Math.Min( + scrollOffset, + scrollBar.Maximum - scrollBar.LargeChange + 1 + ); Invalidate(); } diff --git a/OpenNest/Controls/FilterPanel.cs b/OpenNest/Controls/FilterPanel.cs index 5cc07e9..6e6e2da 100644 --- a/OpenNest/Controls/FilterPanel.cs +++ b/OpenNest/Controls/FilterPanel.cs @@ -1,10 +1,10 @@ -using OpenNest.Bending; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; +using OpenNest.Bending; +using OpenNest.Geometry; namespace OpenNest.Controls { @@ -41,13 +41,13 @@ namespace OpenNest.Controls HeaderText = "Bend Lines (0)", Dock = DockStyle.Top, ExpandedHeight = 120, - IsExpanded = false + IsExpanded = false, }; bendLinesList = new ListBox { Dock = DockStyle.Fill, BorderStyle = BorderStyle.None, - Font = new Font("Segoe UI", 9f) + Font = new Font("Segoe UI", 9f), }; bendLinesList.SelectedIndexChanged += (s, e) => BendLineSelected?.Invoke(this, bendLinesList.SelectedIndex); @@ -56,7 +56,7 @@ namespace OpenNest.Controls { Text = "Edit", AutoSize = true, - Font = new Font("Segoe UI", 8f) + Font = new Font("Segoe UI", 8f), }; bendEditLink.LinkClicked += (s, e) => { @@ -68,7 +68,7 @@ namespace OpenNest.Controls { Text = "Remove", AutoSize = true, - Font = new Font("Segoe UI", 8f) + Font = new Font("Segoe UI", 8f), }; bendDeleteLink.LinkClicked += (s, e) => { @@ -86,17 +86,16 @@ namespace OpenNest.Controls { Text = "Add Bend Line", AutoSize = true, - Font = new Font("Segoe UI", 8f) + Font = new Font("Segoe UI", 8f), }; - bendAddLink.LinkClicked += (s, e) => - AddBendLineClicked?.Invoke(this, EventArgs.Empty); + bendAddLink.LinkClicked += (s, e) => AddBendLineClicked?.Invoke(this, EventArgs.Empty); var bendLinksPanel = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 20, FlowDirection = FlowDirection.LeftToRight, - WrapContents = false + WrapContents = false, }; bendLinksPanel.Controls.Add(bendAddLink); bendLinksPanel.Controls.Add(bendEditLink); @@ -111,7 +110,7 @@ namespace OpenNest.Controls HeaderText = "Line Types (0)", Dock = DockStyle.Top, ExpandedHeight = 100, - IsExpanded = true + IsExpanded = true, }; lineTypesList = CreateCheckedList(); lineTypesPanel.ContentPanel.Controls.Add(lineTypesList); @@ -122,7 +121,7 @@ namespace OpenNest.Controls HeaderText = "Colors (0)", Dock = DockStyle.Top, ExpandedHeight = 100, - IsExpanded = true + IsExpanded = true, }; colorsList = new ListBox { @@ -131,7 +130,7 @@ namespace OpenNest.Controls Font = new Font("Segoe UI", 9f), DrawMode = DrawMode.OwnerDrawFixed, ItemHeight = 20, - SelectionMode = SelectionMode.None + SelectionMode = SelectionMode.None, }; colorsList.DrawItem += ColorsList_DrawItem; colorsList.MouseClick += ColorsList_MouseClick; @@ -143,12 +142,24 @@ namespace OpenNest.Controls HeaderText = "Layers", Dock = DockStyle.Top, ExpandedHeight = 160, - IsExpanded = true + IsExpanded = true, }; var checkAllPanel = new Panel { Dock = DockStyle.Top, Height = 22 }; - var checkAll = new LinkLabel { Text = "All", AutoSize = true, Location = new Point(4, 2), Font = new Font("Segoe UI", 8f) }; - var uncheckAll = new LinkLabel { Text = "None", AutoSize = true, Location = new Point(30, 2), Font = new Font("Segoe UI", 8f) }; + var checkAll = new LinkLabel + { + Text = "All", + AutoSize = true, + Location = new Point(4, 2), + Font = new Font("Segoe UI", 8f), + }; + var uncheckAll = new LinkLabel + { + Text = "None", + AutoSize = true, + Location = new Point(30, 2), + Font = new Font("Segoe UI", 8f), + }; checkAll.LinkClicked += (s, e) => SetAllChecked(layersList, true); uncheckAll.LinkClicked += (s, e) => SetAllChecked(layersList, false); checkAllPanel.Controls.AddRange(new Control[] { checkAll, uncheckAll }); @@ -171,7 +182,7 @@ namespace OpenNest.Controls Dock = DockStyle.Fill, BorderStyle = BorderStyle.None, CheckOnClick = true, - Font = new Font("Segoe UI", 9f) + Font = new Font("Segoe UI", 9f), }; list.ItemCheck += (s, e) => { @@ -258,7 +269,8 @@ namespace OpenNest.Controls foreach (var entity in entities) { - var layerVisible = entity.Layer?.Name == null || !hiddenLayers.Contains(entity.Layer.Name); + var layerVisible = + entity.Layer?.Name == null || !hiddenLayers.Contains(entity.Layer.Name); var colorVisible = !hiddenColors.Contains(entity.Color.ToArgb()); var ltVisible = !hiddenLineTypes.Contains(entity.LineTypeName ?? "Continuous"); @@ -277,7 +289,8 @@ namespace OpenNest.Controls private void ColorsList_MouseClick(object sender, MouseEventArgs e) { var index = colorsList.IndexFromPoint(e.Location); - if (index < 0) return; + if (index < 0) + return; var item = (ColorItem)colorsList.Items[index]; item.IsChecked = !item.IsChecked; colorsList.Invalidate(colorsList.GetItemRectangle(index)); @@ -286,18 +299,25 @@ namespace OpenNest.Controls private void ColorsList_DrawItem(object sender, DrawItemEventArgs e) { - if (e.Index < 0) return; + if (e.Index < 0) + return; e.Graphics.FillRectangle(Brushes.White, e.Bounds); var colorItem = (ColorItem)colorsList.Items[e.Index]; - var checkSize = CheckBoxRenderer.GetGlyphSize(e.Graphics, - System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal); + var checkSize = CheckBoxRenderer.GetGlyphSize( + e.Graphics, + System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal + ); var checkY = e.Bounds.Top + (e.Bounds.Height - checkSize.Height) / 2; var checkState = colorItem.IsChecked ? System.Windows.Forms.VisualStyles.CheckBoxState.CheckedNormal : System.Windows.Forms.VisualStyles.CheckBoxState.UncheckedNormal; - CheckBoxRenderer.DrawCheckBox(e.Graphics, new Point(e.Bounds.Left + 2, checkY), checkState); + CheckBoxRenderer.DrawCheckBox( + e.Graphics, + new Point(e.Bounds.Left + 2, checkY), + checkState + ); var swatchX = e.Bounds.Left + checkSize.Width + 6; var swatchRect = new Rectangle(swatchX, e.Bounds.Top + 2, 16, e.Bounds.Height - 4); @@ -305,8 +325,13 @@ namespace OpenNest.Controls e.Graphics.FillRectangle(brush, swatchRect); e.Graphics.DrawRectangle(Pens.Gray, swatchRect); - TextRenderer.DrawText(e.Graphics, colorItem.ToString(), e.Font, - new Point(swatchRect.Right + 4, e.Bounds.Top + 1), SystemColors.WindowText); + TextRenderer.DrawText( + e.Graphics, + colorItem.ToString(), + e.Font, + new Point(swatchRect.Right + 4, e.Bounds.Top + 1), + SystemColors.WindowText + ); } public void SetPickMode(bool active) @@ -329,7 +354,9 @@ namespace OpenNest.Controls } public override string ToString() => $"#{Color.R:X2}{Color.G:X2}{Color.B:X2}"; + public override bool Equals(object obj) => obj is ColorItem other && Argb == other.Argb; + public override int GetHashCode() => Argb; } } diff --git a/OpenNest/Controls/LayoutViewGL.cs b/OpenNest/Controls/LayoutViewGL.cs index ae08d9e..3572c38 100644 --- a/OpenNest/Controls/LayoutViewGL.cs +++ b/OpenNest/Controls/LayoutViewGL.cs @@ -1,12 +1,12 @@ -using System.Runtime.Remoting.Messaging; +using System; +using System.Drawing; +using System.Runtime.Remoting.Messaging; +using System.Windows.Forms; using libPep; using libPep.Codes; -using OpenTK.Graphics.OpenGL; -using System; -using System.Drawing; -using System.Windows.Forms; using OpenNest.Geometry; using OpenNest.Math; +using OpenTK.Graphics.OpenGL; namespace OpenNest.Controls { @@ -294,14 +294,10 @@ namespace OpenNest.Controls } // start angle in radians - var startAngle = System.Math.Atan2( - curpos.Y - center.Y, - curpos.X - center.X); + var startAngle = System.Math.Atan2(curpos.Y - center.Y, curpos.X - center.X); // end angle in radians - var endAngle = System.Math.Atan2( - endpt.Y - center.Y, - endpt.X - center.X); + var endAngle = System.Math.Atan2(endpt.Y - center.Y, endpt.X - center.X); endAngle = NormalizeAngle(endAngle); startAngle = NormalizeAngle(startAngle); @@ -340,7 +336,8 @@ namespace OpenNest.Controls { GL.Vertex2( System.Math.Cos(startAngle + angle * i) * radius + center.X, - System.Math.Sin(startAngle + angle * i) * radius + center.Y); + System.Math.Sin(startAngle + angle * i) * radius + center.Y + ); } GL.End(); @@ -355,9 +352,9 @@ namespace OpenNest.Controls { GL.Vertex2( System.Math.Cos(i) * radius + center.X, - System.Math.Sin(i) * radius + center.Y); + System.Math.Sin(i) * radius + center.Y + ); } - } private static double NormalizeAngle(double angle) diff --git a/OpenNest/Controls/NumericUpDown.cs b/OpenNest/Controls/NumericUpDown.cs index bc9557a..29bcbbf 100644 --- a/OpenNest/Controls/NumericUpDown.cs +++ b/OpenNest/Controls/NumericUpDown.cs @@ -6,8 +6,6 @@ namespace OpenNest.Controls { private string suffix; - - public NumericUpDown() { suffix = string.Empty; diff --git a/OpenNest/Controls/PhaseStepperControl.cs b/OpenNest/Controls/PhaseStepperControl.cs index 7e4a0ee..a3478f9 100644 --- a/OpenNest/Controls/PhaseStepperControl.cs +++ b/OpenNest/Controls/PhaseStepperControl.cs @@ -27,7 +27,12 @@ namespace OpenNest.Controls public PhaseStepperControl() { DoubleBuffered = true; - SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true); + SetStyle( + ControlStyles.OptimizedDoubleBuffer + | ControlStyles.AllPaintingInWmPaint + | ControlStyles.UserPaint, + true + ); Height = 60; } @@ -67,7 +72,8 @@ namespace OpenNest.Controls g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; var count = Phases.Length; - if (count == 0) return; + if (count == 0) + return; var padding = 30; var usableWidth = Width - padding * 2; @@ -102,25 +108,41 @@ namespace OpenNest.Controls if (isActive) { // Glow - g.FillEllipse(glowBrush, - cx - activeRadius - 3, circleY - activeRadius - 3, - (activeRadius + 3) * 2, (activeRadius + 3) * 2); + g.FillEllipse( + glowBrush, + cx - activeRadius - 3, + circleY - activeRadius - 3, + (activeRadius + 3) * 2, + (activeRadius + 3) * 2 + ); // Filled circle - g.FillEllipse(accentBrush, - cx - activeRadius, circleY - activeRadius, - activeRadius * 2, activeRadius * 2); + g.FillEllipse( + accentBrush, + cx - activeRadius, + circleY - activeRadius, + activeRadius * 2, + activeRadius * 2 + ); } else if (isVisited) { - g.FillEllipse(accentBrush, - cx - normalRadius, circleY - normalRadius, - normalRadius * 2, normalRadius * 2); + g.FillEllipse( + accentBrush, + cx - normalRadius, + circleY - normalRadius, + normalRadius * 2, + normalRadius * 2 + ); } else { - g.DrawEllipse(pendingPen, - cx - normalRadius, circleY - normalRadius, - normalRadius * 2, normalRadius * 2); + g.DrawEllipse( + pendingPen, + cx - normalRadius, + circleY - normalRadius, + normalRadius * 2, + normalRadius * 2 + ); } // Label @@ -128,8 +150,13 @@ namespace OpenNest.Controls var font = isVisited || isActive ? BoldLabelFont : LabelFont; var brush = isVisited || isActive ? activeTextBrush : pendingTextBrush; var labelSize = g.MeasureString(label, font); - g.DrawString(label, font, brush, - cx - labelSize.Width / 2, circleY + activeRadius + 5); + g.DrawString( + label, + font, + brush, + cx - labelSize.Width / 2, + circleY + activeRadius + 5 + ); } } } diff --git a/OpenNest/Controls/PlateRenderer.cs b/OpenNest/Controls/PlateRenderer.cs index ef5f1b5..94cefba 100644 --- a/OpenNest/Controls/PlateRenderer.cs +++ b/OpenNest/Controls/PlateRenderer.cs @@ -1,12 +1,12 @@ -using OpenNest.Bending; -using OpenNest.CNC; -using OpenNest.Geometry; -using OpenNest.Math; using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; +using OpenNest.Bending; +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.Math; namespace OpenNest.Controls { @@ -25,13 +25,17 @@ namespace OpenNest.Controls var plateRect = new RectangleF { Width = view.LengthWorldToGui(plate.Size.Length), - Height = view.LengthWorldToGui(plate.Size.Width) + Height = view.LengthWorldToGui(plate.Size.Width), }; var edgeSpacingRect = new RectangleF { - Width = view.LengthWorldToGui(plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right), - Height = view.LengthWorldToGui(plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom) + Width = view.LengthWorldToGui( + plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right + ), + Height = view.LengthWorldToGui( + plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom + ), }; switch (plate.Quadrant) @@ -40,28 +44,35 @@ namespace OpenNest.Controls plateRect.Location = view.PointWorldToGraph(0, 0); edgeSpacingRect.Location = view.PointWorldToGraph( plate.EdgeSpacing.Left, - plate.EdgeSpacing.Bottom); + plate.EdgeSpacing.Bottom + ); break; case 2: plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, 0); edgeSpacingRect.Location = view.PointWorldToGraph( plate.EdgeSpacing.Left - plate.Size.Length, - plate.EdgeSpacing.Bottom); + plate.EdgeSpacing.Bottom + ); break; case 3: - plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, -plate.Size.Width); + plateRect.Location = view.PointWorldToGraph( + -plate.Size.Length, + -plate.Size.Width + ); edgeSpacingRect.Location = view.PointWorldToGraph( plate.EdgeSpacing.Left - plate.Size.Length, - plate.EdgeSpacing.Bottom - plate.Size.Width); + plate.EdgeSpacing.Bottom - plate.Size.Width + ); break; case 4: plateRect.Location = view.PointWorldToGraph(0, -plate.Size.Width); edgeSpacingRect.Location = view.PointWorldToGraph( plate.EdgeSpacing.Left, - plate.EdgeSpacing.Bottom - plate.Size.Width); + plate.EdgeSpacing.Bottom - plate.Size.Width + ); break; default: @@ -77,18 +88,22 @@ namespace OpenNest.Controls if (!edgeSpacingRect.Contains(viewBounds)) { - g.DrawRectangle(view.ColorScheme.EdgeSpacingPen, - edgeSpacingRect.X, - edgeSpacingRect.Y, - edgeSpacingRect.Width, - edgeSpacingRect.Height); + g.DrawRectangle( + view.ColorScheme.EdgeSpacingPen, + edgeSpacingRect.X, + edgeSpacingRect.Y, + edgeSpacingRect.Width, + edgeSpacingRect.Height + ); } - g.DrawRectangle(view.ColorScheme.LayoutOutlinePen, + g.DrawRectangle( + view.ColorScheme.LayoutOutlinePen, plateRect.X, plateRect.Y, plateRect.Width, - plateRect.Height); + plateRect.Height + ); } public void DrawParts(Graphics g) @@ -172,8 +187,10 @@ namespace OpenNest.Controls for (var i = 0; i < program.Codes.Count - 1; i += 2) { - if (program.Codes[i] is RapidMove rapid && - program.Codes[i + 1] is LinearMove linear) + if ( + program.Codes[i] is RapidMove rapid + && program.Codes[i + 1] is LinearMove linear + ) { DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen); } @@ -191,14 +208,11 @@ namespace OpenNest.Controls { Location = view.PointWorldToGraph(workArea.Location), Width = view.LengthWorldToGui(workArea.Length), - Height = view.LengthWorldToGui(workArea.Width) + Height = view.LengthWorldToGui(workArea.Width), }; rect.Y -= rect.Height; - using var pen = new Pen(Color.Red, 1.5f) - { - DashStyle = DashStyle.Dash - }; + using var pen = new Pen(Color.Red, 1.5f) { DashStyle = DashStyle.Dash }; g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height); } @@ -230,9 +244,10 @@ namespace OpenNest.Controls var h = view.LengthWorldToGui(box.Width); var rect = new RectangleF(loc.X, loc.Y - h, w, h); - var priority = view.DebugRemnantPriorities != null && i < view.DebugRemnantPriorities.Count - ? System.Math.Min(view.DebugRemnantPriorities[i], 2) - : 0; + var priority = + view.DebugRemnantPriorities != null && i < view.DebugRemnantPriorities.Count + ? System.Math.Min(view.DebugRemnantPriorities[i], 2) + : 0; using var brush = new SolidBrush(PriorityFills[priority]); g.FillRectangle(brush, rect); @@ -242,19 +257,27 @@ namespace OpenNest.Controls var label = $"P{priority} {box.Width:F1}x{box.Length:F1}"; using var font = new Font("Segoe UI", 8f); - using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using var sf = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center, + }; g.DrawString(label, font, Brushes.Black, rect, sf); } } private void DrawBendLines(Graphics g, Part part) { - if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) + if ( + !view.ShowBendLines + || part.BaseDrawing.Bends == null + || part.BaseDrawing.Bends.Count == 0 + ) return; using var bendPen = new Pen(Color.Yellow, 1.5f) { - DashStyle = System.Drawing.Drawing2D.DashStyle.Dash + DashStyle = System.Drawing.Drawing2D.DashStyle.Dash, }; foreach (var bend in part.BaseDrawing.Bends) @@ -280,7 +303,11 @@ namespace OpenNest.Controls private void DrawEtchMarks(Graphics g, Part part) { - if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) + if ( + !view.ShowBendLines + || part.BaseDrawing.Bends == null + || part.BaseDrawing.Bends.Count == 0 + ) return; using var etchPen = new Pen(Color.Green, 1.5f); @@ -331,7 +358,12 @@ namespace OpenNest.Controls private void DrawGrainWarning(Graphics g, Part part) { var plate = view.Plate; - if (!view.ShowBendLines || plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0) + if ( + !view.ShowBendLines + || plate == null + || part.BaseDrawing.Bends == null + || part.BaseDrawing.Bends.Count == 0 + ) return; var grainAngle = plate.GrainAngle; @@ -341,10 +373,12 @@ namespace OpenNest.Controls { var bendAngle = bend.LineAngle + part.Rotation; bendAngle = bendAngle % System.Math.PI; - if (bendAngle < 0) bendAngle += System.Math.PI; + if (bendAngle < 0) + bendAngle += System.Math.PI; var grainNormalized = grainAngle % System.Math.PI; - if (grainNormalized < 0) grainNormalized += System.Math.PI; + if (grainNormalized < 0) + grainNormalized += System.Math.PI; var diff = System.Math.Abs(bendAngle - grainNormalized); diff = System.Math.Min(diff, System.Math.PI - diff); @@ -354,11 +388,17 @@ namespace OpenNest.Controls var box = part.BaseDrawing.Program.BoundingBox(); var location = part.Location; var pt1 = view.PointWorldToGraph(location); - var pt2 = view.PointWorldToGraph(new Vector( - location.X + box.Length, location.Y + box.Width)); + var pt2 = view.PointWorldToGraph( + new Vector(location.X + box.Length, location.Y + box.Width) + ); using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f); - g.DrawRectangle(warnPen, pt1.X, pt2.Y, - System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y)); + g.DrawRectangle( + warnPen, + pt1.X, + pt2.Y, + System.Math.Abs(pt2.X - pt1.X), + System.Math.Abs(pt2.Y - pt1.Y) + ); return; } } @@ -415,7 +455,14 @@ namespace OpenNest.Controls } } - private void DrawProgramPiercePoints(Graphics g, Program pgm, Vector basePos, ref Vector pos, Brush brush, Pen pen) + private void DrawProgramPiercePoints( + Graphics g, + Program pgm, + Vector basePos, + ref Vector pos, + Brush brush, + Pen pen + ) { for (var i = 0; i < pgm.Length; ++i) { @@ -434,11 +481,13 @@ namespace OpenNest.Controls else { var motion = code as Motion; - if (motion == null) continue; + if (motion == null) + continue; - var endpt = pgm.Mode == Mode.Incremental - ? motion.EndPoint + pos - : motion.EndPoint + basePos; + var endpt = + pgm.Mode == Mode.Incremental + ? motion.EndPoint + pos + : motion.EndPoint + basePos; if (code.Type == CodeType.RapidMove) { @@ -465,7 +514,15 @@ namespace OpenNest.Controls var part = view.Plate.Parts[i]; var pgm = part.Program; var pos = part.Location; - CutDirectionArrows.DrawProgram(g, view, pgm, ref pos, pen, arrowSpacingWorld, arrowSize); + CutDirectionArrows.DrawProgram( + g, + view, + pgm, + ref pos, + pen, + arrowSpacingWorld, + arrowSize + ); } } @@ -483,10 +540,16 @@ namespace OpenNest.Controls { Location = view.PointWorldToGraph(box.Location), Width = view.LengthWorldToGui(box.Length), - Height = view.LengthWorldToGui(box.Width) + Height = view.LengthWorldToGui(box.Width), }; - g.DrawRectangle(view.ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height); + g.DrawRectangle( + view.ColorScheme.BoundingBoxPen, + rect.X, + rect.Y - rect.Height, + rect.Width, + rect.Height + ); } } } diff --git a/OpenNest/Controls/PlateView.cs b/OpenNest/Controls/PlateView.cs index 189cba8..12b40b8 100644 --- a/OpenNest/Controls/PlateView.cs +++ b/OpenNest/Controls/PlateView.cs @@ -1,10 +1,4 @@ -using OpenNest.Actions; -using OpenNest.Collections; -using OpenNest.Engine.Fill; -using OpenNest.Forms; -using OpenNest.Geometry; -using OpenNest.Math; -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; @@ -14,6 +8,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using OpenNest.Actions; +using OpenNest.Collections; +using OpenNest.Engine.Fill; +using OpenNest.Forms; +using OpenNest.Geometry; +using OpenNest.Math; using Timer = System.Timers.Timer; namespace OpenNest.Controls @@ -80,9 +80,7 @@ namespace OpenNest.Controls } public PlateView() - : this(ColorScheme.Default) - { - } + : this(ColorScheme.Default) { } public PlateView(ColorScheme colorScheme) { @@ -97,7 +95,7 @@ namespace OpenNest.Controls { AutoReset = false, Enabled = true, - Interval = 50 + Interval = 50, }; redrawTimer.Elapsed += redrawTimer_Elapsed; @@ -105,9 +103,11 @@ namespace OpenNest.Controls hoverTimer.Elapsed += hoverTimer_Elapsed; SetStyle( - ControlStyles.AllPaintingInWmPaint | - ControlStyles.OptimizedDoubleBuffer | - ControlStyles.UserPaint, true); + ControlStyles.AllPaintingInWmPaint + | ControlStyles.OptimizedDoubleBuffer + | ControlStyles.UserPaint, + true + ); ViewScale = 1.0f; RotateIncrementAngle = 10; @@ -163,8 +163,7 @@ namespace OpenNest.Controls internal Brush PreviewBrush => previewManager.PreviewBrush; internal Pen PreviewPen => previewManager.PreviewPen; - internal RectangleF GetViewBounds() => - new RectangleF(-origin.X, -origin.Y, Width, Height); + internal RectangleF GetViewBounds() => new RectangleF(-origin.X, -origin.Y, Width, Height); internal PlateRenderer Renderer => renderer; @@ -248,7 +247,8 @@ namespace OpenNest.Controls protected override void OnMouseDown(MouseEventArgs e) { - if (!Focused) Focus(); + if (!Focused) + Focus(); if (e.Button == MouseButtons.Middle) middleMouseDownPoint = e.Location; @@ -304,9 +304,10 @@ namespace OpenNest.Controls if (SelectedParts.Count > 0 && ((ModifierKeys & Keys.Shift) == Keys.Shift)) { - var increment = (ModifierKeys & Keys.Control) == Keys.Control - ? RotateIncrementAngle * 0.1 - : RotateIncrementAngle; + var increment = + (ModifierKeys & Keys.Control) == Keys.Control + ? RotateIncrementAngle * 0.1 + : RotateIncrementAngle; var angle = Angle.ToRadians((e.Delta > 0 ? -increment : increment) * multiplier); @@ -317,9 +318,15 @@ namespace OpenNest.Controls if (AllowZoom) { if (e.Delta > 0) - ZoomToControlPoint(e.Location, (float)System.Math.Pow(ZoomInFactor, multiplier)); + ZoomToControlPoint( + e.Location, + (float)System.Math.Pow(ZoomInFactor, multiplier) + ); else - ZoomToControlPoint(e.Location, (float)System.Math.Pow(ZoomOutFactor, multiplier)); + ZoomToControlPoint( + e.Location, + (float)System.Math.Pow(ZoomOutFactor, multiplier) + ); } } @@ -519,18 +526,27 @@ namespace OpenNest.Controls Invalidate(); } - public CutOff GetCutOffAtPoint(Vector point, double tolerance) => cutOffHandler.GetCutOffAtPoint(point, tolerance); + public CutOff GetCutOffAtPoint(Vector point, double tolerance) => + cutOffHandler.GetCutOffAtPoint(point, tolerance); public LayoutPart GetPartAtControlPoint(Point pt) => selection.GetPartAtControlPoint(pt); + public LayoutPart GetPartAtGraphPoint(PointF pt) => selection.GetPartAtGraphPoint(pt); + public LayoutPart GetPartAtPoint(Vector pt) => selection.GetPartAtPoint(pt); - public IList GetPartsFromWindow(RectangleF rect, SelectionType selectionType) => selection.GetPartsFromWindow(rect, selectionType); + + public IList GetPartsFromWindow(RectangleF rect, SelectionType selectionType) => + selection.GetPartsFromWindow(rect, selectionType); public void SetAction(Type type) => actionManager.SetAction(type); - public void SetAction(Type type, params object[] args) => actionManager.SetAction(type, args); + + public void SetAction(Type type, params object[] args) => + actionManager.SetAction(type, args); public void AlignSelected(AlignType alignType) => selection.AlignSelected(alignType); - public void AlignSelected(AlignType alignType, LayoutPart fixedPart) => selection.AlignSelected(alignType, fixedPart); + + public void AlignSelected(AlignType alignType, LayoutPart fixedPart) => + selection.AlignSelected(alignType, fixedPart); public void AddPartFromDrawing(Drawing dwg, Vector location) { @@ -538,15 +554,21 @@ namespace OpenNest.Controls part.Offset( part.Location.X - part.BoundingBox.Center.X, - part.Location.Y - part.BoundingBox.Center.Y); + part.Location.Y - part.BoundingBox.Center.Y + ); Plate.Parts.Add(part); } - public void SetStationaryParts(List parts) => previewManager.SetStationaryParts(parts); + public void SetStationaryParts(List parts) => + previewManager.SetStationaryParts(parts); + public void SetActiveParts(List parts) => previewManager.SetActiveParts(parts); + public void ClearPreviewParts() => previewManager.ClearPreviewParts(); - public void AcceptPreviewParts(List parts) => previewManager.AcceptPreviewParts(parts); + + public void AcceptPreviewParts(List parts) => + previewManager.AcceptPreviewParts(parts); public async void FillWithProgress(List groupParts, Box workArea) { @@ -621,16 +643,17 @@ namespace OpenNest.Controls public void RemoveSelectedParts() => selection.RemoveSelectedParts(); - private void redrawTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { - if (IsDisposed || !IsHandleCreated) return; + if (IsDisposed || !IsHandleCreated) + return; BeginInvoke(new System.Action(Invalidate)); } private void hoverTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { - if (IsDisposed || !IsHandleCreated) return; + if (IsDisposed || !IsHandleCreated) + return; BeginInvoke(new System.Action(HoverCheck)); } @@ -641,8 +664,7 @@ namespace OpenNest.Controls for (var i = parts.Count - 1; i >= 0; --i) { - if (parts[i].Path.GetBounds().Contains(graphPt) && - parts[i].Path.IsVisible(graphPt)) + if (parts[i].Path.GetBounds().Contains(graphPt) && parts[i].Path.IsVisible(graphPt)) { hitPart = parts[i]; break; @@ -674,7 +696,9 @@ namespace OpenNest.Controls } public void DeselectAll() => selection.DeselectAll(); + public void SelectAll() => selection.SelectAll(); + public void NotifySelectionChanged() => selection.NotifySelectionChanged(); public override void ZoomToPoint(Vector pt, float zoomFactor, bool redraw = true) @@ -685,7 +709,13 @@ namespace OpenNest.Controls Invalidate(); } - public override void ZoomToArea(double x, double y, double width, double height, bool redraw = true) + public override void ZoomToArea( + double x, + double y, + double width, + double height, + bool redraw = true + ) { base.ZoomToArea(x, y, width, height, false); diff --git a/OpenNest/Controls/PreviewManager.cs b/OpenNest/Controls/PreviewManager.cs index e6dc9ed..62b7f95 100644 --- a/OpenNest/Controls/PreviewManager.cs +++ b/OpenNest/Controls/PreviewManager.cs @@ -18,10 +18,14 @@ namespace OpenNest.Controls activeParts.Count > 0 ? activeParts : stationaryParts; public Brush PreviewBrush => - activeParts.Count > 0 ? view.ColorScheme.ActivePreviewPartBrush : view.ColorScheme.PreviewPartBrush; + activeParts.Count > 0 + ? view.ColorScheme.ActivePreviewPartBrush + : view.ColorScheme.PreviewPartBrush; public Pen PreviewPen => - activeParts.Count > 0 ? view.ColorScheme.ActivePreviewPartPen : view.ColorScheme.PreviewPartPen; + activeParts.Count > 0 + ? view.ColorScheme.ActivePreviewPartPen + : view.ColorScheme.PreviewPartPen; public void SetStationaryParts(List parts) { diff --git a/OpenNest/Controls/ProgramEditorControl.cs b/OpenNest/Controls/ProgramEditorControl.cs index 0b026aa..2077b9c 100644 --- a/OpenNest/Controls/ProgramEditorControl.cs +++ b/OpenNest/Controls/ProgramEditorControl.cs @@ -1,12 +1,12 @@ -using OpenNest.CNC; -using OpenNest.Converters; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Text.RegularExpressions; using System.Windows.Forms; +using OpenNest.CNC; +using OpenNest.Converters; +using OpenNest.Geometry; namespace OpenNest.Controls { @@ -107,7 +107,8 @@ namespace OpenNest.Controls foreach (var contour in contours) { var sub = ConvertGeometry.ToProgram(contour.Shape); - if (sub == null) continue; + if (sub == null) + continue; sb.AppendLine(); sb.AppendLine($"; {contour.Label} ({contour.DirectionLabel})"); @@ -120,18 +121,24 @@ namespace OpenNest.Controls { if (!lastWasRapid) sb.AppendLine(); - sb.AppendLine($"G00 X{FormatCoord(rapid.EndPoint.X)} Y{FormatCoord(rapid.EndPoint.Y)}"); + sb.AppendLine( + $"G00 X{FormatCoord(rapid.EndPoint.X)} Y{FormatCoord(rapid.EndPoint.Y)}" + ); lastWasRapid = true; } else if (code is ArcMove arc) { var g = arc.Rotation == RotationType.CW ? "G02" : "G03"; - sb.AppendLine($"{g} X{FormatCoord(arc.EndPoint.X)} Y{FormatCoord(arc.EndPoint.Y)} I{FormatCoord(arc.CenterPoint.X)} J{FormatCoord(arc.CenterPoint.Y)}"); + sb.AppendLine( + $"{g} X{FormatCoord(arc.EndPoint.X)} Y{FormatCoord(arc.EndPoint.Y)} I{FormatCoord(arc.CenterPoint.X)} J{FormatCoord(arc.CenterPoint.Y)}" + ); lastWasRapid = false; } else if (code is LinearMove linear) { - sb.AppendLine($"G01 X{FormatCoord(linear.EndPoint.X)} Y{FormatCoord(linear.EndPoint.Y)}"); + sb.AppendLine( + $"G01 X{FormatCoord(linear.EndPoint.X)} Y{FormatCoord(linear.EndPoint.Y)}" + ); lastWasRapid = false; } } @@ -142,13 +149,16 @@ namespace OpenNest.Controls private static string FormatCoord(double value) { - return System.Math.Round(value, 4).ToString("0.####", System.Globalization.CultureInfo.InvariantCulture); + return System + .Math.Round(value, 4) + .ToString("0.####", System.Globalization.CultureInfo.InvariantCulture); } private void ApplyHighlighting() { var text = gcodeEditor.Text; - if (string.IsNullOrEmpty(text)) return; + if (string.IsNullOrEmpty(text)) + return; gcodeEditor.SuspendLayout(); @@ -236,16 +246,17 @@ namespace OpenNest.Controls private void OnDrawContourItem(object sender, DrawItemEventArgs e) { - if (e.Index < 0 || e.Index >= contours.Count) return; + if (e.Index < 0 || e.Index >= contours.Count) + return; var contour = contours[e.Index]; var selected = (e.State & DrawItemState.Selected) != 0; var bounds = e.Bounds; // Background - using var bgBrush = new SolidBrush(selected - ? Color.FromArgb(230, 238, 255) - : Color.White); + using var bgBrush = new SolidBrush( + selected ? Color.FromArgb(230, 238, 255) : Color.White + ); e.Graphics.FillRectangle(bgBrush, bounds); // Accent bar @@ -268,7 +279,13 @@ namespace OpenNest.Controls // Label using var labelFont = new Font("Segoe UI", 9f, FontStyle.Bold); using var labelBrush = new SolidBrush(Color.FromArgb(40, 40, 40)); - e.Graphics.DrawString(contour.Label, labelFont, labelBrush, bounds.X + 32, bounds.Y + 4); + e.Graphics.DrawString( + contour.Label, + labelFont, + labelBrush, + bounds.X + 32, + bounds.Y + 4 + ); // Info line var info = $"{contour.DirectionLabel} \u00B7 {contour.DimensionLabel}"; @@ -278,7 +295,13 @@ namespace OpenNest.Controls // Separator using var sepPen = new Pen(Color.FromArgb(230, 230, 230)); - e.Graphics.DrawLine(sepPen, bounds.X + 8, bounds.Bottom - 1, bounds.Right - 8, bounds.Bottom - 1); + e.Graphics.DrawLine( + sepPen, + bounds.X + 8, + bounds.Bottom - 1, + bounds.Right - 8, + bounds.Bottom - 1 + ); } private void OnContourSelectionChanged(object sender, EventArgs e) @@ -288,7 +311,8 @@ namespace OpenNest.Controls private void OnReverseClicked(object sender, EventArgs e) { - if (contourList.SelectedIndices.Count == 0) return; + if (contourList.SelectedIndices.Count == 0) + return; foreach (int index in contourList.SelectedIndices) { @@ -319,8 +343,10 @@ namespace OpenNest.Controls private void OnMoveUpClicked(object sender, EventArgs e) { var index = contourList.SelectedIndex; - if (index <= 0) return; - if (contours[index].Type == ContourClassification.Perimeter) return; + if (index <= 0) + return; + if (contours[index].Type == ContourClassification.Perimeter) + return; (contours[index], contours[index - 1]) = (contours[index - 1], contours[index]); RebuildAfterReorder(index - 1); @@ -329,9 +355,12 @@ namespace OpenNest.Controls private void OnMoveDownClicked(object sender, EventArgs e) { var index = contourList.SelectedIndex; - if (index < 0 || index >= contours.Count - 1) return; - if (contours[index].Type == ContourClassification.Perimeter) return; - if (contours[index + 1].Type == ContourClassification.Perimeter) return; + if (index < 0 || index >= contours.Count - 1) + return; + if (contours[index].Type == ContourClassification.Perimeter) + return; + if (contours[index + 1].Type == ContourClassification.Perimeter) + return; (contours[index], contours[index + 1]) = (contours[index + 1], contours[index]); RebuildAfterReorder(index + 1); @@ -341,10 +370,14 @@ namespace OpenNest.Controls { // Nearest-neighbor sort for non-perimeter contours var perimeterIndex = contours.FindIndex(c => c.Type == ContourClassification.Perimeter); - if (perimeterIndex < 0) return; + if (perimeterIndex < 0) + return; - var nonPerimeter = contours.Where(c => c.Type != ContourClassification.Perimeter).ToList(); - if (nonPerimeter.Count <= 1) return; + var nonPerimeter = contours + .Where(c => c.Type != ContourClassification.Perimeter) + .ToList(); + if (nonPerimeter.Count <= 1) + return; var sorted = new List(); var remaining = new List(nonPerimeter); @@ -444,7 +477,8 @@ namespace OpenNest.Controls private void OnPreviewPaintOverlay(Graphics g) { - if (contours.Count == 0) return; + if (contours.Count == 0) + return; var spacing = preview.LengthGuiToWorld(60f); var arrowSize = 8f; @@ -453,16 +487,17 @@ namespace OpenNest.Controls for (var i = 0; i < contours.Count; i++) { - if (!contourList.SelectedIndices.Contains(i)) continue; + if (!contourList.SelectedIndices.Contains(i)) + continue; var contour = contours[i]; var pgm = ConvertGeometry.ToProgram(contour.Shape); - if (pgm == null) continue; + if (pgm == null) + continue; var pos = new Vector(); CutDirectionArrows.DrawProgram(g, preview, pgm, ref pos, pen, spacing, arrowSize); } } - } } diff --git a/OpenNest/Controls/QuadrantSelect.cs b/OpenNest/Controls/QuadrantSelect.cs index 9c6c80a..29e8133 100644 --- a/OpenNest/Controls/QuadrantSelect.cs +++ b/OpenNest/Controls/QuadrantSelect.cs @@ -7,9 +7,7 @@ namespace OpenNest.Controls { private int quadrant; - public QuadrantSelect() - { - } + public QuadrantSelect() { } public int Quadrant { @@ -60,8 +58,9 @@ namespace OpenNest.Controls new StringFormat() { Alignment = StringAlignment.Center, - LineAlignment = StringAlignment.Center - }); + LineAlignment = StringAlignment.Center, + } + ); } protected override void OnMouseClick(MouseEventArgs e) diff --git a/OpenNest/Controls/SelectionManager.cs b/OpenNest/Controls/SelectionManager.cs index 47ab709..7690b66 100644 --- a/OpenNest/Controls/SelectionManager.cs +++ b/OpenNest/Controls/SelectionManager.cs @@ -1,9 +1,9 @@ -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; using System.Linq; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Controls { @@ -96,10 +96,16 @@ namespace OpenNest.Controls switch (alignType) { case AlignType.Bottom: - Align.Bottom(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList()); + Align.Bottom( + fixedPart.BasePart, + selectedParts.Select(p => p.BasePart).ToList() + ); break; case AlignType.Horizontally: - Align.Horizontally(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList()); + Align.Horizontally( + fixedPart.BasePart, + selectedParts.Select(p => p.BasePart).ToList() + ); break; case AlignType.Left: Align.Left(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList()); @@ -111,13 +117,20 @@ namespace OpenNest.Controls Align.Top(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList()); break; case AlignType.Vertically: - Align.Vertically(fixedPart.BasePart, selectedParts.Select(p => p.BasePart).ToList()); + Align.Vertically( + fixedPart.BasePart, + selectedParts.Select(p => p.BasePart).ToList() + ); break; case AlignType.EvenlySpaceHorizontally: - Align.EvenlyDistributeHorizontally(selectedParts.Select(p => p.BasePart).ToList()); + Align.EvenlyDistributeHorizontally( + selectedParts.Select(p => p.BasePart).ToList() + ); break; case AlignType.EvenlySpaceVertically: - Align.EvenlyDistributeVertically(selectedParts.Select(p => p.BasePart).ToList()); + Align.EvenlyDistributeVertically( + selectedParts.Select(p => p.BasePart).ToList() + ); break; default: return; diff --git a/OpenNest/Controls/ShapePreviewControl.cs b/OpenNest/Controls/ShapePreviewControl.cs index 6ddde19..0358065 100644 --- a/OpenNest/Controls/ShapePreviewControl.cs +++ b/OpenNest/Controls/ShapePreviewControl.cs @@ -63,14 +63,16 @@ namespace OpenNest.Controls private void PaintInfo(Graphics g) { - if (infoLines == null) return; + if (infoLines == null) + return; var lineHeight = Font.GetHeight(g) + 1; var y = 4f; foreach (var line in infoLines) { - if (string.IsNullOrEmpty(line)) continue; + if (string.IsNullOrEmpty(line)) + continue; g.DrawString(line, Font, Brushes.Black, 4, y); y += lineHeight; } diff --git a/OpenNest/Document.cs b/OpenNest/Document.cs index 05a3e98..d2ec600 100644 --- a/OpenNest/Document.cs +++ b/OpenNest/Document.cs @@ -1,6 +1,6 @@ -using OpenNest.IO; using System; using System.IO; +using OpenNest.IO; namespace OpenNest { diff --git a/OpenNest/Forms/AutoNestForm.cs b/OpenNest/Forms/AutoNestForm.cs index 36d2448..3067350 100644 --- a/OpenNest/Forms/AutoNestForm.cs +++ b/OpenNest/Forms/AutoNestForm.cs @@ -94,49 +94,61 @@ namespace OpenNest.Forms private void SetupPartsGrid() { - partsGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "DrawingName", - HeaderText = "Drawing Name", - Width = 160, - ReadOnly = true, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); - partsGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "Quantity", - HeaderText = "Qty", - Width = 50, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); - partsGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "Priority", - HeaderText = "Priority", - Width = 55, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); - partsGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "RotationStart", - HeaderText = "Rot Start", - Width = 65, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); - partsGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "RotationEnd", - HeaderText = "Rot End", - Width = 60, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); - partsGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "StepAngle", - HeaderText = "Step", - Width = 55, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); + partsGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "DrawingName", + HeaderText = "Drawing Name", + Width = 160, + ReadOnly = true, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); + partsGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "Quantity", + HeaderText = "Qty", + Width = 50, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); + partsGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "Priority", + HeaderText = "Priority", + Width = 55, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); + partsGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "RotationStart", + HeaderText = "Rot Start", + Width = 65, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); + partsGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "RotationEnd", + HeaderText = "Rot End", + Width = 60, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); + partsGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "StepAngle", + HeaderText = "Step", + Width = 55, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); partsGrid.CellValueChanged += PartsGrid_CellValueChanged; partsGrid.CurrentCellDirtyStateChanged += (s, e) => @@ -148,20 +160,24 @@ namespace OpenNest.Forms private void SetupPlateGrid() { - plateGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "Size", - HeaderText = "Size", - Width = 120, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); - plateGrid.Columns.Add(new DataGridViewTextBoxColumn - { - DataPropertyName = "Cost", - HeaderText = "Cost", - Width = 70, - AutoSizeMode = DataGridViewAutoSizeColumnMode.None, - }); + plateGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "Size", + HeaderText = "Size", + Width = 120, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); + plateGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + DataPropertyName = "Cost", + HeaderText = "Cost", + Width = 70, + AutoSizeMode = DataGridViewAutoSizeColumnMode.None, + } + ); plateGrid.CellValidating += PlateGrid_CellValidating; } @@ -208,7 +224,8 @@ namespace OpenNest.Forms { var result = new List(); var gridItems = plateGrid.DataSource as List; - if (gridItems == null) return result; + if (gridItems == null) + return result; foreach (var item in gridItems) { @@ -217,12 +234,14 @@ namespace OpenNest.Forms if (width <= 0 || length <= 0) continue; - result.Add(new PlateOption - { - Width = width, - Length = length, - Cost = item.Cost, - }); + result.Add( + new PlateOption + { + Width = width, + Length = length, + Cost = item.Cost, + } + ); } return result; @@ -232,11 +251,13 @@ namespace OpenNest.Forms { if (options != null && options.Count > 0) { - var items = options.Select(o => new PlateOptionItem - { - Size = FormatSize(o.Width, o.Length), - Cost = o.Cost, - }).ToList(); + var items = options + .Select(o => new PlateOptionItem + { + Size = FormatSize(o.Width, o.Length), + Cost = o.Cost, + }) + .ToList(); plateGrid.DataSource = items; optimizePlateSizeBox.Checked = true; } @@ -302,7 +323,8 @@ namespace OpenNest.Forms private void PartsGrid_CellValueChanged(object sender, DataGridViewCellEventArgs e) { - if (e.RowIndex < 0) return; + if (e.RowIndex < 0) + return; if (partsGrid.Columns[e.ColumnIndex].DataPropertyName == "Quantity") UpdateSummary(); } @@ -331,11 +353,19 @@ namespace OpenNest.Forms { width = 0; length = 0; - if (string.IsNullOrWhiteSpace(value)) return false; + if (string.IsNullOrWhiteSpace(value)) + return false; var match = SizePattern.Match(value.Trim()); - if (!match.Success) return false; - width = double.Parse(match.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture); - length = double.Parse(match.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture); + if (!match.Success) + return false; + width = double.Parse( + match.Groups[1].Value, + System.Globalization.CultureInfo.InvariantCulture + ); + length = double.Parse( + match.Groups[2].Value, + System.Globalization.CultureInfo.InvariantCulture + ); return true; } @@ -346,8 +376,10 @@ namespace OpenNest.Forms private void PartsGrid_DataError(object sender, DataGridViewDataErrorEventArgs e) { - MessageBox.Show("Invalid input. Expected input type is " + - partsGrid[e.ColumnIndex, e.RowIndex].ValueType.Name); + MessageBox.Show( + "Invalid input. Expected input type is " + + partsGrid[e.ColumnIndex, e.RowIndex].ValueType.Name + ); } private DataGridViewItem GetDataGridViewItem(Drawing dwg) diff --git a/OpenNest/Forms/BendLineDialog.cs b/OpenNest/Forms/BendLineDialog.cs index 130119a..39fc7ab 100644 --- a/OpenNest/Forms/BendLineDialog.cs +++ b/OpenNest/Forms/BendLineDialog.cs @@ -1,7 +1,7 @@ -using OpenNest.Bending; using System; using System.Drawing; using System.Windows.Forms; +using OpenNest.Bending; namespace OpenNest.Forms { @@ -24,19 +24,31 @@ namespace OpenNest.Forms var font = new Font("Segoe UI", 9f); // Direction - var lblDir = new Label { Text = "Direction:", Location = new Point(12, 15), AutoSize = true, Font = font }; + var lblDir = new Label + { + Text = "Direction:", + Location = new Point(12, 15), + AutoSize = true, + Font = font, + }; cboDirection = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Location = new Point(100, 12), Width = 130, - Font = font + Font = font, }; cboDirection.Items.AddRange(new object[] { "Down", "Up" }); cboDirection.SelectedIndex = 0; // Angle - var lblAngle = new Label { Text = "Angle:", Location = new Point(12, 47), AutoSize = true, Font = font }; + var lblAngle = new Label + { + Text = "Angle:", + Location = new Point(12, 47), + AutoSize = true, + Font = font, + }; numAngle = new NumericUpDown { Location = new Point(100, 44), @@ -45,11 +57,17 @@ namespace OpenNest.Forms Minimum = 0, Maximum = 180, DecimalPlaces = 1, - Value = 90 + Value = 90, }; // Radius (with checkbox to enable) - chkRadius = new CheckBox { Text = "Radius:", Location = new Point(12, 79), AutoSize = true, Font = font }; + chkRadius = new CheckBox + { + Text = "Radius:", + Location = new Point(12, 79), + AutoSize = true, + Font = font, + }; numRadius = new NumericUpDown { Location = new Point(100, 76), @@ -59,7 +77,7 @@ namespace OpenNest.Forms Maximum = 25, DecimalPlaces = 3, Increment = 0.0625m, - Enabled = false + Enabled = false, }; chkRadius.CheckedChanged += (s, e) => numRadius.Enabled = chkRadius.Checked; @@ -70,7 +88,7 @@ namespace OpenNest.Forms DialogResult = DialogResult.OK, Location = new Point(62, 120), Size = new Size(80, 28), - Font = font + Font = font, }; var btnCancel = new Button { @@ -78,23 +96,29 @@ namespace OpenNest.Forms DialogResult = DialogResult.Cancel, Location = new Point(150, 120), Size = new Size(80, 28), - Font = font + Font = font, }; AcceptButton = btnOk; CancelButton = btnCancel; - Controls.AddRange(new Control[] { - lblDir, cboDirection, - lblAngle, numAngle, - chkRadius, numRadius, - btnOk, btnCancel - }); + Controls.AddRange( + new Control[] + { + lblDir, + cboDirection, + lblAngle, + numAngle, + chkRadius, + numRadius, + btnOk, + btnCancel, + } + ); } - public BendDirection Direction => cboDirection.SelectedIndex == 0 - ? BendDirection.Down - : BendDirection.Up; + public BendDirection Direction => + cboDirection.SelectedIndex == 0 ? BendDirection.Down : BendDirection.Up; public double BendAngle => (double)numAngle.Value; diff --git a/OpenNest/Forms/BestFitViewerForm.cs b/OpenNest/Forms/BestFitViewerForm.cs index 1851f23..cbdd1a7 100644 --- a/OpenNest/Forms/BestFitViewerForm.cs +++ b/OpenNest/Forms/BestFitViewerForm.cs @@ -1,6 +1,3 @@ -using OpenNest.Collections; -using OpenNest.Controls; -using OpenNest.Engine.BestFit; using System; using System.Collections.Generic; using System.Diagnostics; @@ -10,6 +7,9 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using OpenNest.Collections; +using OpenNest.Controls; +using OpenNest.Engine.BestFit; namespace OpenNest.Forms { @@ -18,7 +18,12 @@ namespace OpenNest.Forms private const int WM_SETREDRAW = 0x000B; [DllImport("user32.dll")] - private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + private static extern IntPtr SendMessage( + IntPtr hWnd, + int msg, + IntPtr wParam, + IntPtr lParam + ); private const int Columns = 5; private const int Rows = 3; @@ -47,7 +52,11 @@ namespace OpenNest.Forms public Drawing SelectedDrawing => activeDrawing; public List SelectedParts { get; private set; } - public BestFitViewerForm(DrawingCollection drawings, Plate plate, Units units = Units.Inches) + public BestFitViewerForm( + DrawingCollection drawings, + Plate plate, + Units units = Units.Inches + ) { this.drawings = drawings.ToList(); this.plate = plate; @@ -122,7 +131,10 @@ namespace OpenNest.Forms var width = plate.Size.Width; var spacing = plate.PartSpacing; - var result = await Task.Run(() => ComputeResults(drawing, length, width, spacing), cts.Token); + var result = await Task.Run( + () => ComputeResults(drawing, length, width, spacing), + cts.Token + ); if (cts.Token.IsCancellationRequested) return; @@ -132,13 +144,14 @@ namespace OpenNest.Forms keptCount = result.KeptCount; computeSeconds = result.ComputeSeconds; totalSeconds = result.TotalSeconds; - pageCount = System.Math.Max(1, (int)System.Math.Ceiling(results.Count / (double)ItemsPerPage)); + pageCount = System.Math.Max( + 1, + (int)System.Math.Ceiling(results.Count / (double)ItemsPerPage) + ); ShowPage(0); } - catch (OperationCanceledException) - { - } + catch (OperationCanceledException) { } finally { if (cts == computeCts) @@ -160,7 +173,10 @@ namespace OpenNest.Forms gridPanel.Controls.Clear(); lblLoading = null; EnsureLoadingLabel(); - lblLoading.Text = string.Format("Computing best fits for {0}...", activeDrawing.Name); + lblLoading.Text = string.Format( + "Computing best fits for {0}...", + activeDrawing.Name + ); gridPanel.ResumeLayout(true); } else @@ -181,14 +197,19 @@ namespace OpenNest.Forms TextAlign = ContentAlignment.MiddleCenter, ForeColor = Color.Gray, Font = new Font(Font.FontFamily, 14f), - Dock = DockStyle.Fill + Dock = DockStyle.Fill, }; gridPanel.Controls.Add(lblLoading, 0, 0); gridPanel.SetColumnSpan(lblLoading, Columns); gridPanel.SetRowSpan(lblLoading, Rows); } - private static ComputeResult ComputeResults(Drawing drawing, double length, double width, double spacing) + private static ComputeResult ComputeResults( + Drawing drawing, + double length, + double width, + double spacing + ) { var sw = Stopwatch.StartNew(); @@ -200,7 +221,8 @@ namespace OpenNest.Forms foreach (var r in all) { - if (r.Keep) kept++; + if (r.Keep) + kept++; } sw.Stop(); @@ -211,7 +233,7 @@ namespace OpenNest.Forms TotalResults = total, KeptCount = kept, ComputeSeconds = computeMs / 1000.0, - TotalSeconds = sw.Elapsed.TotalSeconds + TotalSeconds = sw.Elapsed.TotalSeconds, }; } @@ -252,9 +274,16 @@ namespace OpenNest.Forms txtPage.Text = (currentPage + 1).ToString(); lblPageCount.Text = string.Format("/ {0}", pageCount); - Text = string.Format("Best-Fit Viewer — {0} candidates ({1} kept) | Compute: {2:F1}s | Total: {3:F1}s | Showing {4}-{5} of {6}", - totalResults, keptCount, computeSeconds, totalSeconds, - start + 1, start + count, results.Count); + Text = string.Format( + "Best-Fit Viewer — {0} candidates ({1} kept) | Compute: {2:F1}s | Total: {3:F1}s | Showing {4}-{5} of {6}", + totalResults, + keptCount, + computeSeconds, + totalSeconds, + start + 1, + start + count, + results.Count + ); } private void btnPrev_Click(object sender, EventArgs e) => NavigatePage(-1); @@ -264,7 +293,14 @@ namespace OpenNest.Forms private void CenterNavControls() { var gap = 6; - var groupWidth = btnPrev.Width + gap + txtPage.Width + gap + lblPageCount.Width + gap + btnNext.Width; + var groupWidth = + btnPrev.Width + + gap + + txtPage.Width + + gap + + lblPageCount.Width + + gap + + btnNext.Width; var x = (navPanel.Width - groupWidth) / 2; var midY = navPanel.Height / 2; @@ -313,7 +349,7 @@ namespace OpenNest.Forms BoundingBoxColor = bgColor, RapidColor = Color.DodgerBlue, OriginColor = bgColor, - EdgeSpacingColor = bgColor + EdgeSpacingColor = bgColor, }; var cell = new BestFitCell(colorScheme); @@ -321,9 +357,7 @@ namespace OpenNest.Forms cell.Dock = DockStyle.Fill; var parts = result.BuildCanonicalParts(); - cell.Plate.Size = new Geometry.Size( - result.BoundingHeight, - result.BoundingWidth); + cell.Plate.Size = new Geometry.Size(result.BoundingHeight, result.BoundingWidth); foreach (var part in parts) cell.Plate.Parts.Add(part); diff --git a/OpenNest/Forms/BomImportForm.cs b/OpenNest/Forms/BomImportForm.cs index 7218d85..46df805 100644 --- a/OpenNest/Forms/BomImportForm.cs +++ b/OpenNest/Forms/BomImportForm.cs @@ -1,6 +1,3 @@ -using OpenNest.Geometry; -using OpenNest.IO; -using OpenNest.IO.Bom; using System; using System.Collections.Generic; using System.Data; @@ -8,6 +5,9 @@ using System.Drawing; using System.IO; using System.Linq; using System.Windows.Forms; +using OpenNest.Geometry; +using OpenNest.IO; +using OpenNest.IO.Bom; namespace OpenNest.Forms { @@ -108,8 +108,12 @@ namespace OpenNest.Forms { if (!File.Exists(txtBomFile.Text)) { - MessageBox.Show("BOM file does not exist.", "Validation Error", - MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show( + "BOM file does not exist.", + "Validation Error", + MessageBoxButtons.OK, + MessageBoxIcon.Warning + ); return; } @@ -129,8 +133,12 @@ namespace OpenNest.Forms } catch (Exception ex) { - MessageBox.Show($"Error reading BOM: {ex.Message}", "Error", - MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show( + $"Error reading BOM: {ex.Message}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); } } @@ -139,9 +147,9 @@ namespace OpenNest.Forms var matchedPaths = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var group in analysis.Groups) - foreach (var part in group.Parts) - if (part.DxfPath != null) - matchedPaths[part.Item.FileName ?? ""] = part.DxfPath; + foreach (var part in group.Parts) + if (part.DxfPath != null) + matchedPaths[part.Item.FileName ?? ""] = part.DxfPath; _parts = new List(); @@ -165,8 +173,10 @@ namespace OpenNest.Forms else { var lookupName = item.FileName; - if (lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) - || lookupName.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)) + if ( + lookupName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) + || lookupName.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) + ) lookupName = Path.GetFileNameWithoutExtension(lookupName); if (matchedPaths.TryGetValue(lookupName, out var dxfPath)) @@ -278,17 +288,21 @@ namespace OpenNest.Forms // Save existing settings before rebuilding SaveGroupSettings(); - var defaultWidth = double.TryParse(txtPlateWidth.Text, out var w) ? w : _templateDefaults.Size.Width; - var defaultLength = double.TryParse(txtPlateLength.Text, out var l) ? l : _templateDefaults.Size.Length; + var defaultWidth = double.TryParse(txtPlateWidth.Text, out var w) + ? w + : _templateDefaults.Size.Width; + var defaultLength = double.TryParse(txtPlateLength.Text, out var l) + ? l + : _templateDefaults.Size.Length; var groups = _parts - .Where(p => p.IsEditable - && !string.IsNullOrWhiteSpace(p.Material) - && p.Thickness.HasValue) + .Where(p => + p.IsEditable && !string.IsNullOrWhiteSpace(p.Material) && p.Thickness.HasValue + ) .GroupBy(p => new { Material = p.Material.ToUpperInvariant(), - Thickness = p.Thickness.Value + Thickness = p.Thickness.Value, }) .OrderBy(g => g.First().Material) .ThenBy(g => g.Key.Thickness) @@ -358,19 +372,31 @@ namespace OpenNest.Forms _groupSettings[key] = new GroupSettings { - PlateWidth = row["Plate Width"] is double pw ? pw : _templateDefaults.Size.Width, - PlateLength = row["Plate Length"] is double pl ? pl : _templateDefaults.Size.Length, - PartSpacing = row["Part Spacing"] is double ps ? ps : _templateDefaults.PartSpacing, - EdgeLeft = row["Edge Left"] is double el ? el : _templateDefaults.EdgeSpacing.Left, - EdgeBottom = row["Edge Bottom"] is double eb ? eb : _templateDefaults.EdgeSpacing.Bottom, - EdgeRight = row["Edge Right"] is double er ? er : _templateDefaults.EdgeSpacing.Right, + PlateWidth = row["Plate Width"] is double pw + ? pw + : _templateDefaults.Size.Width, + PlateLength = row["Plate Length"] is double pl + ? pl + : _templateDefaults.Size.Length, + PartSpacing = row["Part Spacing"] is double ps + ? ps + : _templateDefaults.PartSpacing, + EdgeLeft = row["Edge Left"] is double el + ? el + : _templateDefaults.EdgeSpacing.Left, + EdgeBottom = row["Edge Bottom"] is double eb + ? eb + : _templateDefaults.EdgeSpacing.Bottom, + EdgeRight = row["Edge Right"] is double er + ? er + : _templateDefaults.EdgeSpacing.Right, EdgeTop = row["Edge Top"] is double et ? et : _templateDefaults.EdgeSpacing.Top, }; } } - private static string GroupKey(string material, double thickness) - => $"{material?.ToUpperInvariant()}|{thickness}"; + private static string GroupKey(string material, double thickness) => + $"{material?.ToUpperInvariant()}|{thickness}"; #endregion @@ -388,9 +414,10 @@ namespace OpenNest.Forms if (noDxf > 0) summaryParts.Add($"{noDxf} no DXF found"); - lblSummary.Text = summaryParts.Count > 0 - ? string.Join(", ", summaryParts) - : $"{matched} parts matched"; + lblSummary.Text = + summaryParts.Count > 0 + ? string.Join(", ", summaryParts) + : $"{matched} parts matched"; } #endregion @@ -405,25 +432,35 @@ namespace OpenNest.Forms // Save latest group edits SaveGroupSettings(); - var defaultWidth = double.TryParse(txtPlateWidth.Text, out var dw) ? dw : _templateDefaults.Size.Width; - var defaultLength = double.TryParse(txtPlateLength.Text, out var dl) ? dl : _templateDefaults.Size.Length; + var defaultWidth = double.TryParse(txtPlateWidth.Text, out var dw) + ? dw + : _templateDefaults.Size.Width; + var defaultLength = double.TryParse(txtPlateLength.Text, out var dl) + ? dl + : _templateDefaults.Size.Length; var groups = _parts - .Where(p => p.IsEditable + .Where(p => + p.IsEditable && !string.IsNullOrWhiteSpace(p.Material) && p.Thickness.HasValue - && !string.IsNullOrWhiteSpace(p.DxfPath)) + && !string.IsNullOrWhiteSpace(p.DxfPath) + ) .GroupBy(p => new { Material = p.Material.ToUpperInvariant(), - Thickness = p.Thickness.Value + Thickness = p.Thickness.Value, }) .ToList(); if (groups.Count == 0) { - MessageBox.Show("No groups with matched DXF files to create nests from.", "Nothing to Create", - MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show( + "No groups with matched DXF files to create nests from.", + "Nothing to Create", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); return; } @@ -455,7 +492,12 @@ namespace OpenNest.Forms nest.Material = new Material(material); nest.PlateDefaults.Quadrant = _templateDefaults.Quadrant; nest.PlateDefaults.PartSpacing = partSpacing; - nest.PlateDefaults.EdgeSpacing = new Spacing(edgeLeft, edgeBottom, edgeRight, edgeTop); + nest.PlateDefaults.EdgeSpacing = new Spacing( + edgeLeft, + edgeBottom, + edgeRight, + edgeTop + ); foreach (var part in group) { @@ -467,8 +509,10 @@ namespace OpenNest.Forms try { - var drawing = CadImporter.ImportDrawing(part.DxfPath, - new CadImportOptions { Quantity = part.Qty ?? 1 }); + var drawing = CadImporter.ImportDrawing( + part.DxfPath, + new CadImportOptions { Quantity = part.Qty ?? 1 } + ); drawing.Material = new Material(material); nest.Drawings.Add(drawing); } @@ -493,10 +537,16 @@ namespace OpenNest.Forms var summary = $"{nestsCreated} nest{(nestsCreated != 1 ? "s" : "")} created."; if (importErrors.Count > 0) - summary += $"\n\n{importErrors.Count} import error(s):\n" + string.Join("\n", importErrors); + summary += + $"\n\n{importErrors.Count} import error(s):\n" + + string.Join("\n", importErrors); - MessageBox.Show(summary, "Import Complete", MessageBoxButtons.OK, - importErrors.Count > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); + MessageBox.Show( + summary, + "Import Complete", + MessageBoxButtons.OK, + importErrors.Count > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information + ); Close(); } diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index 9a8e9cb..12647f5 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -1,10 +1,3 @@ -using OpenNest.Bending; -using OpenNest.CNC; -using OpenNest.Controls; -using OpenNest.Converters; -using OpenNest.Geometry; -using OpenNest.IO; -using OpenNest.IO.Bending; using System; using System.Collections.Generic; using System.Drawing; @@ -13,12 +6,18 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using OpenNest.Bending; +using OpenNest.CNC; +using OpenNest.Controls; +using OpenNest.Converters; +using OpenNest.Geometry; +using OpenNest.IO; +using OpenNest.IO.Bending; namespace OpenNest.Forms { public partial class CadConverterForm : Form { - private SimplifierViewerForm simplifierViewer; private bool staleProgram = true; @@ -168,9 +167,7 @@ namespace OpenNest.Forms txtCustomer.Text = item.Customer ?? ""; var bounds = item.Bounds; - lblDimensions.Text = bounds != null - ? $"{bounds.Width:0.#} x {bounds.Length:0.#}" - : ""; + lblDimensions.Text = bounds != null ? $"{bounds.Width:0.#} x {bounds.Length:0.#}" : ""; lblEntityCount.Text = $"{item.EntityCount} entities"; entityView1.ZoomToFit(); @@ -183,27 +180,33 @@ namespace OpenNest.Forms // Only check original (unsimplified) entities var entities = item.OriginalEntities ?? item.Entities; - if (entities == null || entities.Count < 10) return; + if (entities == null || entities.Count < 10) + return; // Quick line count check — need at least MinLines consecutive lines var lineCount = entities.Count(e => e is Geometry.Line); - if (lineCount < 3) return; + if (lineCount < 3) + return; // Run a quick analysis on a background thread var capturedEntities = new List(entities); Task.Run(() => - { - var shapes = ShapeBuilder.GetShapes(capturedEntities); - var simplifier = new GeometrySimplifier(); - var count = 0; - foreach (var shape in shapes) - count += simplifier.Analyze(shape).Count; - return count; - }).ContinueWith(t => - { - if (t.IsCompletedSuccessfully && t.Result > 0) - HighlightSimplifyButton(t.Result); - }, TaskScheduler.FromCurrentSynchronizationContext()); + { + var shapes = ShapeBuilder.GetShapes(capturedEntities); + var simplifier = new GeometrySimplifier(); + var count = 0; + foreach (var shape in shapes) + count += simplifier.Analyze(shape).Count; + return count; + }) + .ContinueWith( + t => + { + if (t.IsCompletedSuccessfully && t.Result > 0) + HighlightSimplifyButton(t.Result); + }, + TaskScheduler.FromCurrentSynchronizationContext() + ); } private void HighlightSimplifyButton(int candidateCount) @@ -233,7 +236,8 @@ namespace OpenNest.Forms private void OnFilterChanged(object sender, EventArgs e) { var item = CurrentItem; - if (item == null) return; + if (item == null) + return; filterPanel.ApplyFilters(item.Entities); ReHidePromotedEntities(item.Bends); @@ -280,7 +284,8 @@ namespace OpenNest.Forms private void OnBendLineRemoved(object sender, int index) { var item = CurrentItem; - if (item == null || index < 0 || index >= item.Bends.Count) return; + if (item == null || index < 0 || index >= item.Bends.Count) + return; var bend = item.Bends[index]; if (bend.SourceEntity != null) @@ -299,13 +304,15 @@ namespace OpenNest.Forms private void OnBendLineEdited(object sender, int index) { var item = CurrentItem; - if (item == null || index < 0 || index >= item.Bends.Count) return; + if (item == null || index < 0 || index >= item.Bends.Count) + return; var bend = item.Bends[index]; using var dialog = new BendLineDialog(); dialog.LoadBend(bend); - if (dialog.ShowDialog(this) != DialogResult.OK) return; + if (dialog.ShowDialog(this) != DialogResult.OK) + return; bend.Direction = dialog.Direction; bend.Angle = dialog.BendAngle; @@ -322,7 +329,8 @@ namespace OpenNest.Forms private void OnQuantityChanged(object sender, EventArgs e) { var item = CurrentItem; - if (item == null) return; + if (item == null) + return; item.Quantity = (int)numQuantity.Value; fileList.Invalidate(); @@ -344,10 +352,12 @@ namespace OpenNest.Forms private void OnSplitClicked(object sender, EventArgs e) { var item = CurrentItem; - if (item == null) return; + if (item == null) + return; var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList(); - if (entities.Count == 0) return; + if (entities.Count == 0) + return; var normalized = ShapeProfile.NormalizeEntities(entities); var pgm = ConvertGeometry.ToProgram(normalized); @@ -360,15 +370,23 @@ namespace OpenNest.Forms } var drawing = new Drawing(item.Name, pgm); - drawing.Bends = item.Bends.Select(b => new Bend - { - StartPoint = new Vector(b.StartPoint.X - originOffset.X, b.StartPoint.Y - originOffset.Y), - EndPoint = new Vector(b.EndPoint.X - originOffset.X, b.EndPoint.Y - originOffset.Y), - Direction = b.Direction, - Angle = b.Angle, - Radius = b.Radius, - NoteText = b.NoteText, - }).ToList(); + drawing.Bends = item + .Bends.Select(b => new Bend + { + StartPoint = new Vector( + b.StartPoint.X - originOffset.X, + b.StartPoint.Y - originOffset.Y + ), + EndPoint = new Vector( + b.EndPoint.X - originOffset.X, + b.EndPoint.Y - originOffset.Y + ), + Direction = b.Direction, + Angle = b.Angle, + Radius = b.Radius, + NoteText = b.NoteText, + }) + .ToList(); using var form = new SplitDrawingForm(drawing); if (form.ShowDialog(this) != DialogResult.OK || form.ResultDrawings?.Count <= 1) @@ -377,9 +395,10 @@ namespace OpenNest.Forms // Write split DXF files and re-import var sourceDir = Path.GetDirectoryName(item.Path); var baseName = Path.GetFileNameWithoutExtension(item.Path); - var writableDir = Directory.Exists(sourceDir) && IsDirectoryWritable(sourceDir) - ? sourceDir - : Path.GetTempPath(); + var writableDir = + Directory.Exists(sourceDir) && IsDirectoryWritable(sourceDir) + ? sourceDir + : Path.GetTempPath(); var index = fileList.SelectedIndex; @@ -407,7 +426,7 @@ namespace OpenNest.Forms Customer = item.Customer, Bends = splitDrawing.Bends ?? new List(), Bounds = result.Entities.GetBoundingBox(), - EntityCount = result.Entities.Count + EntityCount = result.Entities.Count, }; splitItems.Add(splitItem); } @@ -418,8 +437,12 @@ namespace OpenNest.Forms fileList.AddItem(splitItem); if (writableDir != sourceDir) - MessageBox.Show($"Split files written to: {writableDir}", "Split Output", - MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show( + $"Split files written to: {writableDir}", + "Split Output", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); } private void OnAddBendLineClicked(object sender, EventArgs e) @@ -436,7 +459,8 @@ namespace OpenNest.Forms return; var item = CurrentItem; - if (item == null) return; + if (item == null) + return; var bend = new Bend { @@ -445,7 +469,7 @@ namespace OpenNest.Forms Direction = dialog.Direction, Angle = dialog.BendAngle, Radius = dialog.BendRadius, - SourceEntity = line + SourceEntity = line, }; line.IsVisible = false; @@ -467,10 +491,12 @@ namespace OpenNest.Forms private void OnTextConvertRequested(object sender, Controls.CadText text) { var item = CurrentItem; - if (item == null) return; + if (item == null) + return; var font = LoadChrFont(); - if (font == null) return; + if (font == null) + return; var layer = new Geometry.Layer("ENGRAVE") { @@ -484,13 +510,15 @@ namespace OpenNest.Forms var box = entities.GetBoundingBox(); var shiftX = text.HAlign switch { - System.Drawing.StringAlignment.Center => text.Position.X - (box.Left + box.Right) / 2, + System.Drawing.StringAlignment.Center => text.Position.X + - (box.Left + box.Right) / 2, System.Drawing.StringAlignment.Far => text.Position.X - box.Right, _ => text.Position.X - box.Left, }; var shiftY = text.VAlign switch { - System.Drawing.StringAlignment.Center => text.Position.Y - (box.Top + box.Bottom) / 2, + System.Drawing.StringAlignment.Center => text.Position.Y + - (box.Top + box.Bottom) / 2, System.Drawing.StringAlignment.Near => text.Position.Y - box.Top, _ => text.Position.Y - box.Bottom, }; @@ -500,8 +528,12 @@ namespace OpenNest.Forms } if (entities.Count == 0) { - MessageBox.Show($"No geometry produced for \"{text.Value}\".", "Convert Text", - MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show( + $"No geometry produced for \"{text.Value}\".", + "Convert Text", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); return; } @@ -556,8 +588,12 @@ namespace OpenNest.Forms } catch (Exception ex) { - MessageBox.Show($"Error loading font: {ex.Message}", "Font Error", - MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show( + $"Error loading font: {ex.Message}", + "Font Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); return null; } } @@ -584,9 +620,12 @@ namespace OpenNest.Forms if (e.Data.GetDataPresent(DataFormats.FileDrop)) { var files = (string[])e.Data.GetData(DataFormats.FileDrop); - var dxfFiles = files.Where(f => - f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) || - f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase)).ToArray(); + var dxfFiles = files + .Where(f => + f.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".dwg", StringComparison.OrdinalIgnoreCase) + ) + .ToArray(); if (dxfFiles.Length > 0) AddFiles(dxfFiles); } @@ -617,7 +656,8 @@ namespace OpenNest.Forms var screen = Screen.FromControl(this); simplifierViewer.Location = new Point( System.Math.Min(Right, screen.WorkingArea.Right - simplifierViewer.Width), - Top); + Top + ); } simplifierViewer.LoadShapes(shapes, entityView1); @@ -658,21 +698,24 @@ namespace OpenNest.Forms private void OnExportDxfClick(object sender, EventArgs e) { var item = CurrentItem; - if (item == null) return; + if (item == null) + return; using var dlg = new SaveFileDialog { - Filter = "DXF 2018 (*.dxf)|*.dxf|" + - "DXF 2013 (*.dxf)|*.dxf|" + - "DXF 2010 (*.dxf)|*.dxf|" + - "DXF 2007 (*.dxf)|*.dxf|" + - "DXF 2004 (*.dxf)|*.dxf|" + - "DXF 2000 (*.dxf)|*.dxf|" + - "DXF R14 (*.dxf)|*.dxf", + Filter = + "DXF 2018 (*.dxf)|*.dxf|" + + "DXF 2013 (*.dxf)|*.dxf|" + + "DXF 2010 (*.dxf)|*.dxf|" + + "DXF 2007 (*.dxf)|*.dxf|" + + "DXF 2004 (*.dxf)|*.dxf|" + + "DXF 2000 (*.dxf)|*.dxf|" + + "DXF R14 (*.dxf)|*.dxf", FileName = Path.ChangeExtension(item.Name, ".dxf"), }; - if (dlg.ShowDialog() != DialogResult.OK) return; + if (dlg.ShowDialog() != DialogResult.OK) + return; var version = dlg.FilterIndex switch { @@ -691,11 +734,17 @@ namespace OpenNest.Forms switch (entity) { case Geometry.Line line: - doc.Entities.Add(new ACadSharp.Entities.Line - { - StartPoint = new CSMath.XYZ(line.StartPoint.X, line.StartPoint.Y, 0), - EndPoint = new CSMath.XYZ(line.EndPoint.X, line.EndPoint.Y, 0), - }); + doc.Entities.Add( + new ACadSharp.Entities.Line + { + StartPoint = new CSMath.XYZ( + line.StartPoint.X, + line.StartPoint.Y, + 0 + ), + EndPoint = new CSMath.XYZ(line.EndPoint.X, line.EndPoint.Y, 0), + } + ); break; case Geometry.Arc arc: @@ -703,21 +752,25 @@ namespace OpenNest.Forms var endAngle = arc.EndAngle; if (arc.IsReversed) OpenNest.Math.Generic.Swap(ref startAngle, ref endAngle); - doc.Entities.Add(new ACadSharp.Entities.Arc - { - Center = new CSMath.XYZ(arc.Center.X, arc.Center.Y, 0), - Radius = arc.Radius, - StartAngle = startAngle, - EndAngle = endAngle, - }); + doc.Entities.Add( + new ACadSharp.Entities.Arc + { + Center = new CSMath.XYZ(arc.Center.X, arc.Center.Y, 0), + Radius = arc.Radius, + StartAngle = startAngle, + EndAngle = endAngle, + } + ); break; case Geometry.Circle circle: - doc.Entities.Add(new ACadSharp.Entities.Circle - { - Center = new CSMath.XYZ(circle.Center.X, circle.Center.Y, 0), - Radius = circle.Radius, - }); + doc.Entities.Add( + new ACadSharp.Entities.Circle + { + Center = new CSMath.XYZ(circle.Center.X, circle.Center.Y, 0), + Radius = circle.Radius, + } + ); break; } } @@ -770,11 +823,12 @@ namespace OpenNest.Forms Quantity = drawing.Quantity.Required, Customer = drawing.Customer ?? string.Empty, Bends = drawing.Bends?.ToList() ?? new List(), - SuppressedEntityIds = drawing.SuppressedEntityIds.Count > 0 - ? new HashSet(drawing.SuppressedEntityIds) - : null, + SuppressedEntityIds = + drawing.SuppressedEntityIds.Count > 0 + ? new HashSet(drawing.SuppressedEntityIds) + : null, Bounds = bounds, - EntityCount = entities.Count + EntityCount = entities.Count, }; fileList.AddItem(item); @@ -791,9 +845,7 @@ namespace OpenNest.Forms foreach (var item in fileList.Items) { - var visible = item.Entities - .Where(e => e.Layer.IsVisible && e.IsVisible) - .ToList(); + var visible = item.Entities.Where(e => e.Layer.IsVisible && e.IsVisible).ToList(); if (visible.Count == 0) continue; @@ -809,9 +861,10 @@ namespace OpenNest.Forms Name = item.Name, }; - var editedProgram = (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null) - ? programEditor.Program - : null; + var editedProgram = + (item == CurrentItem && programEditor.IsDirty && programEditor.Program != null) + ? programEditor.Program + : null; var drawing = CadImporter.BuildDrawing( result, @@ -819,7 +872,8 @@ namespace OpenNest.Forms result.Bends, item.Quantity, item.Customer, - editedProgram); + editedProgram + ); drawings.Add(drawing); @@ -835,7 +889,8 @@ namespace OpenNest.Forms private static void ReHidePromotedEntities(List bends) { - if (bends == null) return; + if (bends == null) + return; foreach (var bend in bends) { if (bend.SourceEntity != null) @@ -855,9 +910,7 @@ namespace OpenNest.Forms } // If all entities on a layer are suppressed, uncheck the layer too - var layerGroups = item.Entities - .Where(e => e.Layer != null) - .GroupBy(e => e.Layer); + var layerGroups = item.Entities.Where(e => e.Layer != null).GroupBy(e => e.Layer); foreach (var group in layerGroups) { @@ -871,10 +924,11 @@ namespace OpenNest.Forms var bendSources = new HashSet( (item.Bends ?? new List()) .Where(b => b.SourceEntity != null) - .Select(b => b.SourceEntity)); + .Select(b => b.SourceEntity) + ); - var suppressed = item.Entities - .Where(e => !(e.Layer.IsVisible && e.IsVisible)) + var suppressed = item + .Entities.Where(e => !(e.Layer.IsVisible && e.IsVisible)) .Where(e => !bendSources.Contains(e)) .Select(e => e.Id); @@ -893,12 +947,16 @@ namespace OpenNest.Forms File.Delete(testFile); return true; } - catch { return false; } + catch + { + return false; + } } private static string GetUniquePath(string path) { - if (!File.Exists(path)) return path; + if (!File.Exists(path)) + return path; var dir = Path.GetDirectoryName(path); var name = Path.GetFileNameWithoutExtension(path); @@ -919,7 +977,8 @@ namespace OpenNest.Forms private static List ExtractTexts(ACadSharp.CadDocument doc) { var texts = new List(); - if (doc == null) return texts; + if (doc == null) + return texts; foreach (var entity in doc.Entities) { @@ -927,47 +986,66 @@ namespace OpenNest.Forms { case ACadSharp.Entities.MText mtext: var (mh, mv) = MapAttachmentPoint(mtext.AttachmentPoint); - texts.Add(new CadText - { - SourceHandle = mtext.Handle, - Position = new Vector(mtext.InsertPoint.X, mtext.InsertPoint.Y), - Value = ReplaceControlCodes(StripMTextFormatting(mtext.Value)), - Height = mtext.Height, - Rotation = mtext.Rotation, - LayerName = mtext.Layer?.Name, - HAlign = mh, - VAlign = mv, - }); + texts.Add( + new CadText + { + SourceHandle = mtext.Handle, + Position = new Vector(mtext.InsertPoint.X, mtext.InsertPoint.Y), + Value = ReplaceControlCodes(StripMTextFormatting(mtext.Value)), + Height = mtext.Height, + Rotation = mtext.Rotation, + LayerName = mtext.Layer?.Name, + HAlign = mh, + VAlign = mv, + } + ); break; case ACadSharp.Entities.TextEntity text: - var useAlignment = text.HorizontalAlignment != 0 - || text.VerticalAlignment != 0; + var useAlignment = + text.HorizontalAlignment != 0 || text.VerticalAlignment != 0; var pt = useAlignment ? text.AlignmentPoint : text.InsertPoint; var ha = text.HorizontalAlignment switch { - ACadSharp.Entities.TextHorizontalAlignment.Center => System.Drawing.StringAlignment.Center, - ACadSharp.Entities.TextHorizontalAlignment.Right => System.Drawing.StringAlignment.Far, + ACadSharp.Entities.TextHorizontalAlignment.Center => System + .Drawing + .StringAlignment + .Center, + ACadSharp.Entities.TextHorizontalAlignment.Right => System + .Drawing + .StringAlignment + .Far, _ => System.Drawing.StringAlignment.Near, }; var va = text.VerticalAlignment switch { - ACadSharp.Entities.TextVerticalAlignmentType.Middle => System.Drawing.StringAlignment.Center, - ACadSharp.Entities.TextVerticalAlignmentType.Top => System.Drawing.StringAlignment.Near, - ACadSharp.Entities.TextVerticalAlignmentType.Bottom => System.Drawing.StringAlignment.Far, + ACadSharp.Entities.TextVerticalAlignmentType.Middle => System + .Drawing + .StringAlignment + .Center, + ACadSharp.Entities.TextVerticalAlignmentType.Top => System + .Drawing + .StringAlignment + .Near, + ACadSharp.Entities.TextVerticalAlignmentType.Bottom => System + .Drawing + .StringAlignment + .Far, _ => System.Drawing.StringAlignment.Far, }; - texts.Add(new CadText - { - SourceHandle = text.Handle, - Position = new Vector(pt.X, pt.Y), - Value = ReplaceControlCodes(text.Value), - Height = text.Height, - Rotation = text.Rotation, - LayerName = text.Layer?.Name, - HAlign = ha, - VAlign = va, - }); + texts.Add( + new CadText + { + SourceHandle = text.Handle, + Position = new Vector(pt.X, pt.Y), + Value = ReplaceControlCodes(text.Value), + Height = text.Height, + Rotation = text.Rotation, + LayerName = text.Layer?.Name, + HAlign = ha, + VAlign = va, + } + ); break; } } @@ -975,27 +1053,41 @@ namespace OpenNest.Forms return texts; } - private static (System.Drawing.StringAlignment h, System.Drawing.StringAlignment v) MapAttachmentPoint( - ACadSharp.Entities.AttachmentPointType apt) + private static ( + System.Drawing.StringAlignment h, + System.Drawing.StringAlignment v + ) MapAttachmentPoint(ACadSharp.Entities.AttachmentPointType apt) { var h = apt switch { ACadSharp.Entities.AttachmentPointType.TopCenter - or ACadSharp.Entities.AttachmentPointType.MiddleCenter - or ACadSharp.Entities.AttachmentPointType.BottomCenter => System.Drawing.StringAlignment.Center, + or ACadSharp.Entities.AttachmentPointType.MiddleCenter + or ACadSharp.Entities.AttachmentPointType.BottomCenter => System + .Drawing + .StringAlignment + .Center, ACadSharp.Entities.AttachmentPointType.TopRight - or ACadSharp.Entities.AttachmentPointType.MiddleRight - or ACadSharp.Entities.AttachmentPointType.BottomRight => System.Drawing.StringAlignment.Far, + or ACadSharp.Entities.AttachmentPointType.MiddleRight + or ACadSharp.Entities.AttachmentPointType.BottomRight => System + .Drawing + .StringAlignment + .Far, _ => System.Drawing.StringAlignment.Near, }; var v = apt switch { ACadSharp.Entities.AttachmentPointType.MiddleLeft - or ACadSharp.Entities.AttachmentPointType.MiddleCenter - or ACadSharp.Entities.AttachmentPointType.MiddleRight => System.Drawing.StringAlignment.Center, + or ACadSharp.Entities.AttachmentPointType.MiddleCenter + or ACadSharp.Entities.AttachmentPointType.MiddleRight => System + .Drawing + .StringAlignment + .Center, ACadSharp.Entities.AttachmentPointType.BottomLeft - or ACadSharp.Entities.AttachmentPointType.BottomCenter - or ACadSharp.Entities.AttachmentPointType.BottomRight => System.Drawing.StringAlignment.Far, + or ACadSharp.Entities.AttachmentPointType.BottomCenter + or ACadSharp.Entities.AttachmentPointType.BottomRight => System + .Drawing + .StringAlignment + .Far, _ => System.Drawing.StringAlignment.Near, }; return (h, v); @@ -1003,17 +1095,22 @@ namespace OpenNest.Forms private static string StripMTextFormatting(string text) { - if (string.IsNullOrEmpty(text)) return text; - var result = System.Text.RegularExpressions.Regex.Replace(text, @"\\[A-Za-z][^;]*;", ""); + if (string.IsNullOrEmpty(text)) + return text; + var result = System.Text.RegularExpressions.Regex.Replace( + text, + @"\\[A-Za-z][^;]*;", + "" + ); result = result.Replace("{", "").Replace("}", ""); return result.Trim(); } private static string ReplaceControlCodes(string text) { - if (string.IsNullOrEmpty(text)) return text; - return text - .Replace("%%p", "±") + if (string.IsNullOrEmpty(text)) + return text; + return text.Replace("%%p", "±") .Replace("%%P", "±") .Replace("%%d", "°") .Replace("%%D", "°") @@ -1022,9 +1119,6 @@ namespace OpenNest.Forms .Replace("%%%", "%"); } - private void filterPanel_Paint(object sender, PaintEventArgs e) - { - - } + private void filterPanel_Paint(object sender, PaintEventArgs e) { } } } diff --git a/OpenNest/Forms/CutParametersForm.cs b/OpenNest/Forms/CutParametersForm.cs index 625e2b8..6385437 100644 --- a/OpenNest/Forms/CutParametersForm.cs +++ b/OpenNest/Forms/CutParametersForm.cs @@ -58,7 +58,7 @@ namespace OpenNest.Forms Feedrate = (double)numericUpDown1.Value, RapidTravelRate = (double)numericUpDown2.Value, PierceTime = TimeSpan.FromSeconds((double)numericUpDown3.Value), - Units = units + Units = units, }; } } diff --git a/OpenNest/Forms/CuttingParametersDialog.cs b/OpenNest/Forms/CuttingParametersDialog.cs index d26b4a3..d3490ac 100644 --- a/OpenNest/Forms/CuttingParametersDialog.cs +++ b/OpenNest/Forms/CuttingParametersDialog.cs @@ -1,7 +1,7 @@ -using OpenNest.CNC.CuttingStrategy; -using OpenNest.Controls; using System.Drawing; using System.Windows.Forms; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Controls; namespace OpenNest.Forms { @@ -18,23 +18,16 @@ namespace OpenNest.Forms MinimizeBox = false; StartPosition = FormStartPosition.CenterParent; - cuttingPanel = new CuttingPanel - { - Dock = DockStyle.Fill - }; + cuttingPanel = new CuttingPanel { Dock = DockStyle.Fill }; - var buttonPanel = new Panel - { - Dock = DockStyle.Bottom, - Height = 40 - }; + var buttonPanel = new Panel { Dock = DockStyle.Bottom, Height = 40 }; var btnOk = new Button { Text = "OK", DialogResult = DialogResult.OK, Size = new Size(80, 28), - Location = new Point(220, 6) + Location = new Point(220, 6), }; var btnCancel = new Button @@ -42,7 +35,7 @@ namespace OpenNest.Forms Text = "Cancel", DialogResult = DialogResult.Cancel, Size = new Size(80, 28), - Location = new Point(305, 6) + Location = new Point(305, 6), }; buttonPanel.Controls.Add(btnOk); diff --git a/OpenNest/Forms/CuttingParametersSerializer.cs b/OpenNest/Forms/CuttingParametersSerializer.cs index f66a225..f2c627e 100644 --- a/OpenNest/Forms/CuttingParametersSerializer.cs +++ b/OpenNest/Forms/CuttingParametersSerializer.cs @@ -1,5 +1,5 @@ -using OpenNest.CNC.CuttingStrategy; using System.Text.Json; +using OpenNest.CNC.CuttingStrategy; namespace OpenNest.Forms { @@ -8,7 +8,7 @@ namespace OpenNest.Forms private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = false, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; public static string Serialize(CuttingParameters p) @@ -27,7 +27,7 @@ namespace OpenNest.Forms RoundLeadInAngles = p.RoundLeadInAngles, LeadInAngleIncrement = p.LeadInAngleIncrement, AutoTabMinSize = p.AutoTabMinSize, - AutoTabMaxSize = p.AutoTabMaxSize + AutoTabMaxSize = p.AutoTabMaxSize, }; return JsonSerializer.Serialize(dto, JsonOptions); } @@ -50,9 +50,10 @@ namespace OpenNest.Forms TabConfig = new NormalTab { Size = dto.TabWidth }, PierceClearance = dto.PierceClearance, RoundLeadInAngles = dto.RoundLeadInAngles, - LeadInAngleIncrement = dto.LeadInAngleIncrement > 0 ? dto.LeadInAngleIncrement : 5.0, + LeadInAngleIncrement = + dto.LeadInAngleIncrement > 0 ? dto.LeadInAngleIncrement : 5.0, AutoTabMinSize = dto.AutoTabMinSize, - AutoTabMaxSize = dto.AutoTabMaxSize + AutoTabMaxSize = dto.AutoTabMaxSize, }; } @@ -60,26 +61,67 @@ namespace OpenNest.Forms { return leadIn switch { - LineLeadIn line => new LeadInDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle }, + LineLeadIn line => new LeadInDto + { + Type = "Line", + Length = line.Length, + ApproachAngle = line.ApproachAngle, + }, ArcLeadIn arc => new LeadInDto { Type = "Arc", Radius = arc.Radius }, - LineArcLeadIn la => new LeadInDto { Type = "LineArc", LineLength = la.LineLength, ArcRadius = la.ArcRadius, ApproachAngle = la.ApproachAngle }, - CleanHoleLeadIn ch => new LeadInDto { Type = "CleanHole", LineLength = ch.LineLength, ArcRadius = ch.ArcRadius, Kerf = ch.Kerf }, - LineLineLeadIn ll => new LeadInDto { Type = "LineLine", Length1 = ll.Length1, Angle1 = ll.ApproachAngle1, Length2 = ll.Length2, Angle2 = ll.ApproachAngle2 }, - _ => new LeadInDto { Type = "None" } + LineArcLeadIn la => new LeadInDto + { + Type = "LineArc", + LineLength = la.LineLength, + ArcRadius = la.ArcRadius, + ApproachAngle = la.ApproachAngle, + }, + CleanHoleLeadIn ch => new LeadInDto + { + Type = "CleanHole", + LineLength = ch.LineLength, + ArcRadius = ch.ArcRadius, + Kerf = ch.Kerf, + }, + LineLineLeadIn ll => new LeadInDto + { + Type = "LineLine", + Length1 = ll.Length1, + Angle1 = ll.ApproachAngle1, + Length2 = ll.Length2, + Angle2 = ll.ApproachAngle2, + }, + _ => new LeadInDto { Type = "None" }, }; } private static LeadIn FromDto(LeadInDto dto) { - if (dto == null) return new NoLeadIn(); + if (dto == null) + return new NoLeadIn(); return dto.Type switch { "Line" => new LineLeadIn { Length = dto.Length, ApproachAngle = dto.ApproachAngle }, "Arc" => new ArcLeadIn { Radius = dto.Radius }, - "LineArc" => new LineArcLeadIn { LineLength = dto.LineLength, ArcRadius = dto.ArcRadius, ApproachAngle = dto.ApproachAngle }, - "CleanHole" => new CleanHoleLeadIn { LineLength = dto.LineLength, ArcRadius = dto.ArcRadius, Kerf = dto.Kerf }, - "LineLine" => new LineLineLeadIn { Length1 = dto.Length1, ApproachAngle1 = dto.Angle1, Length2 = dto.Length2, ApproachAngle2 = dto.Angle2 }, - _ => new NoLeadIn() + "LineArc" => new LineArcLeadIn + { + LineLength = dto.LineLength, + ArcRadius = dto.ArcRadius, + ApproachAngle = dto.ApproachAngle, + }, + "CleanHole" => new CleanHoleLeadIn + { + LineLength = dto.LineLength, + ArcRadius = dto.ArcRadius, + Kerf = dto.Kerf, + }, + "LineLine" => new LineLineLeadIn + { + Length1 = dto.Length1, + ApproachAngle1 = dto.Angle1, + Length2 = dto.Length2, + ApproachAngle2 = dto.Angle2, + }, + _ => new NoLeadIn(), }; } @@ -87,20 +129,30 @@ namespace OpenNest.Forms { return leadOut switch { - LineLeadOut line => new LeadOutDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle }, + LineLeadOut line => new LeadOutDto + { + Type = "Line", + Length = line.Length, + ApproachAngle = line.ApproachAngle, + }, ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius }, - _ => new LeadOutDto { Type = "None" } + _ => new LeadOutDto { Type = "None" }, }; } private static LeadOut FromLeadOutDto(LeadOutDto dto) { - if (dto == null) return new NoLeadOut(); + if (dto == null) + return new NoLeadOut(); return dto.Type switch { - "Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle }, + "Line" => new LineLeadOut + { + Length = dto.Length, + ApproachAngle = dto.ApproachAngle, + }, "Arc" => new ArcLeadOut { Radius = dto.Radius }, - _ => new NoLeadOut() + _ => new NoLeadOut(), }; } diff --git a/OpenNest/Forms/EditNestForm.cs b/OpenNest/Forms/EditNestForm.cs index f45ba36..46da225 100644 --- a/OpenNest/Forms/EditNestForm.cs +++ b/OpenNest/Forms/EditNestForm.cs @@ -1,4 +1,12 @@ -using OpenNest.Actions; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Windows.Forms; +using OpenNest.Actions; +using OpenNest.Api; using OpenNest.CNC.CuttingStrategy; using OpenNest.Collections; using OpenNest.Controls; @@ -8,14 +16,6 @@ using OpenNest.IO; using OpenNest.Math; using OpenNest.Properties; using OpenNest.Shapes; -using System; -using System.ComponentModel; -using System.Diagnostics; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Windows.Forms; -using OpenNest.Api; using Timer = System.Timers.Timer; namespace OpenNest.Forms @@ -86,7 +86,7 @@ namespace OpenNest.Forms Dock = DockStyle.Top, Height = 30, BackColor = Color.FromArgb(240, 240, 240), - Padding = new Padding(4, 0, 4, 0) + Padding = new Padding(4, 0, 4, 0), }; plateInfoLabel = new Label @@ -96,7 +96,7 @@ namespace OpenNest.Forms Font = new Font("Segoe UI", 12f, FontStyle.Bold), ForeColor = Color.FromArgb(120, 120, 120), Dock = DockStyle.Left, - Padding = new Padding(4, 4, 4, 4) + Padding = new Padding(4, 4, 4, 4), }; var btnSize = new System.Drawing.Size(28, 28); @@ -118,7 +118,7 @@ namespace OpenNest.Forms { Width = btnSize.Width * 4, Height = btnSize.Height, - Anchor = AnchorStyles.None + Anchor = AnchorStyles.None, }; btnFirstPlate.Location = new Point(0, 0); @@ -126,7 +126,9 @@ namespace OpenNest.Forms btnNextPlate.Location = new Point(btnSize.Width * 2, 0); btnLastPlate.Location = new Point(btnSize.Width * 3, 0); - navPanel.Controls.AddRange(new Control[] { btnFirstPlate, btnPreviousPlate, btnNextPlate, btnLastPlate }); + navPanel.Controls.AddRange( + new Control[] { btnFirstPlate, btnPreviousPlate, btnNextPlate, btnLastPlate } + ); plateHeaderPanel.Controls.Add(navPanel); plateHeaderPanel.Controls.Add(plateInfoLabel); @@ -148,7 +150,7 @@ namespace OpenNest.Forms { Dock = DockStyle.Fill, AutoScroll = true, - BackColor = Color.White + BackColor = Color.White, }; viewSplitContainer = new SplitContainer @@ -156,7 +158,7 @@ namespace OpenNest.Forms Dock = DockStyle.Fill, Orientation = Orientation.Vertical, FixedPanel = FixedPanel.Panel2, - Panel2MinSize = 0 + Panel2MinSize = 0, }; viewSplitContainer.Panel1.Controls.Add(PlateView); @@ -187,7 +189,7 @@ namespace OpenNest.Forms Size = new System.Drawing.Size(28, 28), FlatStyle = FlatStyle.Flat, FlatAppearance = { BorderSize = 0 }, - Cursor = Cursors.Hand + Cursor = Cursors.Hand, }; } @@ -198,7 +200,7 @@ namespace OpenNest.Forms { AutoReset = false, Enabled = true, - Interval = 50 + Interval = 50, }; updateDrawingListTimer.Elapsed += drawingListUpdateTimer_Elapsed; @@ -221,7 +223,6 @@ namespace OpenNest.Forms drawingListBox1.DeleteRequested += drawingListBox1_DeleteRequested; } - public void UpdatePlateList() { updatingPlateList = true; @@ -270,7 +271,11 @@ namespace OpenNest.Forms foreach (var dwg in Nest.Drawings.OrderBy(d => d.Name).ToList()) { - if (hideNestedButton.Checked && dwg.Quantity.Required > 0 && dwg.Quantity.Remaining == 0) + if ( + hideNestedButton.Checked + && dwg.Quantity.Required > 0 + && dwg.Quantity.Remaining == 0 + ) continue; drawingListBox1.Items.Add(dwg); @@ -329,7 +334,8 @@ namespace OpenNest.Forms { var dlg = new OpenFileDialog(); dlg.Multiselect = true; - dlg.Filter = "CAD Files (*.dxf;*.dwg)|*.dxf;*.dwg|DXF Files (*.dxf)|*.dxf|DWG Files (*.dwg)|*.dwg"; + dlg.Filter = + "CAD Files (*.dxf;*.dwg)|*.dxf;*.dwg|DXF Files (*.dxf)|*.dxf|DWG Files (*.dwg)|*.dwg"; if (dlg.ShowDialog() != DialogResult.OK) return; @@ -351,9 +357,10 @@ namespace OpenNest.Forms public bool Export() { var dlg = new SaveFileDialog(); - dlg.Filter = "DXF file (*.dxf)|*.dxf|" + - "Image as displayed (*.jpg)|*.jpg|" + - "Locations and rotations (*.txt)|*.txt"; + dlg.Filter = + "DXF file (*.dxf)|*.dxf|" + + "Image as displayed (*.jpg)|*.jpg|" + + "Locations and rotations (*.txt)|*.txt"; dlg.FileName = string.Format("{0}-P{1}", Nest.Name, PlateManager.CurrentIndex + 1); dlg.AddExtension = true; @@ -371,7 +378,10 @@ namespace OpenNest.Forms try { var img = new Bitmap(PlateView.Width, PlateView.Height); - PlateView.DrawToBitmap(img, new Rectangle(0, 0, PlateView.Width, PlateView.Height)); + PlateView.DrawToBitmap( + img, + new Rectangle(0, 0, PlateView.Width, PlateView.Height) + ); img.Save(dlg.FileName); } catch { } @@ -390,11 +400,13 @@ namespace OpenNest.Forms { var pt = part.BaseDrawing.Source.Offset.Rotate(part.Rotation); - writer.WriteLine("{0}|{1},{2}|{3}", + writer.WriteLine( + "{0}|{1},{2}|{3}", part.BaseDrawing.Source.Path, System.Math.Round(part.Location.X - pt.X, 8), System.Math.Round(part.Location.Y - pt.Y, 8), - Angle.ToDegrees(part.Rotation)); + Angle.ToDegrees(part.Rotation) + ); } } catch { } @@ -415,9 +427,9 @@ namespace OpenNest.Forms do { - if (!Export()) return; - } - while (PlateManager.LoadNext()); + if (!Export()) + return; + } while (PlateManager.LoadNext()); } public void RotateCw() @@ -642,8 +654,12 @@ namespace OpenNest.Forms if (plate != null) { - plateInfoLabel.Text = string.Format("Plate {0} of {1} | {2}", - PlateManager.CurrentIndex + 1, PlateManager.Count, plate.Size); + plateInfoLabel.Text = string.Format( + "Plate {0} of {1} | {2}", + PlateManager.CurrentIndex + 1, + PlateManager.Count, + plate.Size + ); } else { @@ -735,10 +751,7 @@ namespace OpenNest.Forms plate.CuttingParameters = parameters; SaveCuttingParameters(parameters); - var assigner = new LeadInAssigner - { - Sequencer = new LeftSideSequencer() - }; + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; assigner.Assign(plate); foreach (var lp in PlateView.Parts) @@ -795,10 +808,7 @@ namespace OpenNest.Forms parameters = dlg.GetParameters(); SaveCuttingParameters(parameters); - var assigner = new LeadInAssigner - { - Sequencer = new LeftSideSequencer() - }; + var assigner = new LeadInAssigner { Sequencer = new LeftSideSequencer() }; foreach (var plate in Nest.Plates) { @@ -855,8 +865,13 @@ namespace OpenNest.Forms var json = Properties.Settings.Default.CuttingParametersJson; if (!string.IsNullOrEmpty(json)) { - try { return CuttingParametersSerializer.Deserialize(json); } - catch { /* fall through */ } + try + { + return CuttingParametersSerializer.Deserialize(json); + } + catch + { /* fall through */ + } } return new CuttingParameters(); @@ -880,7 +895,8 @@ namespace OpenNest.Forms form.ShowDialog(); var drawings = form.GetDrawings(); - if (drawings.Count == 0) return; + if (drawings.Count == 0) + return; drawings.ForEach(d => Nest.Drawings.Add(d)); UpdateDrawingList(); @@ -927,9 +943,9 @@ namespace OpenNest.Forms // Refresh all parts to use the updated programs foreach (var plate in Nest.Plates) - foreach (var part in plate.Parts) - if (!part.BaseDrawing.IsCutOff) - part.Update(); + foreach (var part in plate.Parts) + if (!part.BaseDrawing.IsCutOff) + part.Update(); UpdateDrawingList(); PlateView.Invalidate(); @@ -942,7 +958,8 @@ namespace OpenNest.Forms "Clean Drawings", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, - MessageBoxDefaultButton.Button1); + MessageBoxDefaultButton.Button1 + ); if (result == DialogResult.Yes) { @@ -1016,33 +1033,35 @@ namespace OpenNest.Forms if (!drawingListBox1.IsHandleCreated) return; - drawingListBox1.Invoke(new MethodInvoker(() => - { - if (hideNestedButton.Checked) + drawingListBox1.Invoke( + new MethodInvoker(() => { - drawingListBox1.BeginUpdate(); - - for (var i = drawingListBox1.Items.Count - 1; i >= 0; i--) + if (hideNestedButton.Checked) { - var dwg = (Drawing)drawingListBox1.Items[i]; - if (dwg.Quantity.Required > 0 && dwg.Quantity.Remaining == 0) - drawingListBox1.Items.RemoveAt(i); + drawingListBox1.BeginUpdate(); + + for (var i = drawingListBox1.Items.Count - 1; i >= 0; i--) + { + var dwg = (Drawing)drawingListBox1.Items[i]; + if (dwg.Quantity.Required > 0 && dwg.Quantity.Remaining == 0) + drawingListBox1.Items.RemoveAt(i); + } + + foreach (var dwg in Nest.Drawings.OrderBy(d => d.Name)) + { + if (dwg.Quantity.Required > 0 && dwg.Quantity.Remaining == 0) + continue; + + if (!drawingListBox1.Items.Contains(dwg)) + drawingListBox1.Items.Add(dwg); + } + + drawingListBox1.EndUpdate(); } - foreach (var dwg in Nest.Drawings.OrderBy(d => d.Name)) - { - if (dwg.Quantity.Required > 0 && dwg.Quantity.Remaining == 0) - continue; - - if (!drawingListBox1.Items.Contains(dwg)) - drawingListBox1.Items.Add(dwg); - } - - drawingListBox1.EndUpdate(); - } - - drawingListBox1.Invalidate(); - })); + drawingListBox1.Invalidate(); + }) + ); } private void drawingListBox1_DoubleClick(object sender, EventArgs e) @@ -1075,7 +1094,8 @@ namespace OpenNest.Forms "Delete Drawing", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, - MessageBoxDefaultButton.Button2); + MessageBoxDefaultButton.Button2 + ); if (result != DialogResult.Yes) return; diff --git a/OpenNest/Forms/EditNestInfoForm.cs b/OpenNest/Forms/EditNestInfoForm.cs index e82c066..9a93770 100644 --- a/OpenNest/Forms/EditNestInfoForm.cs +++ b/OpenNest/Forms/EditNestInfoForm.cs @@ -1,6 +1,6 @@ -using OpenNest.Geometry; -using System; +using System; using System.Windows.Forms; +using OpenNest.Geometry; using Timer = System.Timers.Timer; namespace OpenNest.Forms @@ -23,7 +23,7 @@ namespace OpenNest.Forms SynchronizingObject = this, Enabled = true, AutoReset = false, - Interval = SystemInformation.KeyboardDelay + 1 + Interval = SystemInformation.KeyboardDelay + 1, }; timer.Elapsed += (sender, e) => EnableCheck(); EnableCheck(); @@ -151,7 +151,7 @@ namespace OpenNest.Forms leftSpacingBox, topSpacingBox, rightSpacingBox, - bottomSpacingBox + bottomSpacingBox, }; var unitString = " " + UnitsHelper.GetShortString(GetUnits()); @@ -223,7 +223,12 @@ namespace OpenNest.Forms nest.Material = new Material(MaterialName); nest.PlateDefaults.Size = OpenNest.Geometry.Size.Parse(SizeString); nest.PlateDefaults.PartSpacing = PartSpacing; - nest.PlateDefaults.EdgeSpacing = new Spacing(LeftSpacing, BottomSpacing, RightSpacing, TopSpacing); + nest.PlateDefaults.EdgeSpacing = new Spacing( + LeftSpacing, + BottomSpacing, + RightSpacing, + TopSpacing + ); nest.PlateDefaults.Quadrant = Quadrant; } diff --git a/OpenNest/Forms/EditPlateForm.cs b/OpenNest/Forms/EditPlateForm.cs index e2dab60..d2cfda6 100644 --- a/OpenNest/Forms/EditPlateForm.cs +++ b/OpenNest/Forms/EditPlateForm.cs @@ -22,7 +22,7 @@ namespace OpenNest.Forms SynchronizingObject = this, Enabled = true, AutoReset = false, - Interval = SystemInformation.KeyboardDelay + 1 + Interval = SystemInformation.KeyboardDelay + 1, }; timer.Elapsed += (sender, e) => EnableCheck(); @@ -62,7 +62,7 @@ namespace OpenNest.Forms numericUpDownEdgeSpacingBottom, numericUpDownEdgeSpacingLeft, numericUpDownEdgeSpacingRight, - numericUpDownEdgeSpacingTop + numericUpDownEdgeSpacingTop, }; foreach (var control in controls) diff --git a/OpenNest/Forms/FillPlateForm.cs b/OpenNest/Forms/FillPlateForm.cs index c51d1b8..31be2a9 100644 --- a/OpenNest/Forms/FillPlateForm.cs +++ b/OpenNest/Forms/FillPlateForm.cs @@ -1,8 +1,8 @@ -using OpenNest.Collections; +using System.Drawing; +using System.Windows.Forms; +using OpenNest.Collections; using OpenNest.Controls; using OpenNest.Geometry; -using System.Drawing; -using System.Windows.Forms; namespace OpenNest.Forms { @@ -41,7 +41,8 @@ namespace OpenNest.Forms control.AddPartFromDrawing(dwg, Vector.Zero); control.MouseDoubleClick += (sender, e) => { - SelectedDrawing = control.Plate.Parts.Count > 0 ? control.Plate.Parts[0].BaseDrawing : null; + SelectedDrawing = + control.Plate.Parts.Count > 0 ? control.Plate.Parts[0].BaseDrawing : null; Close(); }; control.Dock = DockStyle.Fill; diff --git a/OpenNest/Forms/MachineConfigForm.cs b/OpenNest/Forms/MachineConfigForm.cs index 02b1b4a..20d197a 100644 --- a/OpenNest/Forms/MachineConfigForm.cs +++ b/OpenNest/Forms/MachineConfigForm.cs @@ -1,4 +1,3 @@ -using OpenNest.Data; using System; using System.Collections.Generic; using System.Drawing; @@ -7,6 +6,7 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Windows.Forms; +using OpenNest.Data; namespace OpenNest.Forms { @@ -31,14 +31,10 @@ namespace OpenNest.Forms { Dock = DockStyle.Fill, SplitterDistance = 250, - FixedPanel = FixedPanel.Panel1 + FixedPanel = FixedPanel.Panel1, }; - _tree = new TreeView - { - Dock = DockStyle.Fill, - HideSelection = false - }; + _tree = new TreeView { Dock = DockStyle.Fill, HideSelection = false }; _tree.AfterSelect += Tree_AfterSelect; var treeButtonPanel = new FlowLayoutPanel @@ -47,7 +43,7 @@ namespace OpenNest.Forms AutoSize = true, FlowDirection = FlowDirection.LeftToRight, WrapContents = true, - Padding = new Padding(2) + Padding = new Padding(2), }; var addMachineButton = new Button { Text = "+ Machine", AutoSize = true }; @@ -63,12 +59,17 @@ namespace OpenNest.Forms var removeThicknessButton = new Button { Text = "- Thickness", AutoSize = true }; removeThicknessButton.Click += RemoveThickness_Click; - treeButtonPanel.Controls.AddRange(new Control[] - { - addMachineButton, removeMachineButton, - addMaterialButton, removeMaterialButton, - addThicknessButton, removeThicknessButton - }); + treeButtonPanel.Controls.AddRange( + new Control[] + { + addMachineButton, + removeMachineButton, + addMaterialButton, + removeMaterialButton, + addThicknessButton, + removeThicknessButton, + } + ); splitContainer.Panel1.Controls.Add(_tree); splitContainer.Panel1.Controls.Add(treeButtonPanel); @@ -81,7 +82,7 @@ namespace OpenNest.Forms Dock = DockStyle.Bottom, AutoSize = true, FlowDirection = FlowDirection.RightToLeft, - Padding = new Padding(4) + Padding = new Padding(4), }; var saveButton = new Button { Text = "Save", AutoSize = true }; @@ -105,7 +106,8 @@ namespace OpenNest.Forms foreach (var summary in _provider.GetMachines()) { var machine = _provider.GetMachine(summary.Id); - if (machine is null) continue; + if (machine is null) + continue; var machineNode = new TreeNode(machine.Name) { Tag = machine }; foreach (var material in machine.Materials) @@ -113,7 +115,10 @@ namespace OpenNest.Forms var matNode = new TreeNode(material.Name) { Tag = material }; foreach (var thickness in material.Thicknesses) { - var thickNode = new TreeNode(thickness.Value.ToString("0.####")) { Tag = thickness }; + var thickNode = new TreeNode(thickness.Value.ToString("0.####")) + { + Tag = thickness, + }; matNode.Nodes.Add(thickNode); } machineNode.Nodes.Add(matNode); @@ -128,7 +133,8 @@ namespace OpenNest.Forms private void Tree_AfterSelect(object sender, TreeViewEventArgs e) { _detailPanel.Controls.Clear(); - if (e.Node?.Tag is null) return; + if (e.Node?.Tag is null) + return; switch (e.Node.Tag) { @@ -153,8 +159,18 @@ namespace OpenNest.Forms var row = 0; AddField(layout, ref row, "Name:", CreateTextBox(machine.Name, v => machine.Name = v)); - AddField(layout, ref row, "Type:", CreateEnumCombo(machine.Type, v => machine.Type = v)); - AddField(layout, ref row, "Units:", CreateEnumCombo(machine.Units, v => machine.Units = v)); + AddField( + layout, + ref row, + "Type:", + CreateEnumCombo(machine.Type, v => machine.Type = v) + ); + AddField( + layout, + ref row, + "Units:", + CreateEnumCombo(machine.Units, v => machine.Units = v) + ); _detailPanel.Controls.Add(layout); } @@ -164,9 +180,24 @@ namespace OpenNest.Forms var layout = CreateDetailLayout(); var row = 0; - AddField(layout, ref row, "Name:", CreateTextBox(material.Name, v => material.Name = v)); - AddField(layout, ref row, "Grade:", CreateTextBox(material.Grade, v => material.Grade = v)); - AddField(layout, ref row, "Density:", CreateNumericBox(material.Density, v => material.Density = v, 4)); + AddField( + layout, + ref row, + "Name:", + CreateTextBox(material.Name, v => material.Name = v) + ); + AddField( + layout, + ref row, + "Grade:", + CreateTextBox(material.Grade, v => material.Grade = v) + ); + AddField( + layout, + ref row, + "Density:", + CreateNumericBox(material.Density, v => material.Density = v, 4) + ); _detailPanel.Controls.Add(layout); } @@ -176,34 +207,134 @@ namespace OpenNest.Forms var layout = CreateDetailLayout(); var row = 0; - AddField(layout, ref row, "Thickness:", CreateNumericBox(thickness.Value, v => thickness.Value = v, 4)); - AddField(layout, ref row, "Kerf:", CreateNumericBox(thickness.Kerf, v => thickness.Kerf = v, 4)); - AddField(layout, ref row, "Assist Gas:", CreateTextBox(thickness.AssistGas, v => thickness.AssistGas = v)); + AddField( + layout, + ref row, + "Thickness:", + CreateNumericBox(thickness.Value, v => thickness.Value = v, 4) + ); + AddField( + layout, + ref row, + "Kerf:", + CreateNumericBox(thickness.Kerf, v => thickness.Kerf = v, 4) + ); + AddField( + layout, + ref row, + "Assist Gas:", + CreateTextBox(thickness.AssistGas, v => thickness.AssistGas = v) + ); AddSectionHeader(layout, ref row, "Lead In"); - AddField(layout, ref row, "Type:", CreateTextBox(thickness.LeadIn.Type, v => thickness.LeadIn.Type = v)); - AddField(layout, ref row, "Length:", CreateNumericBox(thickness.LeadIn.Length, v => thickness.LeadIn.Length = v, 4)); - AddField(layout, ref row, "Angle:", CreateNumericBox(thickness.LeadIn.Angle, v => thickness.LeadIn.Angle = v, 1)); - AddField(layout, ref row, "Radius:", CreateNumericBox(thickness.LeadIn.Radius, v => thickness.LeadIn.Radius = v, 4)); + AddField( + layout, + ref row, + "Type:", + CreateTextBox(thickness.LeadIn.Type, v => thickness.LeadIn.Type = v) + ); + AddField( + layout, + ref row, + "Length:", + CreateNumericBox(thickness.LeadIn.Length, v => thickness.LeadIn.Length = v, 4) + ); + AddField( + layout, + ref row, + "Angle:", + CreateNumericBox(thickness.LeadIn.Angle, v => thickness.LeadIn.Angle = v, 1) + ); + AddField( + layout, + ref row, + "Radius:", + CreateNumericBox(thickness.LeadIn.Radius, v => thickness.LeadIn.Radius = v, 4) + ); AddSectionHeader(layout, ref row, "Lead Out"); - AddField(layout, ref row, "Type:", CreateTextBox(thickness.LeadOut.Type, v => thickness.LeadOut.Type = v)); - AddField(layout, ref row, "Length:", CreateNumericBox(thickness.LeadOut.Length, v => thickness.LeadOut.Length = v, 4)); - AddField(layout, ref row, "Angle:", CreateNumericBox(thickness.LeadOut.Angle, v => thickness.LeadOut.Angle = v, 1)); - AddField(layout, ref row, "Radius:", CreateNumericBox(thickness.LeadOut.Radius, v => thickness.LeadOut.Radius = v, 4)); + AddField( + layout, + ref row, + "Type:", + CreateTextBox(thickness.LeadOut.Type, v => thickness.LeadOut.Type = v) + ); + AddField( + layout, + ref row, + "Length:", + CreateNumericBox(thickness.LeadOut.Length, v => thickness.LeadOut.Length = v, 4) + ); + AddField( + layout, + ref row, + "Angle:", + CreateNumericBox(thickness.LeadOut.Angle, v => thickness.LeadOut.Angle = v, 1) + ); + AddField( + layout, + ref row, + "Radius:", + CreateNumericBox(thickness.LeadOut.Radius, v => thickness.LeadOut.Radius = v, 4) + ); AddSectionHeader(layout, ref row, "Cut Off"); - AddField(layout, ref row, "Part Clearance:", CreateNumericBox(thickness.CutOff.PartClearance, v => thickness.CutOff.PartClearance = v, 4)); - AddField(layout, ref row, "Overtravel:", CreateNumericBox(thickness.CutOff.Overtravel, v => thickness.CutOff.Overtravel = v, 4)); - AddField(layout, ref row, "Min Segment:", CreateNumericBox(thickness.CutOff.MinSegmentLength, v => thickness.CutOff.MinSegmentLength = v, 4)); - AddField(layout, ref row, "Direction:", CreateTextBox(thickness.CutOff.Direction, v => thickness.CutOff.Direction = v)); + AddField( + layout, + ref row, + "Part Clearance:", + CreateNumericBox( + thickness.CutOff.PartClearance, + v => thickness.CutOff.PartClearance = v, + 4 + ) + ); + AddField( + layout, + ref row, + "Overtravel:", + CreateNumericBox( + thickness.CutOff.Overtravel, + v => thickness.CutOff.Overtravel = v, + 4 + ) + ); + AddField( + layout, + ref row, + "Min Segment:", + CreateNumericBox( + thickness.CutOff.MinSegmentLength, + v => thickness.CutOff.MinSegmentLength = v, + 4 + ) + ); + AddField( + layout, + ref row, + "Direction:", + CreateTextBox(thickness.CutOff.Direction, v => thickness.CutOff.Direction = v) + ); AddSectionHeader(layout, ref row, "Plate Sizes"); var sizesText = string.Join(", ", thickness.PlateSizes); - AddField(layout, ref row, "Sizes:", CreateTextBox(sizesText, v => - { - thickness.PlateSizes = v.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); - })); + AddField( + layout, + ref row, + "Sizes:", + CreateTextBox( + sizesText, + v => + { + thickness.PlateSizes = v.Split( + ',', + StringSplitOptions.RemoveEmptyEntries + | StringSplitOptions.TrimEntries + ) + .ToList(); + } + ) + ); _detailPanel.Controls.Add(layout); } @@ -215,18 +346,33 @@ namespace OpenNest.Forms Dock = DockStyle.Top, AutoSize = true, ColumnCount = 2, - Padding = new Padding(8) + Padding = new Padding(8), }; layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); return layout; } - private static void AddField(TableLayoutPanel layout, ref int row, string label, Control control) + private static void AddField( + TableLayoutPanel layout, + ref int row, + string label, + Control control + ) { layout.RowCount = row + 1; layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); - layout.Controls.Add(new Label { Text = label, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 6, 8, 0) }, 0, row); + layout.Controls.Add( + new Label + { + Text = label, + AutoSize = true, + Anchor = AnchorStyles.Left, + Margin = new Padding(0, 6, 8, 0), + }, + 0, + row + ); control.Dock = DockStyle.Fill; layout.Controls.Add(control, 1, row); row++; @@ -241,7 +387,7 @@ namespace OpenNest.Forms Text = text, AutoSize = true, Font = new Font(Control.DefaultFont, FontStyle.Bold), - Margin = new Padding(0, 12, 0, 4) + Margin = new Padding(0, 12, 0, 4), }; layout.SetColumnSpan(label, 2); layout.Controls.Add(label, 0, row); @@ -255,7 +401,11 @@ namespace OpenNest.Forms return textBox; } - private static NumericUpDown CreateNumericBox(double value, Action setter, int decimals) + private static NumericUpDown CreateNumericBox( + double value, + Action setter, + int decimals + ) { var numeric = new NumericUpDown { @@ -263,18 +413,16 @@ namespace OpenNest.Forms Minimum = 0, Maximum = 10000, Increment = (decimal)System.Math.Pow(10, -decimals), - Value = (decimal)value + Value = (decimal)value, }; numeric.ValueChanged += (s, e) => setter((double)numeric.Value); return numeric; } - private static ComboBox CreateEnumCombo(T currentValue, Action setter) where T : struct, Enum + private static ComboBox CreateEnumCombo(T currentValue, Action setter) + where T : struct, Enum { - var combo = new ComboBox - { - DropDownStyle = ComboBoxStyle.DropDownList - }; + var combo = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList }; combo.Items.AddRange(Enum.GetNames().Cast().ToArray()); combo.SelectedItem = currentValue.ToString(); combo.SelectedIndexChanged += (s, e) => @@ -292,7 +440,12 @@ namespace OpenNest.Forms if (machineNode.Tag is MachineConfig machine) _provider.SaveMachine(machine); } - MessageBox.Show("Machine configurations saved.", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show( + "Machine configurations saved.", + "Saved", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); } private void AddMachine_Click(object sender, EventArgs e) @@ -304,8 +457,16 @@ namespace OpenNest.Forms private void RemoveMachine_Click(object sender, EventArgs e) { - if (_tree.SelectedNode?.Tag is not MachineConfig machine) return; - if (MessageBox.Show($"Delete machine '{machine.Name}'?", "Confirm", MessageBoxButtons.YesNo) != DialogResult.Yes) return; + if (_tree.SelectedNode?.Tag is not MachineConfig machine) + return; + if ( + MessageBox.Show( + $"Delete machine '{machine.Name}'?", + "Confirm", + MessageBoxButtons.YesNo + ) != DialogResult.Yes + ) + return; _provider.DeleteMachine(machine.Id); LoadTree(); @@ -313,7 +474,8 @@ namespace OpenNest.Forms private void AddMaterial_Click(object sender, EventArgs e) { - if (_currentMachine is null) return; + if (_currentMachine is null) + return; _currentMachine.Materials.Add(new MaterialConfig { Name = "New Material" }); _provider.SaveMachine(_currentMachine); @@ -322,8 +484,10 @@ namespace OpenNest.Forms private void RemoveMaterial_Click(object sender, EventArgs e) { - if (_tree.SelectedNode?.Tag is not MaterialConfig material) return; - if (_currentMachine is null) return; + if (_tree.SelectedNode?.Tag is not MaterialConfig material) + return; + if (_currentMachine is null) + return; _currentMachine.Materials.Remove(material); _provider.SaveMachine(_currentMachine); @@ -335,7 +499,8 @@ namespace OpenNest.Forms var material = _tree.SelectedNode?.Tag as MaterialConfig; if (material is null && _tree.SelectedNode?.Tag is ThicknessConfig) material = _tree.SelectedNode.Parent?.Tag as MaterialConfig; - if (material is null || _currentMachine is null) return; + if (material is null || _currentMachine is null) + return; material.Thicknesses.Add(new ThicknessConfig { Value = 0.250 }); _provider.SaveMachine(_currentMachine); @@ -344,9 +509,11 @@ namespace OpenNest.Forms private void RemoveThickness_Click(object sender, EventArgs e) { - if (_tree.SelectedNode?.Tag is not ThicknessConfig thickness) return; + if (_tree.SelectedNode?.Tag is not ThicknessConfig thickness) + return; var material = _tree.SelectedNode.Parent?.Tag as MaterialConfig; - if (material is null || _currentMachine is null) return; + if (material is null || _currentMachine is null) + return; material.Thicknesses.Remove(thickness); _provider.SaveMachine(_currentMachine); @@ -355,13 +522,16 @@ namespace OpenNest.Forms private void Import_Click(object sender, EventArgs e) { - using (var dialog = new OpenFileDialog + using ( + var dialog = new OpenFileDialog + { + Filter = "JSON files (*.json)|*.json", + Title = "Import Machine Configuration", + } + ) { - Filter = "JSON files (*.json)|*.json", - Title = "Import Machine Configuration" - }) - { - if (dialog.ShowDialog() != DialogResult.OK) return; + if (dialog.ShowDialog() != DialogResult.OK) + return; try { @@ -369,10 +539,11 @@ namespace OpenNest.Forms var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, }; var machine = JsonSerializer.Deserialize(json, options); - if (machine is null) return; + if (machine is null) + return; machine.Id = Guid.NewGuid(); _provider.SaveMachine(machine); @@ -380,23 +551,32 @@ namespace OpenNest.Forms } catch (Exception ex) { - MessageBox.Show($"Failed to import: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show( + $"Failed to import: {ex.Message}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); } } } private void Export_Click(object sender, EventArgs e) { - if (_currentMachine is null) return; + if (_currentMachine is null) + return; - using (var dialog = new SaveFileDialog + using ( + var dialog = new SaveFileDialog + { + Filter = "JSON files (*.json)|*.json", + FileName = $"{_currentMachine.Name}.json", + Title = "Export Machine Configuration", + } + ) { - Filter = "JSON files (*.json)|*.json", - FileName = $"{_currentMachine.Name}.json", - Title = "Export Machine Configuration" - }) - { - if (dialog.ShowDialog() != DialogResult.OK) return; + if (dialog.ShowDialog() != DialogResult.OK) + return; try { @@ -404,14 +584,19 @@ namespace OpenNest.Forms { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } + Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) }, }; var json = JsonSerializer.Serialize(_currentMachine, options); File.WriteAllText(dialog.FileName, json); } catch (Exception ex) { - MessageBox.Show($"Failed to export: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show( + $"Failed to export: {ex.Message}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); } } } diff --git a/OpenNest/Forms/MainForm.cs b/OpenNest/Forms/MainForm.cs index 99f2e0d..51aada3 100644 --- a/OpenNest/Forms/MainForm.cs +++ b/OpenNest/Forms/MainForm.cs @@ -1,13 +1,4 @@ -using OpenNest.Actions; -using OpenNest.Collections; -using OpenNest.Data; -using OpenNest.Engine.BestFit; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; -using OpenNest.Gpu; -using OpenNest.IO; -using OpenNest.Properties; -using System; +using System; using System.Collections.Generic; using System.Drawing; using System.IO; @@ -16,6 +7,15 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using OpenNest.Actions; +using OpenNest.Collections; +using OpenNest.Data; +using OpenNest.Engine.BestFit; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; +using OpenNest.Gpu; +using OpenNest.IO; +using OpenNest.Properties; namespace OpenNest.Forms { @@ -102,7 +102,8 @@ namespace OpenNest.Forms private static string ToBase36(int value) { const string chars = "2345679ACDEFGHJKLMNPQRSTUVWXYZ"; - if (value == 0) return chars[0].ToString(); + if (value == 0) + return chars[0].ToString(); var result = ""; while (value > 0) @@ -135,8 +136,10 @@ namespace OpenNest.Forms if (screen.Contains(Settings.Default.MainWindowLocation)) Location = Settings.Default.MainWindowLocation; - if (Settings.Default.MainWindowSize.Width <= screen.Width && - Settings.Default.MainWindowSize.Height <= screen.Height) + if ( + Settings.Default.MainWindowSize.Width <= screen.Width + && Settings.Default.MainWindowSize.Height <= screen.Height + ) { Size = Settings.Default.MainWindowSize; } @@ -194,8 +197,10 @@ namespace OpenNest.Forms { mnuNestPreviousPlate.Enabled = !activeForm.PlateManager.IsFirst; mnuNestNextPlate.Enabled = !activeForm.PlateManager.IsLast; - mnuNestFirstPlate.Enabled = activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsFirst; - mnuNestLastPlate.Enabled = activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsLast; + mnuNestFirstPlate.Enabled = + activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsFirst; + mnuNestLastPlate.Enabled = + activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsLast; } } @@ -208,10 +213,20 @@ namespace OpenNest.Forms mnuPlate.Enabled = !locked; // Lock plate navigation - mnuNestPreviousPlate.Enabled = !locked && activeForm != null && !activeForm.PlateManager.IsFirst; - mnuNestNextPlate.Enabled = !locked && activeForm != null && !activeForm.PlateManager.IsLast; - mnuNestFirstPlate.Enabled = !locked && activeForm != null && activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsFirst; - mnuNestLastPlate.Enabled = !locked && activeForm != null && activeForm.PlateManager.Count > 0 && !activeForm.PlateManager.IsLast; + mnuNestPreviousPlate.Enabled = + !locked && activeForm != null && !activeForm.PlateManager.IsFirst; + mnuNestNextPlate.Enabled = + !locked && activeForm != null && !activeForm.PlateManager.IsLast; + mnuNestFirstPlate.Enabled = + !locked + && activeForm != null + && activeForm.PlateManager.Count > 0 + && !activeForm.PlateManager.IsFirst; + mnuNestLastPlate.Enabled = + !locked + && activeForm != null + && activeForm.PlateManager.Count > 0 + && !activeForm.PlateManager.IsLast; } private void UpdateLocationStatus() @@ -222,9 +237,11 @@ namespace OpenNest.Forms return; } - locationStatusLabel.Text = string.Format("Location: [{0}, {1}]", + locationStatusLabel.Text = string.Format( + "Location: [{0}, {1}]", activeForm.PlateView.CurrentPoint.X.ToString("n4"), - activeForm.PlateView.CurrentPoint.Y.ToString("n4")); + activeForm.PlateView.CurrentPoint.Y.ToString("n4") + ); } private void UpdatePlateStatus() @@ -241,19 +258,20 @@ namespace OpenNest.Forms plateIndexStatusLabel.Text = string.Format( "Plate: {0} of {1}", activeForm.PlateManager.CurrentIndex + 1, - activeForm.PlateManager.Count); + activeForm.PlateManager.Count + ); - plateSizeStatusLabel.Text = string.Format( - "Size: {0}", - activeForm.PlateView.Plate.Size); + plateSizeStatusLabel.Text = string.Format("Size: {0}", activeForm.PlateView.Plate.Size); plateQtyStatusLabel.Text = string.Format( "Qty: {0}", - activeForm.PlateView.Plate.Quantity); + activeForm.PlateView.Plate.Quantity + ); plateUtilStatusLabel.Text = string.Format( "Util: {0:P1}", - activeForm.PlateView.Plate.Utilization()); + activeForm.PlateView.Plate.Utilization() + ); } private void UpdateSelectionStatus() @@ -269,17 +287,25 @@ namespace OpenNest.Forms if (selected.Count == 1) { var box = selected[0].BoundingBox; - selectionStatusLabel.Text = string.Format("Selected: [{0}, {1}] {2} x {3}", - box.X.ToString("n4"), box.Y.ToString("n4"), - box.Width.ToString("n4"), box.Length.ToString("n4")); + selectionStatusLabel.Text = string.Format( + "Selected: [{0}, {1}] {2} x {3}", + box.X.ToString("n4"), + box.Y.ToString("n4"), + box.Width.ToString("n4"), + box.Length.ToString("n4") + ); } else { var bounds = selected.Select(p => p.BasePart).ToList().GetBoundingBox(); - selectionStatusLabel.Text = string.Format("Selected ({0}): [{1}, {2}] {3} x {4}", + selectionStatusLabel.Text = string.Format( + "Selected ({0}): [{1}, {2}] {3} x {4}", selected.Count, - bounds.X.ToString("n4"), bounds.Y.ToString("n4"), - bounds.Width.ToString("n4"), bounds.Length.ToString("n4")); + bounds.X.ToString("n4"), + bounds.Y.ToString("n4"), + bounds.Width.ToString("n4"), + bounds.Length.ToString("n4") + ); } } @@ -436,9 +462,8 @@ namespace OpenNest.Forms private void New_Click(object sender, EventArgs e) { - var windowState = ActiveMdiChild != null - ? ActiveMdiChild.WindowState - : FormWindowState.Maximized; + var windowState = + ActiveMdiChild != null ? ActiveMdiChild.WindowState : FormWindowState.Maximized; Nest nest; @@ -455,7 +480,8 @@ namespace OpenNest.Forms $"Failed to load nest template:\n{ex.Message}\n\nA default nest will be created instead.", "Template Error", MessageBoxButtons.OK, - MessageBoxIcon.Warning); + MessageBoxIcon.Warning + ); nest = CreateDefaultNest(); } } @@ -512,13 +538,15 @@ namespace OpenNest.Forms private void Export_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.Export(); } private void ExportAll_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.ExportAll(); } @@ -533,7 +561,8 @@ namespace OpenNest.Forms private void EditCut_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; var selectedParts = activeForm.PlateView.SelectedParts; @@ -547,13 +576,12 @@ namespace OpenNest.Forms } } - private void EditPaste_Click(object sender, EventArgs e) - { - } + private void EditPaste_Click(object sender, EventArgs e) { } private void EditCopy_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; var selectedParts = activeForm.PlateView.SelectedParts; @@ -566,7 +594,8 @@ namespace OpenNest.Forms private void EditSelectAll_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.SelectAllParts(); } @@ -576,81 +605,88 @@ namespace OpenNest.Forms private void ToggleDrawRapids_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.ToggleRapid(); mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid; } private void ToggleDrawPiercePoints_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.TogglePiercePoints(); mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints; } private void ToggleDrawBounds_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.ToggleDrawBounds(); mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds; } private void ToggleDrawOffset_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.ToggleDrawOffset(); mnuViewDrawOffset.Checked = activeForm.PlateView.DrawOffset; } private void ToggleDrawCutDirection_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.ToggleCutDirection(); mnuViewDrawCutDirection.Checked = activeForm.PlateView.DrawCutDirection; } private void ZoomToArea_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.SetAction(typeof(ActionZoomWindow)); } private void ZoomToFit_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.ZoomToFit(); } private void ZoomToPlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.ZoomToPlate(); } private void ZoomToSelected_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.ZoomToSelected(); } private void ZoomIn_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; - var pt = new Point( - activeForm.PlateView.Width / 2, - activeForm.PlateView.Height / 2); + var pt = new Point(activeForm.PlateView.Width / 2, activeForm.PlateView.Height / 2); activeForm.PlateView.ZoomToControlPoint(pt, ZoomInFactor); } private void ZoomOut_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; - var pt = new Point( - activeForm.PlateView.Width / 2, - activeForm.PlateView.Height / 2); + var pt = new Point(activeForm.PlateView.Width / 2, activeForm.PlateView.Height / 2); activeForm.PlateView.ZoomToControlPoint(pt, ZoomOutFactor); } @@ -677,8 +713,12 @@ namespace OpenNest.Forms if (drawings.Count == 0) { - MessageBox.Show("No drawings available.", "Best-Fit Viewer", - MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show( + "No drawings available.", + "Best-Fit Viewer", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); return; } @@ -686,7 +726,8 @@ namespace OpenNest.Forms { if (form.ShowDialog(this) == DialogResult.OK && form.SelectedResult != null) { - var parts = form.SelectedParts + var parts = + form.SelectedParts ?? form.SelectedResult.BuildSourceParts(form.SelectedDrawing); activeForm.PlateView.SetAction(typeof(ActionClone), parts); } @@ -700,8 +741,12 @@ namespace OpenNest.Forms if (activeForm.Nest.Drawings.Count == 0) { - MessageBox.Show("No drawings available.", "Pattern Tile", - MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show( + "No drawings available.", + "Pattern Tile", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); return; } @@ -738,7 +783,8 @@ namespace OpenNest.Forms private void SetOffsetIncrement_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; var form = new SetValueForm(); form.Text = "Set Offset Increment"; form.Value = activeForm.PlateView.OffsetIncrementDistance; @@ -749,7 +795,8 @@ namespace OpenNest.Forms private void SetRotationIncrement_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; var form = new SetValueForm(); form.Text = "Set Rotation Increment"; form.Value = activeForm.PlateView.RotateIncrementAngle; @@ -767,7 +814,11 @@ namespace OpenNest.Forms private void MachineConfig_Click(object sender, EventArgs e) { - var appDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OpenNest", "Machines"); + var appDataPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "OpenNest", + "Machines" + ); var provider = new LocalJsonProvider(appDataPath); provider.EnsureDefaults(); using (var form = new MachineConfigForm(provider)) @@ -778,49 +829,57 @@ namespace OpenNest.Forms private void AlignLeft_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.Left); } private void AlignRight_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.Right); } private void AlignTop_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.Top); } private void AlignBottom_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.Bottom); } private void AlignVertical_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.Vertically); } private void AlignHorizontal_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.Horizontally); } private void EvenlySpaceHorizontally_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.EvenlySpaceHorizontally); } private void EvenlySpaceVertically_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateView.AlignSelected(AlignType.EvenlySpaceVertically); } @@ -830,19 +889,22 @@ namespace OpenNest.Forms private void Import_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.Import(); } private void ShapeLibrary_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; var form = new ShapeLibraryForm(activeForm.Nest.Drawings.Select(d => d.Name)); form.ShowDialog(); var drawings = form.GetDrawings(); - if (drawings.Count == 0) return; + if (drawings.Count == 0) + return; drawings.ForEach(d => activeForm.Nest.Drawings.Add(d)); activeForm.UpdateDrawingList(); @@ -850,37 +912,43 @@ namespace OpenNest.Forms private void EditNest_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.ShowNestInfoEditor(); } private void RemoveEmptyPlates_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.Nest.Plates.RemoveEmptyPlates(); } private void LoadFirstPlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateManager.LoadFirst(); } private void LoadLastPlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateManager.LoadLast(); } private void LoadPreviousPlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateManager.LoadPrevious(); } private void LoadNextPlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlateManager.LoadNext(); } @@ -918,7 +986,8 @@ namespace OpenNest.Forms var screen = Screen.FromControl(this); remnantViewer.Location = new Point( System.Math.Min(Right, screen.WorkingArea.Right - remnantViewer.Width), - Top); + Top + ); } remnantViewer.LoadRemnants(finder, minDim, activeForm.PlateView); @@ -982,14 +1051,28 @@ namespace OpenNest.Forms try { - await RunAutoNestAsync(items, progressForm, progress, nestingCts.Token, - plateOptions, salvageRate, partFirstMode, sortOrder, minRemnantSize, allowPlateCreation); + await RunAutoNestAsync( + items, + progressForm, + progress, + nestingCts.Token, + plateOptions, + salvageRate, + partFirstMode, + sortOrder, + minRemnantSize, + allowPlateCreation + ); } catch (Exception ex) { activeForm.PlateView.ClearPreviewParts(); - MessageBox.Show($"Nesting error: {ex.Message}", "Error", - MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show( + $"Nesting error: {ex.Message}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error + ); } finally { @@ -1011,7 +1094,8 @@ namespace OpenNest.Forms bool partFirstMode = false, PartSortOrder sortOrder = PartSortOrder.BoundingBoxArea, double minRemnantSize = 12.0, - bool allowPlateCreation = true) + bool allowPlateCreation = true + ) { if (partFirstMode) { @@ -1036,7 +1120,8 @@ namespace OpenNest.Forms }; var result = await Task.Run(() => - MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token)); + MultiPlateNester.Nest(items, nestOptions, existingPlates, progress, token) + ); foreach (var pr in result.Plates) { @@ -1065,8 +1150,15 @@ namespace OpenNest.Forms var plate = GetOrCreatePlate(progressForm); var placed = await NestSinglePlateAsync( - plate, plateIndex, remaining, progressForm, progress, token, - plateOptions, salvageRate); + plate, + plateIndex, + remaining, + progressForm, + progress, + token, + plateOptions, + salvageRate + ); if (!placed) break; @@ -1092,17 +1184,29 @@ namespace OpenNest.Forms IProgress progress, CancellationToken token, List plateOptions = null, - double salvageRate = 0.5) + double salvageRate = 0.5 + ) { List nestParts; if (plateOptions != null && plateOptions.Count > 0) { var result = await Task.Run(() => - PlateOptimizer.Optimize(items, plateOptions, salvageRate, plate, progress, token)); + PlateOptimizer.Optimize( + items, + plateOptions, + salvageRate, + plate, + progress, + token + ) + ); - if (result == null || result.Parts.Count == 0 || - (token.IsCancellationRequested && !progressForm.Accepted)) + if ( + result == null + || result.Parts.Count == 0 + || (token.IsCancellationRequested && !progressForm.Accepted) + ) return false; plate.Size = new Geometry.Size(result.ChosenSize.Width, result.ChosenSize.Length); @@ -1121,8 +1225,7 @@ namespace OpenNest.Forms var engine = NestEngineRegistry.Create(plate); engine.PlateNumber = plateIndex; - nestParts = await Task.Run(() => - engine.Nest(items, progress, token)); + nestParts = await Task.Run(() => engine.Nest(items, progress, token)); } activeForm.PlateView.ClearPreviewParts(); @@ -1193,13 +1296,15 @@ namespace OpenNest.Forms private void NestAssignLeadIns_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.AssignLeadInsAllPlates(); } private void NestRemoveLeadIns_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.RemoveLeadInsAllPlates(); } @@ -1209,57 +1314,66 @@ namespace OpenNest.Forms private void SetAsNestDefault_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.SetCurrentPlateAsNestDefault(); } private void AddPlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.Nest.CreatePlate(); NavigationEnableCheck(); } private void EditPlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.EditPlate(); } private void RemovePlate_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.RemoveCurrentPlate(); } private void ResizeToFitParts_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.ResizePlateToFitParts(); UpdatePlateStatus(); } private void RotateCw_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.RotateCw(); } private void RotateCcw_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.RotateCcw(); } private void Rotate180_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.Rotate180(); } private void OpenInExternalCad_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.OpenCurrentPlate(); } @@ -1297,19 +1411,22 @@ namespace OpenNest.Forms private void PlateAssignLeadIns_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.AssignLeadIns_Click(sender, e); } private void PlatePlaceLeadIn_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.PlaceLeadIn_Click(sender, e); } private void PlateRemoveLeadIns_Click(object sender, EventArgs e) { - if (activeForm == null) return; + if (activeForm == null) + return; activeForm.RemoveLeadIns_Click(sender, e); } @@ -1358,8 +1475,11 @@ namespace OpenNest.Forms #region PlateView Events - private void PlateView_PartAdded(object sender, ItemAddedEventArgs e) => UpdatePlateStatus(); - private void PlateView_PartRemoved(object sender, ItemRemovedEventArgs e) => UpdatePlateStatus(); + private void PlateView_PartAdded(object sender, ItemAddedEventArgs e) => + UpdatePlateStatus(); + + private void PlateView_PartRemoved(object sender, ItemRemovedEventArgs e) => + UpdatePlateStatus(); private void PlateView_MouseMove(object sender, MouseEventArgs e) { diff --git a/OpenNest/Forms/NestProgressForm.cs b/OpenNest/Forms/NestProgressForm.cs index 191a93d..96b7f3e 100644 --- a/OpenNest/Forms/NestProgressForm.cs +++ b/OpenNest/Forms/NestProgressForm.cs @@ -82,8 +82,10 @@ namespace OpenNest.Forms SetValueWithFlash(densityValue, densityText, densityFlashColor); densityBar.Value = progress.BestDensity; - SetValueWithFlash(nestedAreaValue, - $"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)"); + SetValueWithFlash( + nestedAreaValue, + $"{progress.NestedWidth:F1} x {progress.NestedLength:F1} ({progress.NestedArea:F1} sq in)" + ); } descriptionValue.Text = !string.IsNullOrEmpty(progress.Description) @@ -130,9 +132,10 @@ namespace OpenNest.Forms return; var elapsed = stopwatch.Elapsed; - elapsedValue.Text = elapsed.TotalHours >= 1 - ? elapsed.ToString(@"h\:mm\:ss") - : elapsed.ToString(@"m\:ss"); + elapsedValue.Text = + elapsed.TotalHours >= 1 + ? elapsed.ToString(@"h\:mm\:ss") + : elapsed.ToString(@"m\:ss"); } private void AcceptButton_Click(object sender, EventArgs e) diff --git a/OpenNest/Forms/OptionsForm.cs b/OpenNest/Forms/OptionsForm.cs index 6e16442..5d991d0 100644 --- a/OpenNest/Forms/OptionsForm.cs +++ b/OpenNest/Forms/OptionsForm.cs @@ -1,9 +1,9 @@ -using OpenNest.Engine.Strategies; -using OpenNest.Properties; using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; +using OpenNest.Engine.Strategies; +using OpenNest.Properties; namespace OpenNest.Forms { @@ -25,36 +25,44 @@ namespace OpenNest.Forms { strategyGrid.AutoGenerateColumns = false; - strategyGrid.Columns.Add(new DataGridViewCheckBoxColumn - { - Name = "Enabled", - HeaderText = "", - Width = 30, - }); + strategyGrid.Columns.Add( + new DataGridViewCheckBoxColumn + { + Name = "Enabled", + HeaderText = "", + Width = 30, + } + ); - strategyGrid.Columns.Add(new DataGridViewTextBoxColumn - { - Name = "Name", - HeaderText = "Strategy", - ReadOnly = true, - AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, - }); + strategyGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + Name = "Name", + HeaderText = "Strategy", + ReadOnly = true, + AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill, + } + ); - strategyGrid.Columns.Add(new DataGridViewTextBoxColumn - { - Name = "Phase", - HeaderText = "Phase", - ReadOnly = true, - Width = 100, - }); + strategyGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + Name = "Phase", + HeaderText = "Phase", + ReadOnly = true, + Width = 100, + } + ); - strategyGrid.Columns.Add(new DataGridViewTextBoxColumn - { - Name = "Order", - HeaderText = "Order", - ReadOnly = true, - Width = 55, - }); + strategyGrid.Columns.Add( + new DataGridViewTextBoxColumn + { + Name = "Order", + HeaderText = "Order", + ReadOnly = true, + Width = 55, + } + ); foreach (var strategy in FillStrategyRegistry.AllStrategies) { @@ -77,7 +85,9 @@ namespace OpenNest.Forms var disabledNames = ParseDisabledStrategies(Settings.Default.DisabledStrategies); foreach (DataGridViewRow row in strategyGrid.Rows) - row.Cells["Enabled"].Value = !disabledNames.Contains((string)row.Cells["Name"].Value); + row.Cells["Enabled"].Value = !disabledNames.Contains( + (string)row.Cells["Name"].Value + ); } private void SaveSettings() @@ -85,7 +95,8 @@ namespace OpenNest.Forms Settings.Default.NestTemplatePath = textBox1.Text; Settings.Default.CreateNewNestOnOpen = checkBox1.Checked; Settings.Default.AutoSizePlateFactor = (double)numericUpDown1.Value; - Settings.Default.ActiveColorScheme = colorSchemeCombo.SelectedItem as string ?? "Classic"; + Settings.Default.ActiveColorScheme = + colorSchemeCombo.SelectedItem as string ?? "Classic"; var disabledNames = new List(); foreach (DataGridViewRow row in strategyGrid.Rows) @@ -125,7 +136,8 @@ namespace OpenNest.Forms return new HashSet( value.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0), - StringComparer.OrdinalIgnoreCase); + StringComparer.OrdinalIgnoreCase + ); } private void SaveSettings_Click(object sender, EventArgs e) diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs index 3945204..b785d1b 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -1,9 +1,9 @@ -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; using GeoSize = OpenNest.Geometry.Size; namespace OpenNest.Forms @@ -64,14 +64,11 @@ namespace OpenNest.Forms e.Value = d.Name; } - private Drawing SelectedDrawingA => - cboDrawingA.SelectedItem as Drawing; + private Drawing SelectedDrawingA => cboDrawingA.SelectedItem as Drawing; - private Drawing SelectedDrawingB => - cboDrawingB.SelectedItem as Drawing; + private Drawing SelectedDrawingB => cboDrawingB.SelectedItem as Drawing; - private double PartSpacing => - (double)nudPartSpacing.Value; + private double PartSpacing => (double)nudPartSpacing.Value; private bool TryGetPlateSize(out GeoSize size) { @@ -167,12 +164,18 @@ namespace OpenNest.Forms var direction = new Vector(dx, dy); var len = System.Math.Sqrt(dx * dx + dy * dy); - if (len > 0) direction = new Vector(dx / len, dy / len); + if (len > 0) + direction = new Vector(dx / len, dy / len); var single = new List { part }; var obstacles = parts.Where(p => p != part).ToList(); - totalMoved += Compactor.Push(single, obstacles, - syntheticWorkArea, spacing, direction); + totalMoved += Compactor.Push( + single, + obstacles, + syntheticWorkArea, + spacing, + direction + ); } if (totalMoved < 0.01) @@ -250,7 +253,13 @@ namespace OpenNest.Forms Cursor = Cursors.WaitCursor; try { - var angles = new[] { 0.0, Math.Angle.ToRadians(90), Math.Angle.ToRadians(180), Math.Angle.ToRadians(270) }; + var angles = new[] + { + 0.0, + Math.Angle.ToRadians(90), + Math.Angle.ToRadians(180), + Math.Angle.ToRadians(270), + }; var bestCell = (List)null; var bestArea = double.MaxValue; @@ -314,11 +323,12 @@ namespace OpenNest.Forms applyDirection = NestDirection.Horizontal; // tie-break var choice = MessageBox.Show( - $"Apply {applyDirection} pattern ({(applyDirection == NestDirection.Horizontal ? hCount : vCount)} parts) to current plate?" + - "\n\nYes = Current plate (clears existing parts)\nNo = New plate", + $"Apply {applyDirection} pattern ({(applyDirection == NestDirection.Horizontal ? hCount : vCount)} parts) to current plate?" + + "\n\nYes = Current plate (clears existing parts)\nNo = New plate", "Apply Pattern", MessageBoxButtons.YesNoCancel, - MessageBoxIcon.Question); + MessageBoxIcon.Question + ); if (choice == DialogResult.Cancel) return; @@ -328,16 +338,23 @@ namespace OpenNest.Forms if (pattern == null) return; - var filler = new FillLinear(new Box(0, 0, plateSize.Length, plateSize.Width), PartSpacing) { Label = "PatternTile-Apply" }; + var filler = new FillLinear( + new Box(0, 0, plateSize.Length, plateSize.Width), + PartSpacing + ) + { + Label = "PatternTile-Apply", + }; var tiledParts = filler.Fill(pattern, applyDirection); Result = new PatternTileResult { Parts = tiledParts, - Target = choice == DialogResult.Yes - ? PatternTileTarget.CurrentPlate - : PatternTileTarget.NewPlate, - PlateSize = plateSize + Target = + choice == DialogResult.Yes + ? PatternTileTarget.CurrentPlate + : PatternTileTarget.NewPlate, + PlateSize = plateSize, }; DialogResult = DialogResult.OK; @@ -348,7 +365,7 @@ namespace OpenNest.Forms public enum PatternTileTarget { CurrentPlate, - NewPlate + NewPlate, } public class PatternTileResult diff --git a/OpenNest/Forms/PostProcessorConfigForm.cs b/OpenNest/Forms/PostProcessorConfigForm.cs index 0582da4..7ba8977 100644 --- a/OpenNest/Forms/PostProcessorConfigForm.cs +++ b/OpenNest/Forms/PostProcessorConfigForm.cs @@ -17,7 +17,10 @@ namespace OpenNest.Forms this.Text = postProcessor.Name + " Settings"; // Deep-clone config as JSON backup for cancel/restore - configBackup = JsonSerializer.Serialize(postProcessor.Config, postProcessor.Config.GetType()); + configBackup = JsonSerializer.Serialize( + postProcessor.Config, + postProcessor.Config.GetType() + ); propertyGrid.SelectedObject = postProcessor.Config; } diff --git a/OpenNest/Forms/RemnantViewerForm.cs b/OpenNest/Forms/RemnantViewerForm.cs index cd9d11a..d2631db 100644 --- a/OpenNest/Forms/RemnantViewerForm.cs +++ b/OpenNest/Forms/RemnantViewerForm.cs @@ -1,10 +1,10 @@ -using OpenNest.Controls; -using OpenNest.Engine.Fill; -using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Engine.Fill; +using OpenNest.Geometry; namespace OpenNest.Forms { @@ -100,9 +100,15 @@ namespace OpenNest.Forms switch (tr.Priority) { - case 0: item.BackColor = Color.FromArgb(220, 255, 220); break; - case 1: item.BackColor = Color.FromArgb(255, 255, 210); break; - default: item.BackColor = Color.FromArgb(255, 220, 220); break; + case 0: + item.BackColor = Color.FromArgb(220, 255, 220); + break; + case 1: + item.BackColor = Color.FromArgb(255, 255, 210); + break; + default: + item.BackColor = Color.FromArgb(255, 220, 220); + break; } listView.Items.Add(item); diff --git a/OpenNest/Forms/ShapeLibraryForm.cs b/OpenNest/Forms/ShapeLibraryForm.cs index 123ab7f..e427d35 100644 --- a/OpenNest/Forms/ShapeLibraryForm.cs +++ b/OpenNest/Forms/ShapeLibraryForm.cs @@ -1,4 +1,3 @@ -using OpenNest.Shapes; using System; using System.Collections.Generic; using System.Drawing; @@ -8,6 +7,7 @@ using System.Reflection; using System.Text.Json; using System.Text.RegularExpressions; using System.Windows.Forms; +using OpenNest.Shapes; namespace OpenNest.Forms { @@ -15,7 +15,7 @@ namespace OpenNest.Forms { private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, }; private readonly List addedDrawings = new List(); @@ -28,9 +28,10 @@ namespace OpenNest.Forms public ShapeLibraryForm(IEnumerable existingDrawingNames = null) { - existingNames = existingDrawingNames != null - ? new HashSet(existingDrawingNames, StringComparer.OrdinalIgnoreCase) - : new HashSet(StringComparer.OrdinalIgnoreCase); + existingNames = + existingDrawingNames != null + ? new HashSet(existingDrawingNames, StringComparer.OrdinalIgnoreCase) + : new HashSet(StringComparer.OrdinalIgnoreCase); InitializeComponent(); DiscoverShapes(); @@ -51,7 +52,8 @@ namespace OpenNest.Forms private void DiscoverShapes() { var baseType = typeof(ShapeDefinition); - var shapeTypes = baseType.Assembly.GetTypes() + var shapeTypes = baseType + .Assembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && baseType.IsAssignableFrom(t)) .OrderBy(t => t.Name) .ToList(); @@ -94,14 +96,16 @@ namespace OpenNest.Forms private void ShapeListBox_DrawItem(object sender, DrawItemEventArgs e) { - if (e.Index < 0) return; + if (e.Index < 0) + return; e.DrawBackground(); var entry = (ShapeEntry)shapeListBox.Items[e.Index]; - var textColor = (e.State & DrawItemState.Selected) != 0 - ? SystemColors.HighlightText - : SystemColors.ControlText; + var textColor = + (e.State & DrawItemState.Selected) != 0 + ? SystemColors.HighlightText + : SystemColors.ControlText; var text = entry.DisplayName; if (entry.HasConfigurations) @@ -119,7 +123,8 @@ namespace OpenNest.Forms private void ShapeListBox_SelectedIndexChanged(object sender, EventArgs e) { - if (shapeListBox.SelectedIndex < 0) return; + if (shapeListBox.SelectedIndex < 0) + return; selectedEntry = (ShapeEntry)shapeListBox.SelectedItem; suppressPreview = true; @@ -150,7 +155,8 @@ namespace OpenNest.Forms private void ConfigComboBox_SelectedIndexChanged(object sender, EventArgs e) { - if (configComboBox.SelectedIndex < 0 || selectedEntry == null) return; + if (configComboBox.SelectedIndex < 0 || selectedEntry == null) + return; var config = selectedEntry.Configurations[configComboBox.SelectedIndex]; nameTextBox.Text = config.Name; @@ -167,7 +173,10 @@ namespace OpenNest.Forms parametersPanel.Controls.Clear(); parameterBindings.Clear(); - var props = shapeType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + var props = shapeType + .GetProperties( + BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly + ) .Where(p => p.CanRead && p.CanWrite && p.Name != "Name") .ToArray(); @@ -180,7 +189,7 @@ namespace OpenNest.Forms { Text = FriendlyName(prop.Name), Location = new Point(parametersPanel.Padding.Left, y), - AutoSize = true + AutoSize = true, }; y += 18; @@ -192,7 +201,7 @@ namespace OpenNest.Forms { Location = new Point(parametersPanel.Padding.Left, y), AutoSize = true, - Checked = sourceValues != null && (bool)prop.GetValue(sourceValues) + Checked = sourceValues != null && (bool)prop.GetValue(sourceValues), }; cb.CheckedChanged += (s, ev) => UpdatePreview(); editor = cb; @@ -204,7 +213,7 @@ namespace OpenNest.Forms Location = new Point(parametersPanel.Padding.Left, y), Width = panelWidth, Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, - DropDownStyle = ComboBoxStyle.DropDownList + DropDownStyle = ComboBoxStyle.DropDownList, }; // Initial population: every entry; the filter runs on first UpdatePreview. @@ -226,7 +235,7 @@ namespace OpenNest.Forms { Location = new Point(parametersPanel.Padding.Left, y), Width = panelWidth, - Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right + Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right, }; if (sourceValues != null) @@ -254,14 +263,16 @@ namespace OpenNest.Forms private void UpdatePreview() { - if (suppressPreview || selectedEntry == null) return; + if (suppressPreview || selectedEntry == null) + return; UpdatePipeSizeFilter(); try { var shape = CreateShapeFromInputs(); - if (shape == null) return; + if (shape == null) + return; var drawing = shape.GetDrawing(); nameTextBox.Text = shape.GenerateName(); @@ -272,10 +283,12 @@ namespace OpenNest.Forms var bb = drawing.Program.BoundingBox(); var info = string.Format("{0:F3} x {1:F3}", bb.Size.Length, bb.Size.Width); - if (shape is PipeFlangeShape flange + if ( + shape is PipeFlangeShape flange && !flange.Blind && !string.IsNullOrEmpty(flange.PipeSize) - && !PipeSizes.TryGetOD(flange.PipeSize, out _)) + && !PipeSizes.TryGetOD(flange.PipeSize, out _) + ) { info += " — Invalid pipe size, no bore cut"; } @@ -293,7 +306,9 @@ namespace OpenNest.Forms { // Find the PipeSize combo and the numeric inputs it depends on. ComboBox pipeCombo = null; - double holePattern = 0, holeDia = 0, clearance = 0; + double holePattern = 0, + holeDia = 0, + clearance = 0; bool blind = false; foreach (var binding in parameterBindings) @@ -408,7 +423,8 @@ namespace OpenNest.Forms try { var shape = CreateShapeFromInputs(); - if (shape == null) return; + if (shape == null) + return; var drawing = shape.GetDrawing(); drawing.Name = GetUniqueName(drawing.Name); @@ -427,7 +443,8 @@ namespace OpenNest.Forms $"Failed to create shape: {ex.Message}", "Error", MessageBoxButtons.OK, - MessageBoxIcon.Warning); + MessageBoxIcon.Warning + ); } } diff --git a/OpenNest/Forms/SimplifierViewerForm.cs b/OpenNest/Forms/SimplifierViewerForm.cs index 5abcf09..0424874 100644 --- a/OpenNest/Forms/SimplifierViewerForm.cs +++ b/OpenNest/Forms/SimplifierViewerForm.cs @@ -102,8 +102,10 @@ public partial class SimplifierViewerForm : Form { var left = entity.OffsetEntity(tol, OffsetSide.Left); var right = entity.OffsetEntity(tol, OffsetSide.Right); - if (left != null) leftEntities.Add(left); - if (right != null) rightEntities.Add(right); + if (left != null) + leftEntities.Add(left); + if (right != null) + rightEntities.Add(right); } entityView.SimplifierToleranceLeft = leftEntities; entityView.SimplifierToleranceRight = rightEntities; @@ -113,7 +115,8 @@ public partial class SimplifierViewerForm : Form candidate.BoundingBox.X - tol * 2, candidate.BoundingBox.Y - tol * 2, candidate.BoundingBox.Length + tol * 4, - candidate.BoundingBox.Width + tol * 4); + candidate.BoundingBox.Width + tol * 4 + ); entityView.ZoomToArea(padded); } @@ -128,7 +131,8 @@ public partial class SimplifierViewerForm : Form private void OnToleranceChanged(object sender, System.EventArgs e) { - if (simplifier == null) return; + if (simplifier == null) + return; simplifier.Tolerance = (double)numTolerance.Value; entityView?.ClearSimplifierPreview(); RunAnalysis(); diff --git a/OpenNest/Forms/SplitDrawingForm.cs b/OpenNest/Forms/SplitDrawingForm.cs index 73eea76..11e1e63 100644 --- a/OpenNest/Forms/SplitDrawingForm.cs +++ b/OpenNest/Forms/SplitDrawingForm.cs @@ -35,8 +35,10 @@ public partial class SplitDrawingForm : Form InitializeComponent(); _drawing = drawing; - _drawingEntities = ConvertProgram.ToGeometry(drawing.Program) - .Where(e => e.Layer != SpecialLayers.Rapid).ToList(); + _drawingEntities = ConvertProgram + .ToGeometry(drawing.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); _drawingBounds = drawing.Program.BoundingBox(); foreach (var entity in _drawingEntities) @@ -80,7 +82,9 @@ public partial class SplitDrawingForm : Form { var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1; for (var i = 1; i <= splits; i++) - _splitLines.Add(new SplitLine(_drawingBounds.X + usable * i, CutOffAxis.Vertical)); + _splitLines.Add( + new SplitLine(_drawingBounds.X + usable * i, CutOffAxis.Vertical) + ); } } else if (axisIndex == 2) @@ -90,19 +94,31 @@ public partial class SplitDrawingForm : Form { var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1; for (var i = 1; i <= splits; i++) - _splitLines.Add(new SplitLine(_drawingBounds.Y + usable * i, CutOffAxis.Horizontal)); + _splitLines.Add( + new SplitLine(_drawingBounds.Y + usable * i, CutOffAxis.Horizontal) + ); } } else { - _splitLines.AddRange(AutoSplitCalculator.FitToPlate(_drawingBounds, plateW, plateH, spacing, overhang)); + _splitLines.AddRange( + AutoSplitCalculator.FitToPlate( + _drawingBounds, + plateW, + plateH, + spacing, + overhang + ) + ); } } else if (radByCount.Checked) { var hPieces = (int)nudHorizontalPieces.Value; var vPieces = (int)nudVerticalPieces.Value; - _splitLines.AddRange(AutoSplitCalculator.SplitByCount(_drawingBounds, hPieces, vPieces)); + _splitLines.AddRange( + AutoSplitCalculator.SplitByCount(_drawingBounds, hPieces, vPieces) + ); } InitializeAllFeaturePositions(); @@ -173,8 +189,10 @@ public partial class SplitDrawingForm : Form private int GetFeatureCount() { - if (radTabs.Checked) return (int)nudTabCount.Value; - if (radSpike.Checked) return (int)nudSpikePairCount.Value; + if (radTabs.Checked) + return (int)nudTabCount.Value; + if (radSpike.Checked) + return (int)nudSpikePairCount.Value; return 0; } @@ -199,7 +217,8 @@ public partial class SplitDrawingForm : Form var extent = end - start; sl.FeaturePositions.Clear(); - if (count <= 0 || extent <= 0) return; + if (count <= 0 || extent <= 0) + return; if (radSpike.Checked) { @@ -233,7 +252,8 @@ public partial class SplitDrawingForm : Form private void OnPreviewMouseDown(object sender, MouseEventArgs e) { - if (e.Button != MouseButtons.Left) return; + if (e.Button != MouseButtons.Left) + return; var worldPt = pnlPreview.PointControlToWorld(e.Location); @@ -279,9 +299,14 @@ public partial class SplitDrawingForm : Form { _hoverLineIndex = lineIdx; _hoverFeatureIndex = featIdx; - pnlPreview.Cursor = _hoverLineIndex >= 0 - ? (_splitLines[_hoverLineIndex].Axis == CutOffAxis.Vertical ? Cursors.SizeNS : Cursors.SizeWE) - : Cursors.Cross; + pnlPreview.Cursor = + _hoverLineIndex >= 0 + ? ( + _splitLines[_hoverLineIndex].Axis == CutOffAxis.Vertical + ? Cursors.SizeNS + : Cursors.SizeWE + ) + : Cursors.Cross; pnlPreview.Invalidate(); } } @@ -307,7 +332,8 @@ public partial class SplitDrawingForm : Form private (int lineIndex, int featureIndex) HitTestFeatureHandle(Vector worldPt) { - if (radStraight.Checked) return (-1, -1); + if (radStraight.Checked) + return (-1, -1); var hitRadius = HandleRadius / pnlPreview.ViewScale; for (var li = 0; li < _splitLines.Count; li++) @@ -337,7 +363,8 @@ public partial class SplitDrawingForm : Form { if (keyData == Keys.Space) { - _currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical; + _currentAxis = + _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical; pnlPreview.Invalidate(); return true; } @@ -377,8 +404,13 @@ public partial class SplitDrawingForm : Form var r = regions[i]; var tl = pnlPreview.PointWorldToGraph(r.Left, r.Top); var br = pnlPreview.PointWorldToGraph(r.Right, r.Bottom); - g.FillRectangle(brush, System.Math.Min(tl.X, br.X), System.Math.Min(tl.Y, br.Y), - System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y)); + g.FillRectangle( + brush, + System.Math.Min(tl.X, br.X), + System.Math.Min(tl.Y, br.Y), + System.Math.Abs(br.X - tl.X), + System.Math.Abs(br.Y - tl.Y) + ); } // Piece number and dimension labels at center of each region @@ -388,7 +420,11 @@ public partial class SplitDrawingForm : Form using var dimFont = new Font("Segoe UI", 11f, FontStyle.Regular, GraphicsUnit.Pixel); using var labelBrush = new SolidBrush(Color.FromArgb(200, 255, 255, 255)); using var shadowBrush = new SolidBrush(Color.FromArgb(160, 0, 0, 0)); - var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + var sf = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center, + }; for (var i = 0; i < regions.Count; i++) { @@ -481,7 +517,8 @@ public partial class SplitDrawingForm : Form var pos = isVert ? snapped.X : snapped.Y; var margin = 10.0; - PointF pp1, pp2; + PointF pp1, + pp2; if (isVert) { pp1 = pnlPreview.PointWorldToGraph(pos, _drawingBounds.Bottom - margin); @@ -496,8 +533,6 @@ public partial class SplitDrawingForm : Form using var previewPen = new Pen(Color.FromArgb(180, 255, 213, 79), 1.5f); previewPen.DashStyle = DashStyle.DashDot; g.DrawLine(previewPen, pp1, pp2); - - } // Feature position handles @@ -511,15 +546,26 @@ public partial class SplitDrawingForm : Form var center = pnlPreview.PointWorldToGraph(GetFeatureHandleWorld(sl, fi)); var isDrag = li == _dragLineIndex && fi == _dragFeatureIndex; var isHover = li == _hoverLineIndex && fi == _hoverFeatureIndex; - var fillColor = isDrag ? Color.FromArgb(255, 82, 82) - : isHover ? Color.FromArgb(255, 183, 77) - : Color.White; + var fillColor = + isDrag ? Color.FromArgb(255, 82, 82) + : isHover ? Color.FromArgb(255, 183, 77) + : Color.White; using var fill = new SolidBrush(fillColor); using var border = new Pen(Color.FromArgb(80, 80, 80)); - g.FillEllipse(fill, center.X - HandleRadius, center.Y - HandleRadius, - HandleRadius * 2, HandleRadius * 2); - g.DrawEllipse(border, center.X - HandleRadius, center.Y - HandleRadius, - HandleRadius * 2, HandleRadius * 2); + g.FillEllipse( + fill, + center.X - HandleRadius, + center.Y - HandleRadius, + HandleRadius * 2, + HandleRadius * 2 + ); + g.DrawEllipse( + border, + center.X - HandleRadius, + center.Y - HandleRadius, + HandleRadius * 2, + HandleRadius * 2 + ); } } } @@ -532,13 +578,19 @@ public partial class SplitDrawingForm : Form Color.FromArgb(40, 255, 183, 77), Color.FromArgb(40, 206, 147, 216), Color.FromArgb(40, 255, 138, 128), - Color.FromArgb(40, 128, 222, 234) + Color.FromArgb(40, 128, 222, 234), }; private List BuildPreviewRegions() { - var verticals = _splitLines.Where(l => l.Axis == CutOffAxis.Vertical).OrderBy(l => l.Position).ToList(); - var horizontals = _splitLines.Where(l => l.Axis == CutOffAxis.Horizontal).OrderBy(l => l.Position).ToList(); + var verticals = _splitLines + .Where(l => l.Axis == CutOffAxis.Vertical) + .OrderBy(l => l.Position) + .ToList(); + var horizontals = _splitLines + .Where(l => l.Axis == CutOffAxis.Horizontal) + .OrderBy(l => l.Position) + .ToList(); var xEdges = new List { _drawingBounds.Left }; xEdges.AddRange(verticals.Select(v => v.Position)); @@ -550,8 +602,15 @@ public partial class SplitDrawingForm : Form var regions = new List(); for (var yi = 0; yi < yEdges.Count - 1; yi++) - for (var xi = 0; xi < xEdges.Count - 1; xi++) - regions.Add(new Box(xEdges[xi], yEdges[yi], xEdges[xi + 1] - xEdges[xi], yEdges[yi + 1] - yEdges[yi])); + for (var xi = 0; xi < xEdges.Count - 1; xi++) + regions.Add( + new Box( + xEdges[xi], + yEdges[yi], + xEdges[xi + 1] - xEdges[xi], + yEdges[yi + 1] - yEdges[yi] + ) + ); return regions; } @@ -562,7 +621,12 @@ public partial class SplitDrawingForm : Form { if (_splitLines.Count == 0) { - MessageBox.Show("No split lines defined.", "Split Drawing", MessageBoxButtons.OK, MessageBoxIcon.Information); + MessageBox.Show( + "No split lines defined.", + "Split Drawing", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); return; } @@ -598,7 +662,8 @@ public partial class SplitDrawingForm : Form private void UpdateUI() { var pieceCount = _splitLines.Count == 0 ? 1 : BuildPreviewRegions().Count; - lblStatus.Text = $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces"; + lblStatus.Text = + $"Part: {_drawingBounds.Width:F2} x {_drawingBounds.Length:F2} | {_splitLines.Count} split lines | {pieceCount} pieces"; } // --- Feature rendering helpers --- @@ -609,7 +674,7 @@ public partial class SplitDrawingForm : Form { SplitType.WeldGapTabs => new WeldGapTabSplit(), SplitType.SpikeGroove => new SpikeGrooveSplit(), - _ => new StraightSplit() + _ => new StraightSplit(), }; } @@ -618,12 +683,18 @@ public partial class SplitDrawingForm : Form return p.Type switch { SplitType.WeldGapTabs => p.TabWidth / 2, - SplitType.SpikeGroove => p.GrooveDepth * System.Math.Tan(OpenNest.Math.Angle.ToRadians(p.SpikeAngle / 2)), - _ => 0 + SplitType.SpikeGroove => p.GrooveDepth + * System.Math.Tan(OpenNest.Math.Angle.ToRadians(p.SpikeAngle / 2)), + _ => 0, }; } - private void DrawFeatureEdge(Graphics g, Pen pen, List entities, bool isVertical) + private void DrawFeatureEdge( + Graphics g, + Pen pen, + List entities, + bool isVertical + ) { foreach (var entity in entities) { diff --git a/OpenNest/Forms/TimingForm.cs b/OpenNest/Forms/TimingForm.cs index e8cf6c3..5e45902 100644 --- a/OpenNest/Forms/TimingForm.cs +++ b/OpenNest/Forms/TimingForm.cs @@ -20,12 +20,20 @@ namespace OpenNest.Forms public void SetCutDistance(double dist) { - cutDistanceLabel.Text = string.Format("{0} {1}", System.Math.Round(dist, 4), UnitsHelper.GetShortString(Units)); + cutDistanceLabel.Text = string.Format( + "{0} {1}", + System.Math.Round(dist, 4), + UnitsHelper.GetShortString(Units) + ); } public void SetRapidDistance(double dist) { - rapidDistanceLabel.Text = string.Format("{0} {1}", System.Math.Round(dist, 4), UnitsHelper.GetShortString(Units)); + rapidDistanceLabel.Text = string.Format( + "{0} {1}", + System.Math.Round(dist, 4), + UnitsHelper.GetShortString(Units) + ); } public void SetIntersectionCount(int count) @@ -40,8 +48,18 @@ namespace OpenNest.Forms public void SetCutParameters(CutParameters cutparams) { - feedrateLabel.Text = string.Format("{0} {1}/{2}", cutparams.Feedrate, UnitsHelper.GetShortString(Units), UnitsHelper.GetShortTimeUnit(Units)); - rapidLabel.Text = string.Format("{0} {1}/{2}", cutparams.RapidTravelRate, UnitsHelper.GetShortString(Units), UnitsHelper.GetShortTimeUnit(Units)); + feedrateLabel.Text = string.Format( + "{0} {1}/{2}", + cutparams.Feedrate, + UnitsHelper.GetShortString(Units), + UnitsHelper.GetShortTimeUnit(Units) + ); + rapidLabel.Text = string.Format( + "{0} {1}/{2}", + cutparams.RapidTravelRate, + UnitsHelper.GetShortString(Units), + UnitsHelper.GetShortTimeUnit(Units) + ); pierceTimeLabel.Text = GetTimeMsg(cutparams.PierceTime); } diff --git a/OpenNest/GraphicsHelper.cs b/OpenNest/GraphicsHelper.cs index 51dc8e8..3487c95 100644 --- a/OpenNest/GraphicsHelper.cs +++ b/OpenNest/GraphicsHelper.cs @@ -1,8 +1,8 @@ -using OpenNest.CNC; +using System.Drawing; +using System.Drawing.Drawing2D; +using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; -using System.Drawing; -using System.Drawing.Drawing2D; namespace OpenNest { @@ -47,7 +47,12 @@ namespace OpenNest return pgm.GetImage(size, pen, null); } - public static Image GetImage(this Program pgm, System.Drawing.Size size, Pen pen, Brush brush) + public static Image GetImage( + this Program pgm, + System.Drawing.Size size, + Pen pen, + Brush brush + ) { var img = new Bitmap(size.Width, size.Height); var path = pgm.GetGraphicsPath(); @@ -66,7 +71,8 @@ namespace OpenNest var offset = new PointF( (size.Width - bounds.Width) * 0.5f - bounds.X, - (size.Height - bounds.Height) * 0.5f - bounds.Y); + (size.Height - bounds.Height) * 0.5f - bounds.Y + ); var graphics = Graphics.FromImage(img); graphics.TranslateTransform(offset.X, offset.Y); @@ -85,8 +91,12 @@ namespace OpenNest return img; } - public static void GetGraphicsPaths(this Program pgm, Vector origin, - out GraphicsPath cutPath, out GraphicsPath leadPath) + public static void GetGraphicsPaths( + this Program pgm, + Vector origin, + out GraphicsPath cutPath, + out GraphicsPath leadPath + ) { cutPath = new GraphicsPath(); leadPath = new GraphicsPath(); @@ -95,8 +105,13 @@ namespace OpenNest AddProgramSplit(cutPath, leadPath, pgm, pgm.Mode, ref curpos); } - private static void AddProgramSplit(GraphicsPath cutPath, GraphicsPath leadPath, - Program pgm, Mode mode, ref Vector curpos) + private static void AddProgramSplit( + GraphicsPath cutPath, + GraphicsPath leadPath, + Program pgm, + Mode mode, + ref Vector curpos + ) { // Capture the frame origin at entry. Sub-program Offsets are relative // to this fixed origin, not to the current tool position. @@ -114,12 +129,15 @@ namespace OpenNest if (arc.Suppressed) { var endpt = arc.EndPoint; - if (mode == Mode.Incremental) endpt += curpos; + if (mode == Mode.Incremental) + endpt += curpos; curpos = endpt; break; } - var arcPath = (arc.Layer == LayerType.Leadin || arc.Layer == LayerType.Leadout) - ? leadPath : cutPath; + var arcPath = + (arc.Layer == LayerType.Leadin || arc.Layer == LayerType.Leadout) + ? leadPath + : cutPath; AddArc(arcPath, arc, mode, ref curpos); break; @@ -128,12 +146,15 @@ namespace OpenNest if (line.Suppressed) { var endpt = line.EndPoint; - if (mode == Mode.Incremental) endpt += curpos; + if (mode == Mode.Incremental) + endpt += curpos; curpos = endpt; break; } - var linePath = (line.Layer == LayerType.Leadin || line.Layer == LayerType.Leadout) - ? leadPath : cutPath; + var linePath = + (line.Layer == LayerType.Leadin || line.Layer == LayerType.Leadout) + ? leadPath + : cutPath; AddLine(linePath, line, mode, ref curpos); break; @@ -161,7 +182,10 @@ namespace OpenNest { cutPath.StartFigure(); leadPath.StartFigure(); - curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y); + curpos = new Vector( + frameOrigin.X + subpgm.Offset.X, + frameOrigin.Y + subpgm.Offset.Y + ); AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos); } mode = tmpmode; @@ -182,14 +206,14 @@ namespace OpenNest } // start angle in degrees - var startAngle = Angle.ToDegrees(System.Math.Atan2( - curpos.Y - center.Y, - curpos.X - center.X)); + var startAngle = Angle.ToDegrees( + System.Math.Atan2(curpos.Y - center.Y, curpos.X - center.X) + ); // end angle in degrees - var endAngle = Angle.ToDegrees(System.Math.Atan2( - endpt.Y - center.Y, - endpt.X - center.X)); + var endAngle = Angle.ToDegrees( + System.Math.Atan2(endpt.Y - center.Y, endpt.X - center.X) + ); endAngle = Angle.NormalizeDeg(endAngle); startAngle = Angle.NormalizeDeg(startAngle); @@ -215,17 +239,18 @@ namespace OpenNest { var sweepAngle = (endAngle - startAngle); - path.AddArc( - pt.X, pt.Y, - size, size, - (float)startAngle, - (float)sweepAngle); + path.AddArc(pt.X, pt.Y, size, size, (float)startAngle, (float)sweepAngle); } curpos = endpt; } - private static void AddLine(GraphicsPath path, LinearMove line, Mode mode, ref Vector curpos) + private static void AddLine( + GraphicsPath path, + LinearMove line, + Mode mode, + ref Vector curpos + ) { var pt = line.EndPoint; @@ -279,14 +304,16 @@ namespace OpenNest var arc = (ArcMove)code; if (arc.Layer != LayerType.Leadin && arc.Layer != LayerType.Leadout) { - if (currentFigure == null) currentFigure = new GraphicsPath(); + if (currentFigure == null) + currentFigure = new GraphicsPath(); AddArc(currentFigure, arc, mode, ref curpos); } else { Flush(); var endpt = arc.EndPoint; - if (mode == Mode.Incremental) endpt += curpos; + if (mode == Mode.Incremental) + endpt += curpos; curpos = endpt; } } @@ -297,14 +324,16 @@ namespace OpenNest var line = (LinearMove)code; if (line.Layer != LayerType.Leadin && line.Layer != LayerType.Leadout) { - if (currentFigure == null) currentFigure = new GraphicsPath(); + if (currentFigure == null) + currentFigure = new GraphicsPath(); AddLine(currentFigure, line, mode, ref curpos); } else { Flush(); var endpt = line.EndPoint; - if (mode == Mode.Incremental) endpt += curpos; + if (mode == Mode.Incremental) + endpt += curpos; curpos = endpt; } } @@ -325,20 +354,23 @@ namespace OpenNest break; case CodeType.SubProgramCall: + { + Flush(); + var tmpmode = mode; + var subpgm = (SubProgramCall)code; + + if (subpgm.Program != null) { - Flush(); - var tmpmode = mode; - var subpgm = (SubProgramCall)code; - - if (subpgm.Program != null) - { - curpos = new Vector(frameOrigin.X + subpgm.Offset.X, frameOrigin.Y + subpgm.Offset.Y); - AddProgram(path, subpgm.Program, mode, ref curpos); - } - - mode = tmpmode; - break; + curpos = new Vector( + frameOrigin.X + subpgm.Offset.X, + frameOrigin.Y + subpgm.Offset.Y + ); + AddProgram(path, subpgm.Program, mode, ref curpos); } + + mode = tmpmode; + break; + } } } @@ -365,7 +397,8 @@ namespace OpenNest (float)diameter, (float)diameter, (float)(startAngle), - (float)sweepAngle); + (float)sweepAngle + ); } private static void AddCircle(GraphicsPath path, Circle circle) @@ -376,7 +409,8 @@ namespace OpenNest (float)(circle.Center.X - circle.Radius), (float)(circle.Center.Y - circle.Radius), (float)diameter, - (float)diameter); + (float)diameter + ); } private static void AddLine(GraphicsPath path, Line line) @@ -385,7 +419,8 @@ namespace OpenNest (float)line.StartPoint.X, (float)line.StartPoint.Y, (float)line.EndPoint.X, - (float)line.EndPoint.Y); + (float)line.EndPoint.Y + ); } private static void AddShape(GraphicsPath path, Shape shape) @@ -394,8 +429,18 @@ namespace OpenNest { if (entity.Layer != null) { - if (string.Equals(entity.Layer.Name, SpecialLayers.Leadin.Name, System.StringComparison.OrdinalIgnoreCase) || - string.Equals(entity.Layer.Name, SpecialLayers.Leadout.Name, System.StringComparison.OrdinalIgnoreCase)) + if ( + string.Equals( + entity.Layer.Name, + SpecialLayers.Leadin.Name, + System.StringComparison.OrdinalIgnoreCase + ) + || string.Equals( + entity.Layer.Name, + SpecialLayers.Leadout.Name, + System.StringComparison.OrdinalIgnoreCase + ) + ) { continue; } diff --git a/OpenNest/LayoutPart.cs b/OpenNest/LayoutPart.cs index e705fd4..225a04b 100644 --- a/OpenNest/LayoutPart.cs +++ b/OpenNest/LayoutPart.cs @@ -1,11 +1,11 @@ -using OpenNest.Controls; -using OpenNest.Converters; -using OpenNest.Geometry; -using System.Collections.Generic; +using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Windows.Forms; +using OpenNest.Controls; +using OpenNest.Converters; +using OpenNest.Geometry; namespace OpenNest { @@ -111,9 +111,16 @@ namespace OpenNest using var sf = new StringFormat { Alignment = StringAlignment.Center, - LineAlignment = StringAlignment.Center + LineAlignment = StringAlignment.Center, }; - g.DrawString(id, programIdFont, Brushes.Black, _labelScreenPoint.X, _labelScreenPoint.Y, sf); + g.DrawString( + id, + programIdFont, + Brushes.Black, + _labelScreenPoint.X, + _labelScreenPoint.Y, + sf + ); } public GraphicsPath OffsetPath { get; private set; } @@ -128,7 +135,10 @@ namespace OpenNest if (shapes.Count == 0) { var bbox = BasePart.BaseDrawing.Program.BoundingBox(); - return new Vector(bbox.Location.X + bbox.Length / 2, bbox.Location.Y + bbox.Width / 2); + return new Vector( + bbox.Location.X + bbox.Length / 2, + bbox.Location.Y + bbox.Width / 2 + ); } var profile = new ShapeProfile(nonRapid); @@ -150,7 +160,11 @@ namespace OpenNest { if (BasePart.HasManualLeadIns) { - BasePart.Program.GetGraphicsPaths(BasePart.Location, out var cutPath, out var leadPath); + BasePart.Program.GetGraphicsPaths( + BasePart.Location, + out var cutPath, + out var leadPath + ); cutPath.Transform(plateView.Matrix); leadPath.Transform(plateView.Matrix); Path = cutPath; @@ -169,7 +183,8 @@ namespace OpenNest var rotatedLabel = _labelPoint.Value.Rotate(BasePart.Rotation); var labelPt = new PointF( (float)(rotatedLabel.X + BasePart.Location.X), - (float)(rotatedLabel.Y + BasePart.Location.Y)); + (float)(rotatedLabel.Y + BasePart.Location.Y) + ); var pts = new[] { labelPt }; plateView.Matrix.TransformPoints(pts); _labelScreenPoint = pts[0]; @@ -179,10 +194,12 @@ namespace OpenNest public void UpdateOffset(double spacing, double tolerance, Matrix matrix) { - if (_offsetPolygonPoints == null || - spacing != _cachedOffsetSpacing || - tolerance != _cachedOffsetTolerance || - BasePart.Rotation != _cachedOffsetRotation) + if ( + _offsetPolygonPoints == null + || spacing != _cachedOffsetSpacing + || tolerance != _cachedOffsetTolerance + || BasePart.Rotation != _cachedOffsetRotation + ) { _offsetPolygonPoints = ComputeOffsetPolygons(spacing, tolerance); _cachedOffsetSpacing = spacing; @@ -203,7 +220,8 @@ namespace OpenNest var result = new List(); var entities = ConvertProgram.ToGeometry(BasePart.Program); var profile = new ShapeProfile( - entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()); + entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList() + ); AddOffsetPolygon(result, profile.Perimeter.OffsetOutward(spacing), tolerance); @@ -213,7 +231,11 @@ namespace OpenNest return result; } - private static void AddOffsetPolygon(List result, Shape offsetEntity, double tolerance) + private static void AddOffsetPolygon( + List result, + Shape offsetEntity, + double tolerance + ) { if (offsetEntity == null) return; diff --git a/OpenNest/MainApp.cs b/OpenNest/MainApp.cs index 952cdfc..67660c0 100644 --- a/OpenNest/MainApp.cs +++ b/OpenNest/MainApp.cs @@ -1,6 +1,6 @@ -using OpenNest.Forms; using System; using System.Windows.Forms; +using OpenNest.Forms; namespace OpenNest { diff --git a/OpenNest/MdiExtensions.cs b/OpenNest/MdiExtensions.cs index 740fd0a..300fd0f 100644 --- a/OpenNest/MdiExtensions.cs +++ b/OpenNest/MdiExtensions.cs @@ -31,9 +31,20 @@ namespace OpenNest Win32.SetWindowLong(c.Handle, Win32.GWL_EXSTYLE, windowLong); // Update the non-client area. - Win32.SetWindowPos(client.Handle, IntPtr.Zero, 0, 0, 0, 0, - Win32.SWP_NOACTIVATE | Win32.SWP_NOMOVE | Win32.SWP_NOSIZE | Win32.SWP_NOZORDER | - Win32.SWP_NOOWNERZORDER | Win32.SWP_FRAMECHANGED); + Win32.SetWindowPos( + client.Handle, + IntPtr.Zero, + 0, + 0, + 0, + 0, + Win32.SWP_NOACTIVATE + | Win32.SWP_NOMOVE + | Win32.SWP_NOSIZE + | Win32.SWP_NOZORDER + | Win32.SWP_NOOWNERZORDER + | Win32.SWP_FRAMECHANGED + ); return true; } diff --git a/OpenNest/PostProcessorMaterials.cs b/OpenNest/PostProcessorMaterials.cs index beadcee..39f4bb6 100644 --- a/OpenNest/PostProcessorMaterials.cs +++ b/OpenNest/PostProcessorMaterials.cs @@ -17,8 +17,10 @@ namespace OpenNest foreach (var name in provider.GetMaterialNames()) { - if (!string.IsNullOrWhiteSpace(name) - && !materials.Contains(name, StringComparer.OrdinalIgnoreCase)) + if ( + !string.IsNullOrWhiteSpace(name) + && !materials.Contains(name, StringComparer.OrdinalIgnoreCase) + ) { materials.Add(name); } diff --git a/OpenNest/SelectionType.cs b/OpenNest/SelectionType.cs index fc172f2..debabdf 100644 --- a/OpenNest/SelectionType.cs +++ b/OpenNest/SelectionType.cs @@ -3,6 +3,6 @@ public enum SelectionType { Intersect, - Contains + Contains, } } diff --git a/OpenNest/ToolStripRenderer.cs b/OpenNest/ToolStripRenderer.cs index 6b41021..936d75a 100644 --- a/OpenNest/ToolStripRenderer.cs +++ b/OpenNest/ToolStripRenderer.cs @@ -12,7 +12,7 @@ namespace OpenNest MediaToolbar, CommunicationsToolbar, BrowserTabBar, - HelpBar + HelpBar, } /// Renders a toolstrip using the UxTheme API via VisualStyleRenderer and a specific style. @@ -30,7 +30,7 @@ namespace OpenNest /// It shouldn't be necessary to P/Invoke like this, however VisualStyleRenderer.GetMargins /// misses out a parameter in its own P/Invoke. /// - static internal class NativeMethods + internal static class NativeMethods { [StructLayout(LayoutKind.Sequential)] public struct MARGINS @@ -42,7 +42,15 @@ namespace OpenNest } [DllImport("uxtheme.dll")] - public extern static int GetThemeMargins(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, int iPropId, IntPtr rect, out MARGINS pMargins); + public static extern int GetThemeMargins( + IntPtr hTheme, + IntPtr hdc, + int iPartId, + int iStateId, + int iPropId, + IntPtr rect, + out MARGINS pMargins + ); } // See http://msdn2.microsoft.com/en-us/library/bb773210.aspx - "Parts and States" @@ -68,13 +76,13 @@ namespace OpenNest SystemClose = 17, SystemMaximize = 18, SystemMinimize = 19, - SystemRestore = 20 + SystemRestore = 20, } enum MenuBarStates : int { Active = 1, - Inactive = 2 + Inactive = 2, } enum MenuBarItemStates : int @@ -84,7 +92,7 @@ namespace OpenNest Pushed = 3, Disabled = 4, DisabledHover = 5, - DisabledPushed = 6 + DisabledPushed = 6, } enum MenuPopupItemStates : int @@ -92,7 +100,7 @@ namespace OpenNest Normal = 1, Hover = 2, Disabled = 3, - DisabledHover = 4 + DisabledHover = 4, } enum MenuPopupCheckStates : int @@ -100,27 +108,27 @@ namespace OpenNest CheckmarkNormal = 1, CheckmarkDisabled = 2, BulletNormal = 3, - BulletDisabled = 4 + BulletDisabled = 4, } enum MenuPopupCheckBackgroundStates : int { Disabled = 1, Normal = 2, - Bitmap = 3 + Bitmap = 3, } enum MenuPopupSubMenuStates : int { Normal = 1, - Disabled = 2 + Disabled = 2, } enum MarginTypes : int { Sizing = 3601, Content = 3602, - Caption = 3603 + Caption = 3603, } static readonly int RebarBackground = 6; @@ -131,8 +139,24 @@ namespace OpenNest try { IntPtr hDC = dc.GetHdc(); - if (0 == NativeMethods.GetThemeMargins(renderer.Handle, hDC, renderer.Part, renderer.State, (int)marginType, IntPtr.Zero, out margins)) - return new Padding(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight); + if ( + 0 + == NativeMethods.GetThemeMargins( + renderer.Handle, + hDC, + renderer.Part, + renderer.State, + (int)marginType, + IntPtr.Zero, + out margins + ) + ) + return new Padding( + margins.cxLeftWidth, + margins.cyTopHeight, + margins.cxRightWidth, + margins.cyBottomHeight + ); return new Padding(0); } finally @@ -149,46 +173,37 @@ namespace OpenNest { if (item.Enabled) return hot ? (int)MenuPopupItemStates.Hover : (int)MenuPopupItemStates.Normal; - return hot ? (int)MenuPopupItemStates.DisabledHover : (int)MenuPopupItemStates.Disabled; + return hot + ? (int)MenuPopupItemStates.DisabledHover + : (int)MenuPopupItemStates.Disabled; } else { if (item.Pressed) - return item.Enabled ? (int)MenuBarItemStates.Pushed : (int)MenuBarItemStates.DisabledPushed; + return item.Enabled + ? (int)MenuBarItemStates.Pushed + : (int)MenuBarItemStates.DisabledPushed; if (item.Enabled) return hot ? (int)MenuBarItemStates.Hover : (int)MenuBarItemStates.Normal; return hot ? (int)MenuBarItemStates.DisabledHover : (int)MenuBarItemStates.Disabled; } } - public ToolbarTheme Theme - { - get; - set; - } + public ToolbarTheme Theme { get; set; } private string RebarClass { - get - { - return SubclassPrefix + "Rebar"; - } + get { return SubclassPrefix + "Rebar"; } } private string ToolbarClass { - get - { - return SubclassPrefix + "ToolBar"; - } + get { return SubclassPrefix + "ToolBar"; } } private string MenuClass { - get - { - return SubclassPrefix + "Menu"; - } + get { return SubclassPrefix + "Menu"; } } private string SubclassPrefix @@ -197,19 +212,27 @@ namespace OpenNest { switch (Theme) { - case ToolbarTheme.MediaToolbar: return "Media::"; - case ToolbarTheme.CommunicationsToolbar: return "Communications::"; - case ToolbarTheme.BrowserTabBar: return "BrowserTabBar::"; - case ToolbarTheme.HelpBar: return "Help::"; - default: return string.Empty; + case ToolbarTheme.MediaToolbar: + return "Media::"; + case ToolbarTheme.CommunicationsToolbar: + return "Communications::"; + case ToolbarTheme.BrowserTabBar: + return "BrowserTabBar::"; + case ToolbarTheme.HelpBar: + return "Help::"; + default: + return string.Empty; } } } private VisualStyleElement Subclass(VisualStyleElement element) { - return VisualStyleElement.CreateElement(SubclassPrefix + element.ClassName, - element.Part, element.State); + return VisualStyleElement.CreateElement( + SubclassPrefix + element.ClassName, + element.Part, + element.State + ); } private bool EnsureRenderer() @@ -233,8 +256,8 @@ namespace OpenNest } // Using just ToolStripManager.Renderer without setting the Renderer individually per ToolStrip means - // that the ToolStrip is not passed to the Initialize method. ToolStripPanels, however, are. So we can - // simply initialize it here too, and this should guarantee that the ToolStrip is initialized at least + // that the ToolStrip is not passed to the Initialize method. ToolStripPanels, however, are. So we can + // simply initialize it here too, and this should guarantee that the ToolStrip is initialized at least // once. Hopefully it isn't any more complicated than this. protected override void InitializePanel(ToolStripPanel toolStripPanel) { @@ -260,7 +283,11 @@ namespace OpenNest insideRect.Inflate(-1, -1); e.Graphics.ExcludeClip(insideRect); - renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds); + renderer.DrawBackground( + e.Graphics, + e.ToolStrip.ClientRectangle, + e.AffectedBounds + ); // Restore the old clip in case the Graphics is used again (does that ever happen?) e.Graphics.Clip = oldClip; @@ -281,7 +308,7 @@ namespace OpenNest // This ensures that's the case. Rectangle rect = item.Bounds; - // The background rectangle should be inset two pixels horizontally (on both sides), but we have + // The background rectangle should be inset two pixels horizontally (on both sides), but we have // to take into account the border. rect.X = item.ContentRectangle.X + 1; rect.Width = item.ContentRectangle.Width - 1; @@ -295,7 +322,9 @@ namespace OpenNest { if (EnsureRenderer()) { - int partID = e.Item.IsOnDropDown ? (int)MenuParts.PopupItem : (int)MenuParts.BarItem; + int partID = e.Item.IsOnDropDown + ? (int)MenuParts.PopupItem + : (int)MenuParts.BarItem; renderer.SetParameters(MenuClass, partID, GetItemState(e.Item)); Rectangle bgRect = GetBackgroundRectangle(e.Item); @@ -313,7 +342,11 @@ namespace OpenNest { // Draw the background using Rebar & RP_BACKGROUND (or, if that is not available, fall back to // Rebar.Band.Normal) - if (VisualStyleRenderer.IsElementDefined(VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0))) + if ( + VisualStyleRenderer.IsElementDefined( + VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0) + ) + ) { renderer.SetParameters(RebarClass, RebarBackground, 0); } @@ -323,7 +356,11 @@ namespace OpenNest } if (renderer.IsBackgroundPartiallyTransparent()) - renderer.DrawParentBackground(e.Graphics, e.ToolStripPanel.ClientRectangle, e.ToolStripPanel); + renderer.DrawParentBackground( + e.Graphics, + e.ToolStripPanel.ClientRectangle, + e.ToolStripPanel + ); renderer.DrawBackground(e.Graphics, e.ToolStripPanel.ClientRectangle); @@ -336,7 +373,9 @@ namespace OpenNest } // Render the background of an actual menu bar, dropdown menu or toolbar. - protected override void OnRenderToolStripBackground(System.Windows.Forms.ToolStripRenderEventArgs e) + protected override void OnRenderToolStripBackground( + System.Windows.Forms.ToolStripRenderEventArgs e + ) { if (EnsureRenderer()) { @@ -361,7 +400,11 @@ namespace OpenNest // A lone toolbar/menubar should act like it's inside a toolbox, I guess. // Maybe I should use the MenuClass in the case of a MenuStrip, although that would break // the other themes... - if (VisualStyleRenderer.IsElementDefined(VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0))) + if ( + VisualStyleRenderer.IsElementDefined( + VisualStyleElement.CreateElement(RebarClass, RebarBackground, 0) + ) + ) renderer.SetParameters(RebarClass, RebarBackground, 0); else renderer.SetParameters(RebarClass, 0, 0); @@ -369,7 +412,11 @@ namespace OpenNest } if (renderer.IsBackgroundPartiallyTransparent()) - renderer.DrawParentBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.ToolStrip); + renderer.DrawParentBackground( + e.Graphics, + e.ToolStrip.ClientRectangle, + e.ToolStrip + ); renderer.DrawBackground(e.Graphics, e.ToolStrip.ClientRectangle, e.AffectedBounds); } @@ -389,7 +436,15 @@ namespace OpenNest base.OnRenderSplitButtonBackground(e); // It doesn't matter what colour of arrow we tell it to draw. OnRenderArrow will compute it from the item anyway. - OnRenderArrow(new ToolStripArrowRenderEventArgs(e.Graphics, sb, sb.DropDownButtonBounds, Color.Red, ArrowDirection.Down)); + OnRenderArrow( + new ToolStripArrowRenderEventArgs( + e.Graphics, + sb, + sb.DropDownButtonBounds, + Color.Red, + ArrowDirection.Down + ) + ); } else { @@ -425,7 +480,14 @@ namespace OpenNest // do that anyway.) // Using the DisplayRectangle gets roughly the right size so that the separator is closer to the text. Padding margins = GetThemeMargins(e.Graphics, MarginTypes.Sizing); - int extraWidth = (e.ToolStrip.Width - e.ToolStrip.DisplayRectangle.Width - margins.Left - margins.Right - 1) - e.AffectedBounds.Width; + int extraWidth = + ( + e.ToolStrip.Width + - e.ToolStrip.DisplayRectangle.Width + - margins.Left + - margins.Right + - 1 + ) - e.AffectedBounds.Width; Rectangle rect = e.AffectedBounds; rect.Y += 2; rect.Height -= 4; @@ -437,7 +499,12 @@ namespace OpenNest } else { - rect = new Rectangle(rect.Width + extraWidth - sepWidth, rect.Y, sepWidth, rect.Height); + rect = new Rectangle( + rect.Width + extraWidth - sepWidth, + rect.Y, + sepWidth, + rect.Height + ); } renderer.DrawBackground(e.Graphics, rect); } @@ -453,16 +520,23 @@ namespace OpenNest if (e.ToolStrip.IsDropDown && EnsureRenderer()) { renderer.SetParameters(MenuClass, (int)MenuParts.PopupSeparator, 0); - Rectangle rect = new Rectangle(e.ToolStrip.DisplayRectangle.Left, 0, e.ToolStrip.DisplayRectangle.Width, e.Item.Height); + Rectangle rect = new Rectangle( + e.ToolStrip.DisplayRectangle.Left, + 0, + e.ToolStrip.DisplayRectangle.Width, + e.Item.Height + ); renderer.DrawBackground(e.Graphics, rect, rect); } else { - e.Graphics.DrawLine(Pens.LightGray, + e.Graphics.DrawLine( + Pens.LightGray, e.Item.ContentRectangle.X, e.Item.ContentRectangle.Y, e.Item.ContentRectangle.X, - e.Item.ContentRectangle.Y + e.Item.Height - 6); + e.Item.ContentRectangle.Y + e.Item.Height - 6 + ); } } @@ -475,9 +549,20 @@ namespace OpenNest // Now, mirror its position if the menu item is RTL. if (e.Item.RightToLeft == RightToLeft.Yes) - bgRect = new Rectangle(e.ToolStrip.ClientSize.Width - bgRect.X - bgRect.Width, bgRect.Y, bgRect.Width, bgRect.Height); + bgRect = new Rectangle( + e.ToolStrip.ClientSize.Width - bgRect.X - bgRect.Width, + bgRect.Y, + bgRect.Width, + bgRect.Height + ); - renderer.SetParameters(MenuClass, (int)MenuParts.PopupCheckBackground, e.Item.Enabled ? (int)MenuPopupCheckBackgroundStates.Normal : (int)MenuPopupCheckBackgroundStates.Disabled); + renderer.SetParameters( + MenuClass, + (int)MenuParts.PopupCheckBackground, + e.Item.Enabled + ? (int)MenuPopupCheckBackgroundStates.Normal + : (int)MenuPopupCheckBackgroundStates.Disabled + ); renderer.DrawBackground(e.Graphics, bgRect); Rectangle checkRect = e.ImageRectangle; @@ -485,7 +570,13 @@ namespace OpenNest checkRect.Y = bgRect.Y + bgRect.Height / 2 - checkRect.Height / 2; // I don't think ToolStrip even supports radio box items, so no need to render them. - renderer.SetParameters(MenuClass, (int)MenuParts.PopupCheck, e.Item.Enabled ? (int)MenuPopupCheckStates.CheckmarkNormal : (int)MenuPopupCheckStates.CheckmarkDisabled); + renderer.SetParameters( + MenuClass, + (int)MenuParts.PopupCheck, + e.Item.Enabled + ? (int)MenuPopupCheckStates.CheckmarkNormal + : (int)MenuPopupCheckStates.CheckmarkDisabled + ); renderer.DrawBackground(e.Graphics, checkRect); } @@ -519,7 +610,11 @@ namespace OpenNest else if (e.Item.Selected) state = VisualStyleElement.Rebar.Chevron.Hot.State; - renderer.SetParameters(rebarClass, VisualStyleElement.Rebar.Chevron.Normal.Part, state); + renderer.SetParameters( + rebarClass, + VisualStyleElement.Rebar.Chevron.Normal.Part, + state + ); renderer.DrawBackground(e.Graphics, new Rectangle(Point.Empty, e.Item.Size)); } else @@ -536,11 +631,13 @@ namespace OpenNest return false; // Needs a more robust check. It seems mono supports very different style sets. - return - VisualStyleRenderer.IsElementDefined( - VisualStyleElement.CreateElement("Menu", - (int)MenuParts.BarBackground, - (int)MenuBarStates.Active)); + return VisualStyleRenderer.IsElementDefined( + VisualStyleElement.CreateElement( + "Menu", + (int)MenuParts.BarBackground, + (int)MenuBarStates.Active + ) + ); } } } diff --git a/OpenNest/Win32.cs b/OpenNest/Win32.cs index 990e639..f1af9d4 100644 --- a/OpenNest/Win32.cs +++ b/OpenNest/Win32.cs @@ -11,7 +11,8 @@ namespace OpenNest string section, string key, string val, - string filePath); + string filePath + ); [DllImport("kernel32")] public static extern int GetPrivateProfileString( @@ -20,7 +21,8 @@ namespace OpenNest string def, StringBuilder retVal, int size, - string filePath); + string filePath + ); [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr hWnd, int nIndex); @@ -29,7 +31,15 @@ namespace OpenNest public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); [DllImport("user32.dll", ExactSpelling = true)] - public static extern int SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + public static extern int SetWindowPos( + IntPtr hWnd, + IntPtr hWndInsertAfter, + int X, + int Y, + int cx, + int cy, + uint uFlags + ); #region Constants diff --git a/tools/StreamGravographJob/Program.cs b/tools/StreamGravographJob/Program.cs index d0778ee..34c9a6b 100644 --- a/tools/StreamGravographJob/Program.cs +++ b/tools/StreamGravographJob/Program.cs @@ -10,8 +10,12 @@ using Microsoft.Win32.SafeHandles; if (args.Length < 2) { Console.Error.WriteLine("Usage:"); - Console.Error.WriteLine(" StreamGravographJob [chunk=256] [flow=rtscts|xonxoff|none]"); - Console.Error.WriteLine(" StreamGravographJob --gen # name: testA | testB | miniB | miniSquare"); + Console.Error.WriteLine( + " StreamGravographJob [chunk=256] [flow=rtscts|xonxoff|none]" + ); + Console.Error.WriteLine( + " StreamGravographJob --gen # name: testA | testB | miniB | miniSquare" + ); Console.Error.WriteLine(" StreamGravographJob --inspect-nest "); Console.Error.WriteLine(" StreamGravographJob --from-nest "); return 2; @@ -21,9 +25,17 @@ if (args.Length < 2) // report dimensions / bounding box / pen-up travel — no bytes written. if (args[0] == "--inspect-nest") { - if (args.Length < 2) { Console.Error.WriteLine("--inspect-nest requires "); return 2; } + if (args.Length < 2) + { + Console.Error.WriteLine("--inspect-nest requires "); + return 2; + } var nestPath = args[1]; - if (!File.Exists(nestPath)) { Console.Error.WriteLine($"Not found: {nestPath}"); return 3; } + if (!File.Exists(nestPath)) + { + Console.Error.WriteLine($"Not found: {nestPath}"); + return 3; + } using var fs = new FileStream(nestPath, FileMode.Open, FileAccess.Read); var reader = new OpenNest.IO.NestReader(fs); @@ -36,7 +48,9 @@ if (args[0] == "--inspect-nest") foreach (var plate in nest.Plates) { plateIdx++; - Console.WriteLine($" Plate {plateIdx}: size={plate.Size.Length} x {plate.Size.Width}, quadrant={plate.Quadrant}, parts={plate.Parts.Count}"); + Console.WriteLine( + $" Plate {plateIdx}: size={plate.Size.Length} x {plate.Size.Width}, quadrant={plate.Quadrant}, parts={plate.Parts.Count}" + ); } var polylines = new OpenNest.Posts.GravographIS.NestPolylineExtractor().Extract(nest); @@ -46,22 +60,30 @@ if (args[0] == "--inspect-nest") return 0; } - double minX = double.PositiveInfinity, minY = double.PositiveInfinity; - double maxX = double.NegativeInfinity, maxY = double.NegativeInfinity; + double minX = double.PositiveInfinity, + minY = double.PositiveInfinity; + double maxX = double.NegativeInfinity, + maxY = double.NegativeInfinity; int totalPts = 0; foreach (var p in polylines) { foreach (var v in p) { - if (v.X < minX) minX = v.X; - if (v.X > maxX) maxX = v.X; - if (v.Y < minY) minY = v.Y; - if (v.Y > maxY) maxY = v.Y; + if (v.X < minX) + minX = v.X; + if (v.X > maxX) + maxX = v.X; + if (v.Y < minY) + minY = v.Y; + if (v.Y > maxY) + maxY = v.Y; } totalPts += p.Count; } Console.WriteLine($"Polylines: {polylines.Count}, total points: {totalPts}"); - Console.WriteLine($"Bounding box (inches): X ∈ [{minX:F3}, {maxX:F3}] Y ∈ [{minY:F3}, {maxY:F3}]"); + Console.WriteLine( + $"Bounding box (inches): X ∈ [{minX:F3}, {maxX:F3}] Y ∈ [{minY:F3}, {maxY:F3}]" + ); Console.WriteLine($"Extents: {maxX - minX:F3}\" × {maxY - minY:F3}\""); // After running the pre-pass (stitch + reorder from origin) — what the writer will actually consume. @@ -75,13 +97,16 @@ if (args[0] == "--inspect-nest") { pi++; Console.WriteLine($"Polyline {pi}: {poly.Count} points"); - var cumX = 0.0; var cumY = 0.0; + var cumX = 0.0; + var cumY = 0.0; for (var i = 0; i < poly.Count; i++) { var v = poly[i]; if (i == 0) { - Console.WriteLine($" [{i}] ({v.X,7:F3}, {v.Y,7:F3}) first DR travel from upper-left origin=({v.X,+7:F3}, {v.Y,+7:F3})"); + Console.WriteLine( + $" [{i}] ({v.X, 7:F3}, {v.Y, 7:F3}) first DR travel from upper-left origin=({v.X, +7:F3}, {v.Y, +7:F3})" + ); } else { @@ -89,7 +114,9 @@ if (args[0] == "--inspect-nest") var dy = v.Y - poly[i - 1].Y; cumX += dx; cumY += dy; - Console.WriteLine($" [{i}] ({v.X,7:F3}, {v.Y,7:F3}) Δ=({dx,+7:F3}, {dy,+7:F3}) cum from origin=({cumX,+7:F3}, {cumY,+7:F3})"); + Console.WriteLine( + $" [{i}] ({v.X, 7:F3}, {v.Y, 7:F3}) Δ=({dx, +7:F3}, {dy, +7:F3}) cum from origin=({cumX, +7:F3}, {cumY, +7:F3})" + ); } } } @@ -99,10 +126,18 @@ if (args[0] == "--inspect-nest") // Convert a .nest file to a .prn job via the full post-processor pipeline. if (args[0] == "--from-nest") { - if (args.Length < 3) { Console.Error.WriteLine("--from-nest requires "); return 2; } + if (args.Length < 3) + { + Console.Error.WriteLine("--from-nest requires "); + return 2; + } var nestPath = args[1]; var outFile = args[2]; - if (!File.Exists(nestPath)) { Console.Error.WriteLine($"Not found: {nestPath}"); return 3; } + if (!File.Exists(nestPath)) + { + Console.Error.WriteLine($"Not found: {nestPath}"); + return 3; + } using var fs = new FileStream(nestPath, FileMode.Open, FileAccess.Read); var nest = new OpenNest.IO.NestReader(fs).Read(); @@ -117,47 +152,65 @@ if (args[0] == "--from-nest") // Generator mode: run the live writer to produce a captured-test file on disk. if (args[0] == "--gen") { - if (args.Length < 3) { Console.Error.WriteLine("--gen requires "); return 2; } + if (args.Length < 3) + { + Console.Error.WriteLine("--gen requires "); + return 2; + } var preset = args[1]; var outFile = args[2]; var polylines = preset.ToLowerInvariant() switch { - "testa" => new System.Collections.Generic.List> - { - new[] { new OpenNest.Geometry.Vector(1, 1), new OpenNest.Geometry.Vector(1, 3) }, - }, - "testb" => new System.Collections.Generic.List> - { - new[] { new OpenNest.Geometry.Vector(1, 1), new OpenNest.Geometry.Vector(1, 3) }, - new[] { new OpenNest.Geometry.Vector(4, 1), new OpenNest.Geometry.Vector(4, 3) }, - new[] { new OpenNest.Geometry.Vector(4, 5), new OpenNest.Geometry.Vector(4, 7) }, - new[] { new OpenNest.Geometry.Vector(1, 5), new OpenNest.Geometry.Vector(1, 7) }, - }, + "testa" => + new System.Collections.Generic.List> + { + new[] { new OpenNest.Geometry.Vector(1, 1), new OpenNest.Geometry.Vector(1, 3) }, + }, + "testb" => + new System.Collections.Generic.List> + { + new[] { new OpenNest.Geometry.Vector(1, 1), new OpenNest.Geometry.Vector(1, 3) }, + new[] { new OpenNest.Geometry.Vector(4, 1), new OpenNest.Geometry.Vector(4, 3) }, + new[] { new OpenNest.Geometry.Vector(4, 5), new OpenNest.Geometry.Vector(4, 7) }, + new[] { new OpenNest.Geometry.Vector(1, 5), new OpenNest.Geometry.Vector(1, 7) }, + }, // Same 4-polyline topology as testB (vertical lines + diagonal PU travels between them), // shrunk to a 0.5" × 1.5" footprint so it stays right near the operator-set work origin. - "minib" => new System.Collections.Generic.List> - { - new[] { new OpenNest.Geometry.Vector(0, 0), new OpenNest.Geometry.Vector(0, 0.5) }, - new[] { new OpenNest.Geometry.Vector(0.5, 0), new OpenNest.Geometry.Vector(0.5, 0.5) }, - new[] { new OpenNest.Geometry.Vector(0.5, 1), new OpenNest.Geometry.Vector(0.5, 1.5) }, - new[] { new OpenNest.Geometry.Vector(0, 1), new OpenNest.Geometry.Vector(0, 1.5) }, - }, + "minib" => + new System.Collections.Generic.List> + { + new[] { new OpenNest.Geometry.Vector(0, 0), new OpenNest.Geometry.Vector(0, 0.5) }, + new[] + { + new OpenNest.Geometry.Vector(0.5, 0), + new OpenNest.Geometry.Vector(0.5, 0.5), + }, + new[] + { + new OpenNest.Geometry.Vector(0.5, 1), + new OpenNest.Geometry.Vector(0.5, 1.5), + }, + new[] { new OpenNest.Geometry.Vector(0, 1), new OpenNest.Geometry.Vector(0, 1.5) }, + }, // Closed 0.5" square as a SINGLE polyline of 5 points → 4-segment PD packet. // Exercises multi-segment PD (one FF FD 50 44 00 00 followed by 4 records, // no intermediate lifts) and bi-directional motion (X+, Y+, X−, Y−). // Returns the head to its starting point so no manual jog needed after. - "minisquare" => new System.Collections.Generic.List> - { - new[] + "minisquare" => + new System.Collections.Generic.List> { - new OpenNest.Geometry.Vector(0, 0), - new OpenNest.Geometry.Vector(0.5, 0), - new OpenNest.Geometry.Vector(0.5, 0.5), - new OpenNest.Geometry.Vector(0, 0.5), - new OpenNest.Geometry.Vector(0, 0), + new[] + { + new OpenNest.Geometry.Vector(0, 0), + new OpenNest.Geometry.Vector(0.5, 0), + new OpenNest.Geometry.Vector(0.5, 0.5), + new OpenNest.Geometry.Vector(0, 0.5), + new OpenNest.Geometry.Vector(0, 0), + }, }, - }, - _ => throw new ArgumentException($"Unknown preset '{preset}' (try testA, testB, miniB, or miniSquare)."), + _ => throw new ArgumentException( + $"Unknown preset '{preset}' (try testA, testB, miniB, or miniSquare)." + ), }; using var outFs = new FileStream(outFile, FileMode.Create, FileAccess.Write); @@ -188,7 +241,9 @@ if (!File.Exists(file)) var bytes = File.ReadAllBytes(file); Console.WriteLine($"File: {file}"); Console.WriteLine($"Size: {bytes.Length} bytes"); -Console.WriteLine($"Header: {BitConverter.ToString(bytes, 0, Math.Min(7, bytes.Length)).Replace('-', ' ')}"); +Console.WriteLine( + $"Header: {BitConverter.ToString(bytes, 0, Math.Min(7, bytes.Length)).Replace('-', ' ')}" +); var ports = SerialPort.GetPortNames(); Array.Sort(ports); @@ -215,11 +270,21 @@ using var port = new SerialPort(portName, 9600, Parity.None, 8, StopBits.One) const uint OPEN_EXISTING = 3; const uint FILE_FLAG_OVERLAPPED = 0x40000000u; var devName = @"\\.\" + portName; - var handle = NativeMethods.CreateFileW(devName, GENERIC_RW, 0, IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, IntPtr.Zero); + var handle = NativeMethods.CreateFileW( + devName, + GENERIC_RW, + 0, + IntPtr.Zero, + OPEN_EXISTING, + FILE_FLAG_OVERLAPPED, + IntPtr.Zero + ); var err = Marshal.GetLastWin32Error(); if (handle.IsInvalid) { - Console.WriteLine($"CreateFile(\"{devName}\", overlapped, exclusive) FAILED: win32={err} ({new Win32Exception(err).Message})"); + Console.WriteLine( + $"CreateFile(\"{devName}\", overlapped, exclusive) FAILED: win32={err} ({new Win32Exception(err).Message})" + ); } else { @@ -240,7 +305,13 @@ try var n = Math.Min(chunk, bytes.Length - i); port.Write(bytes, i, n); } - try { port.BaseStream.Flush(); } catch { /* advisory */ } + try + { + port.BaseStream.Flush(); + } + catch + { /* advisory */ + } Thread.Sleep(500); } finally @@ -254,7 +325,12 @@ return 0; internal static class NativeMethods { - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateFileW")] + [DllImport( + "kernel32.dll", + SetLastError = true, + CharSet = CharSet.Unicode, + EntryPoint = "CreateFileW" + )] internal static extern SafeFileHandle CreateFileW( string lpFileName, uint dwDesiredAccess, @@ -262,5 +338,6 @@ internal static class NativeMethods IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, - IntPtr hTemplateFile); + IntPtr hTemplateFile + ); }