Merge branch 'chore/csharpier-sweep'

# Conflicts:
#	OpenNest.Core/Geometry/ArcFit.cs
#	OpenNest.Core/Geometry/GeometrySimplifier.cs
#	OpenNest.Posts.GravographIS/GravographISWriter.cs
#	OpenNest.Posts.GravographIS/NestPolylineExtractor.cs
This commit is contained in:
aj
2026-09-20 16:53:52 -04:00
473 changed files with 16171 additions and 7660 deletions
+4
View File
@@ -0,0 +1,4 @@
# CSharpier formats only C# sources; project/config XML keeps its layout.
**/*.csproj
**/*.config
**/*.xml
+3
View File
@@ -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
+3
View File
@@ -6,17 +6,20 @@ namespace OpenNest.Api;
public class NestRequest
{
public IReadOnlyList<NestRequestPart> Parts { get; init; } = [];
/// <summary>
/// Explicit available physical stock. Null keeps the legacy unlimited SheetSize fallback;
/// an empty list deliberately means no stock is available.
/// </summary>
public IReadOnlyList<NestRequestPlate> Plates { get; init; }
public Size SheetSize { get; init; } = new(60, 120);
/// <summary>Built-in whole-job placement strategy. Explicit values take precedence over legacy Strategy.</summary>
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;
/// <summary>Legacy compatibility setting; Auto maps to the Default whole-job strategy.</summary>
public NestStrategy Strategy { get; init; } = NestStrategy.Auto;
public CutParameters Cutting { get; init; } = CutParameters.Default;
+1
View File
@@ -7,6 +7,7 @@ public class NestRequestPlate
{
public string Id { get; init; }
public Size Size { get; init; }
/// <summary>Available physical sheets; null means unlimited.</summary>
public int? Quantity { get; init; }
public double PartSpacing { get; init; }
+41 -25
View File
@@ -25,10 +25,12 @@ public class NestResponse
/// <summary>Zero identifies an archive written before response metadata was versioned.</summary>
public int SchemaVersion { get; init; } = CurrentSchemaVersion;
public int SheetCount { get; init; }
/// <summary>Placed-part area divided by total materialized physical-sheet area, as a 0.01.0 ratio.</summary>
public double Utilization { get; init; }
public TimeSpan CutTime { get; init; }
public TimeSpan Elapsed { get; init; }
/// <summary>Null means an older archive did not record whole-job fulfillment status.</summary>
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<NestPartFulfillment>(Fulfillment),
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
PlateStockMappings = PlateStockMappings is null ? [] : new List<NestPlateStockMapping>(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<NestPartFulfillment>(Fulfillment),
StockUsage = StockUsage is null ? [] : new List<NestStockUsage>(StockUsage),
PlateStockMappings = PlateStockMappings is null
? []
: new List<NestPlateStockMapping>(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<NestRequest>(stream, JsonOptions)
request =
await JsonSerializer.DeserializeAsync<NestRequest>(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<NestResponseArchiveDto>(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<NestResponseArchiveDto>(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,
};
}
+104 -46
View File
@@ -16,10 +16,13 @@ public static class NestRunner
public static Task<NestResponse> RunAsync(
NestRequest request,
IProgress<NestProgress> 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<IdentifiedRequestPart> IdentifyParts(IReadOnlyList<NestRequestPart> requestParts)
private static IReadOnlyList<IdentifiedRequestPart> IdentifyParts(
IReadOnlyList<NestRequestPart> requestParts
)
{
var identified = new List<IdentifiedRequestPart>(requestParts.Count);
var ids = new HashSet<string>(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<NestProgress> progress) : IProgress<NestJobProgress>
private sealed class JobProgressBridge(IProgress<NestProgress> progress)
: IProgress<NestJobProgress>
{
public void Report(NestJobProgress value)
{
+4 -1
View File
@@ -1,3 +1,6 @@
namespace OpenNest.Api;
public enum NestStrategy { Auto }
public enum NestStrategy
{
Auto,
}
+21 -6
View File
@@ -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.
/// </summary>
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)
);
}
}
}
+73 -23
View File
@@ -23,8 +23,13 @@ namespace OpenNest.Benchmark
/// <summary>Wall-clock budget for one engine solving one job.</summary>
private static readonly TimeSpan SolveTimeout = TimeSpan.FromMinutes(5);
public static List<JobResult> Run(List<BenchmarkJob> jobs, IReadOnlyList<NestingEngineInfo> engines,
double salvageRate = 0, double minimumSalvageDimension = 0, string outputDirectory = null)
public static List<JobResult> Run(
List<BenchmarkJob> jobs,
IReadOnlyList<NestingEngineInfo> engines,
double salvageRate = 0,
double minimumSalvageDimension = 0,
string outputDirectory = null
)
{
var results = new List<JobResult>(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<DrawingRequest, Drawing, (string Name, int Quantity)>(
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();
+44 -29
View File
@@ -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
/// </summary>
public static class JobLoader
{
public static List<BenchmarkJob> Load(string inputPath, IReadOnlyList<Size> sheetSizeOverrides = null,
double? partSpacingOverride = null)
public static List<BenchmarkJob> Load(
string inputPath,
IReadOnlyList<Size> sheetSizeOverrides = null,
double? partSpacingOverride = null
)
{
var files = ResolveFiles(inputPath);
var jobs = new List<BenchmarkJob>();
@@ -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();
+54 -22
View File
@@ -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.
/// </summary>
public static ValidationResult Validate(List<(Plate Plate, List<Part> Parts)> plateRuns,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements)
public static ValidationResult Validate(
List<(Plate Plate, List<Part> Parts)> plateRuns,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> 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<Part> parts,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
private static void ValidateQuantities(
List<Part> parts,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements,
ValidationResult result
)
{
var placedCounts = parts
.GroupBy<Part, Drawing>(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<Part> parts, Plate plate,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
private static void ValidateBounds(
List<Part> parts,
Plate plate,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> 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.
/// </summary>
private static void ValidateAreaBudget(List<Part> parts, Plate plate, ValidationResult result)
private static void ValidateAreaBudget(
List<Part> 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<Part> parts, double spacing,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements, ValidationResult result)
private static void ValidateSpacing(
List<Part> parts,
double spacing,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> 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
/// <summary>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.</summary>
private static string DisplayName(Part part, IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements) =>
requirements.TryGetValue(part.BaseDrawing, out var requirement) ? requirement.Name : part.BaseDrawing.Name;
private static string DisplayName(
Part part,
IReadOnlyDictionary<Drawing, (string Name, int Quantity)> requirements
) =>
requirements.TryGetValue(part.BaseDrawing, out var requirement)
? requirement.Name
: part.BaseDrawing.Name;
/// <summary>
/// Extracts a part's perimeter as a world-space polygon, optionally inflated
@@ -168,7 +199,8 @@ namespace OpenNest.Benchmark
/// </summary>
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();
+97 -30
View File
@@ -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<Size>();
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 <file.nest | folder> [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 <value> Override part spacing for every job");
Console.Error.WriteLine(" --engines Name1,Name2,... Only benchmark these registered engines (default: all)");
Console.Error.WriteLine(" --csv <path> 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 <value> Both offcut dimensions must qualify; 0 disables credit");
Console.Error.WriteLine(" --output <directory> 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 <value> Override part spacing for every job"
);
Console.Error.WriteLine(
" --engines Name1,Name2,... Only benchmark these registered engines (default: all)"
);
Console.Error.WriteLine(
" --csv <path> 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 <value> Both offcut dimensions must qualify; 0 disables credit"
);
Console.Error.WriteLine(
" --output <directory> Save valid layouts as .nest plus detailed JSON reports"
);
Console.Error.WriteLine(" --help Show this message");
}
+38 -13
View File
@@ -28,19 +28,27 @@ namespace OpenNest.Benchmark
var ranked = jobGroup.OrderBy(r => r, Comparer<JobResult>.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<JobResult> 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());
+168 -67
View File
@@ -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 <input-files...> [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(" <nest.nest> Load nest and fill (existing behavior)");
Console.Error.WriteLine(" <part.dxf> --size WxL Import DXF, create plate, and fill");
Console.Error.WriteLine(" <nest.nest> <part.dxf> Load nest and add imported DXF drawings");
Console.Error.WriteLine(
" <nest.nest> <part.dxf> Load nest and add imported DXF drawings"
);
Console.Error.WriteLine();
Console.Error.WriteLine("Options:");
Console.Error.WriteLine(" --repair-bends-mm <n> 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 <name> Drawing name to fill with (default: first drawing)");
Console.Error.WriteLine(
" --repair-bends-mm <n> 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 <name> Drawing name to fill with (default: first drawing)"
);
Console.Error.WriteLine(" --plate <index> Plate index to fill (default: 0)");
Console.Error.WriteLine(" --quantity <n> Max parts to place (default: 0 = unlimited)");
Console.Error.WriteLine(
" --quantity <n> Max parts to place (default: 0 = unlimited)"
);
Console.Error.WriteLine(" --spacing <value> Override part spacing");
Console.Error.WriteLine(" --size <WxL> Override plate size (e.g. 60x120); required for DXF-only mode");
Console.Error.WriteLine(" --output <path> Output nest file path (default: <input>-result.nest)");
Console.Error.WriteLine(" --template <path> 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 <WxL> Override plate size (e.g. 60x120); required for DXF-only mode"
);
Console.Error.WriteLine(
" --output <path> Output nest file path (default: <input>-result.nest)"
);
Console.Error.WriteLine(
" --template <path> 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 <name> Run a post processor after nesting");
Console.Error.WriteLine(" --post-output <path> Output file for post processor (default: <input>.cnc)");
Console.Error.WriteLine(" --posts-dir <path> Directory containing post processor DLLs (default: Posts/)");
Console.Error.WriteLine(
" --post-output <path> Output file for post processor (default: <input>.cnc)"
);
Console.Error.WriteLine(
" --posts-dir <path> 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");
}
+35 -12
View File
@@ -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<Entity> 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<Part> 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<Entity> 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<Part> 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<Entity> 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<Entity> entities)
@@ -137,14 +155,19 @@ namespace OpenNest
return;
var list = new List<Part>(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;
+2 -3
View File
@@ -1,5 +1,4 @@

namespace OpenNest
namespace OpenNest
{
public enum AlignType
{
@@ -10,6 +9,6 @@ namespace OpenNest
Horizontally,
Vertically,
EvenlySpaceHorizontally,
EvenlySpaceVertically
EvenlySpaceVertically,
}
}
+16 -9
View File
@@ -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<Entity> entities, List<Bend> 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()
+1 -1
View File
@@ -4,6 +4,6 @@ namespace OpenNest.Bending
{
Unknown,
Up,
Down
Down,
}
}
+19 -12
View File
@@ -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<string, string>(VariableRefs) : null
VariableRefs =
VariableRefs != null ? new Dictionary<string, string>(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);
}
}
}
+2 -3
View File
@@ -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,
}
}
+1 -3
View File
@@ -2,9 +2,7 @@
{
public class Comment : ICode
{
public Comment()
{
}
public Comment() { }
public Comment(string value)
{
@@ -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<ContourEntry> ResolveLeadInPoints(List<Shape> cutouts, Vector startPoint)
private static List<ContourEntry> ResolveLeadInPoints(
List<Shape> cutouts,
Vector startPoint
)
{
var entries = new ContourEntry[cutouts.Count];
var currentPoint = startPoint;
@@ -235,7 +261,12 @@ namespace OpenNest.CNC.CuttingStrategy
return new List<ContourEntry>(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<Entity> 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<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display)
private List<ICode> ConvertShapeToMoves(
Shape shape,
Vector startPoint,
LayerType layer = LayerType.Display
)
{
var moves = new List<ICode>();
@@ -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;
}
}
@@ -4,6 +4,6 @@ namespace OpenNest.CNC.CuttingStrategy
{
External,
Internal,
ArcCircle
ArcCircle,
}
}
@@ -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();
@@ -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<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> 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<ICode>
{
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 };
}
}
@@ -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<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> 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<ICode>
{
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,
};
}
}
@@ -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<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW);
public abstract List<ICode> Generate(
Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
@@ -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<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> 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<ICode>
{
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,
};
}
}
@@ -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<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> Generate(
Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
return new List<ICode>
{
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) =>
@@ -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<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> 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<ICode>
{
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,
};
}
}
@@ -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<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> Generate(
Vector contourStartPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{
return new List<ICode>
{
new RapidMove(contourStartPoint)
};
return new List<ICode> { new RapidMove(contourStartPoint) };
}
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
@@ -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<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> 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<ICode>
{
new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout }
new ArcMove(endPoint, arcCenter, winding) { Layer = LayerType.Leadout },
};
}
}
@@ -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<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW);
public abstract List<ICode> Generate(
Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
}
}
@@ -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<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> 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<ICode>
{
new LinearMove(endPoint) { Layer = LayerType.Leadout }
};
return new List<ICode> { new LinearMove(endPoint) { Layer = LayerType.Leadout } };
}
}
}
@@ -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<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
public override List<ICode> Generate(
Vector contourEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{
return new List<ICode>();
}
@@ -9,7 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy
BottomSide = 4,
EdgeStart = 5,
LeftSide = 7,
RightSideAlt = 8
RightSideAlt = 8,
}
public class SequenceParameters
@@ -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<ICode> 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<ICode>();
@@ -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));
@@ -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<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW)
Vector tabStartPoint,
Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
)
{
return new List<ICode>
{
new RapidMove(tabEndPoint)
};
return new List<ICode> { new RapidMove(tabEndPoint) };
}
}
}
@@ -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<ICode> 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<ICode>();
@@ -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;
}
}
}
@@ -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<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW);
Vector tabStartPoint,
Vector tabEndPoint,
double contourNormalAngle,
RotationType winding = RotationType.CW
);
}
}
+1 -3
View File
@@ -6,9 +6,7 @@
public const int UseMax = -2;
public Feedrate()
{
}
public Feedrate() { }
public Feedrate(double value)
{
+2 -3
View File
@@ -1,10 +1,9 @@

namespace OpenNest.CNC
namespace OpenNest.CNC
{
public enum KerfType
{
None,
Left,
Right
Right,
}
}
+2 -3
View File
@@ -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,
}
}
+4 -7
View File
@@ -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<string, string>(VariableRefs) : null
VariableRefs =
VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null,
};
}
+2 -3
View File
@@ -1,9 +1,8 @@

namespace OpenNest.CNC
namespace OpenNest.CNC
{
public enum Mode
{
Absolute,
Incremental
Incremental,
}
}
+186 -175
View File
@@ -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<ICode> Codes;
public Dictionary<string, VariableDefinition> Variables { get; } = new(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, VariableDefinition> Variables { get; } =
new(StringComparer.OrdinalIgnoreCase);
public Dictionary<int, Program> 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];
+2 -2
View File
@@ -20,8 +20,8 @@ namespace OpenNest.CNC
public List<string> 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();
+12 -5
View File
@@ -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<Segment> results)
private static void Walk(
Program pgm,
Vector basePos,
ref Vector pos,
bool skipFirst,
List<Segment> 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)
{
+2 -1
View File
@@ -30,7 +30,8 @@ namespace OpenNest.CNC
return new RapidMove(EndPoint)
{
Suppressed = Suppressed,
VariableRefs = VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null
VariableRefs =
VariableRefs != null ? new Dictionary<string, string>(VariableRefs) : null,
};
}
+1 -3
View File
@@ -9,9 +9,7 @@ namespace OpenNest.CNC
private double rotation;
private Program program;
public SubProgramCall()
{
}
public SubProgramCall() { }
public SubProgramCall(Program program, double rotation)
{
+7 -2
View File
@@ -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;
+3 -2
View File
@@ -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);
@@ -2,7 +2,5 @@
namespace OpenNest.Collections
{
public class DrawingCollection : HashSet<Drawing>
{
}
public class DrawingCollection : HashSet<Drawing> { }
}
+17 -6
View File
@@ -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())
+2 -2
View File
@@ -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
{
+53 -15
View File
@@ -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<Entity> geometry)
private static void AddProgram(
Program program,
ref Mode mode,
ref Vector curpos,
ref List<Entity> 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<Entity> geometry)
private static void AddLinearMove(
LinearMove linearMove,
ref Mode mode,
ref Vector curpos,
ref List<Entity> 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<Entity> geometry)
private static void AddRapidMove(
RapidMove rapidMove,
ref Mode mode,
ref Vector curpos,
ref List<Entity> 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<Entity> geometry)
private static void AddArcMove(
ArcMove arcMove,
ref Mode mode,
ref Vector curpos,
ref List<Entity> 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;
}
+53 -14
View File
@@ -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<Part, Entity> cache = null)
public void Regenerate(
Plate plate,
CutOffSettings settings,
Dictionary<Part, Entity> 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<Part, Entity> cache)
private List<(double Start, double End)> ComputeSegments(
Plate plate,
CutOffSettings settings,
Dictionary<Part, Entity> 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();
+1 -1
View File
@@ -3,7 +3,7 @@ namespace OpenNest
public enum CutDirection
{
TowardOrigin,
AwayFromOrigin
AwayFromOrigin,
}
public class CutOffSettings
+8 -7
View File
@@ -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,
};
}
+22 -24
View File
@@ -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)
+38 -30
View File
@@ -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;
/// <summary>
/// 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); }
}
/// <summary>
@@ -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)
);
}
/// <summary>
@@ -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)
);
}
/// <summary>
@@ -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)
);
}
/// <summary>
@@ -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)
);
}
/// <summary>
@@ -242,21 +246,23 @@ namespace OpenNest.Geometry
public List<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
{
var points = new List<Vector>();
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
/// <returns></returns>
public override bool Intersects(Arc arc, out List<Vector> pts)
{
return Intersect.Intersects(this, arc, out pts); ;
return Intersect.Intersects(this, arc, out pts);
;
}
/// <summary>
+24 -12
View File
@@ -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);
+21 -13
View File
@@ -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)
+25 -29
View File
@@ -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<Vector> ToPoints(int segments = 1000, bool circumscribe = false)
{
var points = new List<Vector>();
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
);
}
/// <summary>
@@ -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)
);
}
/// <summary>
+65 -31
View File
@@ -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<Polygon> holesA = null, List<Polygon> holesB = null)
public static CollisionResult Check(
Polygon a,
Polygon b,
List<Polygon> holesA = null,
List<Polygon> 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<Polygon> holesA = null, List<Polygon> holesB = null)
public static bool HasOverlap(
Polygon a,
Polygon b,
List<Polygon> holesA = null,
List<Polygon> 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<CollisionResult> CheckAll(List<Polygon> polygons,
List<List<Polygon>> holes = null)
public static List<CollisionResult> CheckAll(
List<Polygon> polygons,
List<List<Polygon>> holes = null
)
{
var results = new List<CollisionResult>();
@@ -78,8 +88,7 @@ namespace OpenNest.Geometry
return results;
}
public static bool HasAnyOverlap(List<Polygon> polygons,
List<List<Polygon>> holes = null)
public static bool HasAnyOverlap(List<Polygon> polygons, List<List<Polygon>> 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<Vector>(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<Vector>(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);
}
/// <summary>
@@ -255,12 +268,17 @@ namespace OpenNest.Geometry
/// <summary>
/// Subtracts holes from overlap regions.
/// </summary>
private static List<Polygon> SubtractHoles(List<Polygon> regions,
List<Polygon> holesA, List<Polygon> holesB)
private static List<Polygon> SubtractHoles(
List<Polygon> regions,
List<Polygon> holesA,
List<Polygon> holesB
)
{
var allHoles = new List<Polygon>();
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 &lt; -Epsilon).
/// </summary>
private static List<Vector> ClipOutsideHalfSpace(Polygon piece, Vector edgeStart, Vector edgeEnd)
private static List<Vector> 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<Polygon> polygons, List<Vector> 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;
}
+10 -2
View File
@@ -5,9 +5,17 @@ namespace OpenNest.Geometry
{
public class CollisionResult
{
public static readonly CollisionResult None = new(false, new List<Polygon>(), new List<Vector>());
public static readonly CollisionResult None = new(
false,
new List<Polygon>(),
new List<Vector>()
);
public CollisionResult(bool overlaps, List<Polygon> overlapRegions, List<Vector> intersectionPoints)
public CollisionResult(
bool overlaps,
List<Polygon> overlapRegions,
List<Vector> intersectionPoints
)
{
Overlaps = overlaps;
OverlapRegions = overlapRegions;
+13 -4
View File
@@ -19,8 +19,11 @@ namespace OpenNest.Geometry
var verts = new List<Vector>(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).
/// </summary>
private static bool IsEar(Vector prev, Vector curr, Vector next,
List<Vector> verts, List<int> indices, int n)
private static bool IsEar(
Vector prev,
Vector curr,
Vector next,
List<Vector> verts,
List<int> indices,
int n
)
{
// Must be convex (CCW turn).
if (Cross(prev, curr, next) <= 0)
+8 -2
View File
@@ -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);
+140 -39
View File
@@ -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<Entity> Convert(Vector center, double semiMajor, double semiMinor,
double rotation, double startParam, double endParam, double tolerance = 0.001)
public static List<Entity> 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<Entity>();
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<Entity> ConvertCircle(Vector center, double radius,
double rotation, double startParam, double endParam)
private static List<Entity> 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<Entity>
{
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<double> { 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<Entity> results, int depth)
private static void FitSegment(
Vector center,
double semiMajor,
double semiMinor,
double rotation,
double t0,
double t1,
double tolerance,
List<Entity> 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<Vector> { 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;
+7 -3
View File
@@ -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<Entity> entities, double startAngle = 0, double endAngle = Angle.TwoPI)
public static BoundingRectangleResult FindBestRotation(
this List<Entity> entities,
double startAngle = 0,
double endAngle = Angle.TwoPI
)
{
// Check for Shape entity first (recursive case returns early)
foreach (var entity in entities)
+2 -3
View File
@@ -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,
}
}
+88 -47
View File
@@ -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<Arc> 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<Line> 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<Circle> 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>(T a, T b, out T joined);
private static void MergePass<T>(IList<T> items,
private static void MergePass<T>(
IList<T> items,
Func<IList<T>, T, int, List<T>> findCandidates,
TryJoin<T> tryJoin) where T : class
TryJoin<T> 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<Line> GetCollinearLines(this IList<Line> lines, Line line, int startIndex)
private static List<Line> GetCollinearLines(
this IList<Line> lines,
Line line,
int startIndex
)
{
var collinearLines = new List<Line>();
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<Arc>();
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;
}
+1 -2
View File
@@ -1,5 +1,4 @@

namespace OpenNest.Geometry
namespace OpenNest.Geometry
{
public interface IBoundable
{
+8 -4
View File
@@ -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.
+60 -31
View File
@@ -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<Vector> { i1, i2 } : new List<Vector> { i1 };
+1 -1
View File
@@ -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)
+12 -18
View File
@@ -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
/// <summary>
/// Updates the bounding box.
/// </summary>
public override sealed void UpdateBounds()
public sealed override void UpdateBounds()
{
if (StartPoint.X < EndPoint.X)
{
@@ -429,13 +423,13 @@ namespace OpenNest.Geometry
/// <returns>A tuple of (first, second) sub-lines.</returns>
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);
}
+17 -11
View File
@@ -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;
}
+22 -11
View File
@@ -4,12 +4,14 @@ namespace OpenNest.Geometry
{
public static class PolyLabel
{
public static Vector Find(Polygon outer, IList<Polygon> holes = null, double precision = 0.5)
public static Vector Find(
Polygon outer,
IList<Polygon> 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<Cell>();
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<Polygon> holes)
private static double PointToAllEdgesDist(
double x,
double y,
Polygon outer,
IList<Polygon> holes
)
{
var minDist = PointToPolygonDist(x, y, outer);
+64 -21
View File
@@ -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;
}
+35 -15
View File
@@ -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<Vector> vertices, int n, double angle)
private static BoundingRectangleResult EvaluateAtAngle(
IList<Vector> 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;
+95 -50
View File
@@ -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
/// </summary>
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;
}
}
+10 -3
View File
@@ -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<Shape> GetShapes(IEnumerable<Entity> entities, double? weldTolerance = null)
public static List<Shape> GetShapes(
IEnumerable<Entity> entities,
double? weldTolerance = null
)
{
var lines = new List<Line>();
var arcs = new List<Arc>();
@@ -141,7 +144,11 @@ namespace OpenNest.Geometry
private static void AddToGroup(
List<List<(Entity entity, bool isStart, Vector point)>> groups,
Entity entity, bool isStart, Vector point, double tolerance)
Entity entity,
bool isStart,
Vector point,
double tolerance
)
{
foreach (var group in groups)
{
+1 -2
View File
@@ -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();
}
+2 -1
View File
@@ -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)}";
}
}
+408 -157
View File
@@ -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.
/// </summary>
[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.
/// </summary>
[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.
/// </summary>
[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.
/// </summary>
[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.
/// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, PushDirection direction)
public static double DirectionalDistance(
List<Line> movingLines,
List<Line> 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.
/// </summary>
public static double DirectionalDistance(
List<Line> movingLines, double movingDx, double movingDy,
List<Line> stationaryLines, PushDirection direction)
List<Line> movingLines,
double movingDx,
double movingDy,
List<Line> 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.
/// </summary>
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.
/// </summary>
public static double DirectionalDistance(List<Line> movingLines, List<Line> stationaryLines, Vector direction)
public static double DirectionalDistance(
List<Line> movingLines,
List<Line> 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.
/// </summary>
public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, PushDirection direction)
List<Entity> movingEntities,
List<Entity> stationaryEntities,
PushDirection direction
)
{
return DirectionalDistance(movingEntities, stationaryEntities, DirectionToOffset(direction, 1.0));
return DirectionalDistance(
movingEntities,
stationaryEntities,
DirectionToOffset(direction, 1.0)
);
}
/// <summary>
@@ -517,7 +644,10 @@ namespace OpenNest.Geometry
/// without tessellation.
/// </summary>
public static double DirectionalDistance(
List<Entity> movingEntities, List<Entity> stationaryEntities, Vector direction)
List<Entity> movingEntities,
List<Entity> 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<Entity> arcEntities, List<Entity> lineEntities,
double dirX, double dirY, double minDist)
List<Entity> arcEntities,
List<Entity> 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<Vector> CollectVertices((Vector start, Vector end)[] edges, Vector offset)
private static HashSet<Vector> CollectVertices(
(Vector start, Vector end)[] edges,
Vector offset
)
{
var vertices = new HashSet<Vector>();
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<Box> boxes, out double top, out double btm)
private static bool FindVerticalLimits(
Vector pt,
Box bounds,
List<Box> 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<Box> boxes, out double lft, out double rgt)
private static bool FindHorizontalLimits(
Vector pt,
Box bounds,
List<Box> 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;
+32 -16
View File
@@ -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<Entity> Convert(List<Vector> points, bool isClosed, double tolerance = 0.001)
public static List<Entity> Convert(
List<Vector> points,
bool isClosed,
double tolerance = 0.001
)
{
if (points == null || points.Count < 2)
return new List<Entity>();
@@ -37,8 +41,12 @@ namespace OpenNest.Geometry
return entities;
}
private static ArcFitResult TryFitArc(List<Vector> points, int start,
Vector chainedTangent, double tolerance)
private static ArcFitResult TryFitArc(
List<Vector> 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<Vector> points)
List<Vector> 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<Vector> points, Vector tangent) =>
ArcFit.FitWithStartTangent(points, tangent);
List<Vector> points,
Vector tangent
) => ArcFit.FitWithStartTangent(points, tangent);
private static double MaxRadialDeviation(List<Vector> points, double cx, double cy, double radius) =>
ArcFit.MaxRadialDeviation(points, cx, cy, radius);
private static double MaxRadialDeviation(
List<Vector> points,
double cx,
double cy,
double radius
) => ArcFit.MaxRadialDeviation(points, cx, cy, radius);
private static double SumSignedAngles(Vector center, List<Vector> 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<Vector> 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);
}
+3 -3
View File
@@ -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.
+1 -3
View File
@@ -2,9 +2,7 @@
{
public class Material
{
public Material()
{
}
public Material() { }
public Material(string name)
{
+2 -4
View File
@@ -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;
}
/// <summary>
@@ -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;
}
}
}
+30 -9
View File
@@ -10,13 +10,18 @@ namespace OpenNest.Math
/// </summary>
public static class ExpressionEvaluator
{
public static double Evaluate(string expression, IReadOnlyDictionary<string, double> variables)
public static double Evaluate(
string expression,
IReadOnlyDictionary<string, double> 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;
+5 -3
View File
@@ -7,8 +7,9 @@ namespace OpenNest.Math
{
public static class Fraction
{
public static readonly Regex FractionRegex =
new Regex(@"((?<WholeNum>\d+)(\ |-))?(?<Fraction>\d+\/\d+)");
public static readonly Regex FractionRegex = new Regex(
@"((?<WholeNum>\d+)(\ |-))?(?<Fraction>\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<Match>()
.OrderByDescending(m => m.Index);
+5 -7
View File
@@ -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,
};
}
}
+2 -3
View File
@@ -1,9 +1,8 @@

namespace OpenNest
namespace OpenNest
{
public enum OffsetSide
{
Left,
Right
Right,
}
}
+41 -17
View File
@@ -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<Vector>();
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;
}
+134 -43
View File
@@ -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<Line> 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<Line>();
foreach (var shape in shapes)
@@ -23,10 +25,16 @@ namespace OpenNest
return lines;
}
public static List<Line> GetPartLines(Part part, PushDirection facingDirection, double chordTolerance = 0.001)
public static List<Line> 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<Line>();
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<Entity>();
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<Line> GetOffsetPartLines(Part part, double spacing, double chordTolerance = 0.001,
bool perimeterOnly = false)
public static List<Line> 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<Line>();
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<Line> GetOffsetPartLines(Part part, double spacing, PushDirection facingDirection, double chordTolerance = 0.001)
public static List<Line> 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<Line>();
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<Line> GetPartLines(Part part, Vector facingDirection, double chordTolerance = 0.001)
public static List<Line> 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<Line>();
foreach (var shape in shapes)
@@ -192,20 +240,36 @@ namespace OpenNest
return lines;
}
public static List<Line> GetOffsetPartLines(Part part, double spacing, Vector facingDirection, double chordTolerance = 0.001)
public static List<Line> 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<Line>();
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
/// <summary>
/// Returns only polygon edges whose outward normal faces the specified direction.
/// </summary>
private static List<Line> GetDirectionalLines(Polygon polygon, PushDirection facingDirection)
private static List<Line> 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<Line> lines, Shape offsetEntity,
double chordTolerance, Vector location)
private static void AddOffsetLines(
List<Line> 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<Line> lines, Shape offsetEntity,
double chordTolerance, Vector location, PushDirection facingDirection)
private static void AddOffsetDirectionalLines(
List<Line> 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<Line> lines, Shape offsetEntity,
double chordTolerance, Vector location, Vector facingDirection)
private static void AddOffsetDirectionalLines(
List<Line> lines,
Shape offsetEntity,
double chordTolerance,
Vector location,
Vector facingDirection
)
{
if (offsetEntity == null)
return;
+32 -33
View File
@@ -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
);
}
/// <summary>
@@ -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)
);
}
/// <summary>
@@ -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>Returns a number between 0.0 and 1.0</returns>
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<Vector> 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;
}
}
}
+11 -5
View File
@@ -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()
+1 -2
View File
@@ -1,4 +1,3 @@
namespace OpenNest
{
public enum PushDirection
@@ -6,6 +5,6 @@ namespace OpenNest
Up,
Down,
Left,
Right
Right,
}
}
+2 -3
View File
@@ -1,5 +1,4 @@

namespace OpenNest
namespace OpenNest
{
public enum RelativePosition
{
@@ -8,6 +7,6 @@ namespace OpenNest
Right,
Top,
Bottom,
None
None,
}
}

Some files were not shown because too many files have changed in this diff Show More