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:
@@ -0,0 +1,4 @@
|
||||
# CSharpier formats only C# sources; project/config XML keeps its layout.
|
||||
**/*.csproj
|
||||
**/*.config
|
||||
**/*.xml
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.0–1.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
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
namespace OpenNest.Api;
|
||||
|
||||
public enum NestStrategy { Auto }
|
||||
public enum NestStrategy
|
||||
{
|
||||
Auto,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum AlignType
|
||||
{
|
||||
@@ -10,6 +9,6 @@ namespace OpenNest
|
||||
Horizontally,
|
||||
Vertically,
|
||||
EvenlySpaceHorizontally,
|
||||
EvenlySpaceVertically
|
||||
EvenlySpaceVertically,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,6 +4,6 @@ namespace OpenNest.Bending
|
||||
{
|
||||
Unknown,
|
||||
Up,
|
||||
Down
|
||||
Down,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
|
||||
public const int UseMax = -2;
|
||||
|
||||
public Feedrate()
|
||||
{
|
||||
}
|
||||
public Feedrate() { }
|
||||
|
||||
public Feedrate(double value)
|
||||
{
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
|
||||
namespace OpenNest.CNC
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
public enum KerfType
|
||||
{
|
||||
None,
|
||||
Left,
|
||||
Right
|
||||
Right,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
namespace OpenNest.CNC
|
||||
namespace OpenNest.CNC
|
||||
{
|
||||
public enum Mode
|
||||
{
|
||||
Absolute,
|
||||
Incremental
|
||||
Incremental,
|
||||
}
|
||||
}
|
||||
|
||||
+186
-175
@@ -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];
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ namespace OpenNest.CNC
|
||||
private double rotation;
|
||||
private Program program;
|
||||
|
||||
public SubProgramCall()
|
||||
{
|
||||
}
|
||||
public SubProgramCall() { }
|
||||
|
||||
public SubProgramCall(Program program, double rotation)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> { }
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace OpenNest
|
||||
public enum CutDirection
|
||||
{
|
||||
TowardOrigin,
|
||||
AwayFromOrigin
|
||||
AwayFromOrigin,
|
||||
}
|
||||
|
||||
public class CutOffSettings
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 < -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
public interface IBoundable
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
{
|
||||
public class Material
|
||||
{
|
||||
public Material()
|
||||
{
|
||||
}
|
||||
public Material() { }
|
||||
|
||||
public Material(string name)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
|
||||
namespace OpenNest
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum OffsetSide
|
||||
{
|
||||
Left,
|
||||
Right
|
||||
Right,
|
||||
}
|
||||
}
|
||||
|
||||
+41
-17
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public enum PushDirection
|
||||
@@ -6,6 +5,6 @@ namespace OpenNest
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right
|
||||
Right,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user