feat(cutoffs): cut sheet-separating cut-offs after every part

This commit is contained in:
aj committed 2026-10-13 21:09:35 -04:00
1 parent b9a387b517
commit ddd1e422be
10 files changed
+273 -26

No files matched your search

@@ -83,6 +83,16 @@ internal sealed class CuttingDependencyGraph
{
if (!nodes[cut].IsCutOff)
continue;
if (nodes[cut].SeparatesSheet)
{
// Cutting a line that splits the sheet comes after every part and every other
// cutoff, so the head never travels back across the separated edge. Such a line
// is continuous, so it passes through no part and needs no crossing edge.
for (var other = 0; other < nodes.Count; other++)
if (!nodes[other].SeparatesSheet)
edges[cut].Add(other);
continue;
}
var definition = nodes[cut].CutOff;
for (var part = 0; part < nodes.Count; part++)
{
@@ -190,9 +200,10 @@ internal sealed class DependencyNode
private LeadMaterialSnapshot material;
internal DependencyNode(bool isCutOff, CutOffDefinition cutOff, Box placedBounds, Box cleanBounds,
Func<LeadMaterialSnapshot> materialFactory)
Func<LeadMaterialSnapshot> materialFactory, bool separatesSheet = false)
{
IsCutOff = isCutOff;
SeparatesSheet = isCutOff && separatesSheet;
CutOff = cutOff;
PlacedBounds = Copy(placedBounds);
CleanBounds = cleanBounds == null ? null : Copy(cleanBounds);
@@ -201,6 +212,8 @@ internal sealed class DependencyNode
}
internal bool IsCutOff { get; }
/// <summary>A cutoff whose placed program is one continuous cut across the whole sheet.</summary>
internal bool SeparatesSheet { get; }
internal CutOffDefinition CutOff { get; }
internal Box PlacedBounds { get; }
internal Box CleanBounds { get; }
@@ -147,12 +147,15 @@ public sealed class FixedProgramPlacement
}
return new(SourcePart, SourceOrdinal, Location, Rotation, LeadInsLocked, execution,
OwnedProgramCopy.Copy(proposed, token), Prepared, Material, choices)
{ SelectedProgram = selected, IsCutOff = IsCutOff };
{ SelectedProgram = selected, IsCutOff = IsCutOff, SeparatesSheet = SeparatesSheet };
}
/// <summary>A cutoff: always a fixed open-cut program, never material or regenerated.</summary>
public bool IsCutOff { get; internal init; }
/// <summary>A cutoff that splits the whole sheet; it is cut after every other placement.</summary>
public bool SeparatesSheet { get; internal init; }
public Part SourcePart { get; }
public int SourceOrdinal { get; }
public Vector Location { get; }
@@ -78,12 +78,15 @@ public static class CuttingPlanService
var cut = ExecutionMotionReader.ReadSupported(source.Program, source.Location, request.StartPoint, token);
if (!cut.HasCuttingContour)
throw new ArgumentException("Placed cutoff program has no nonzero cutting motions.");
nodes.Add(new(true, definitions.TryGetValue(source.BaseDrawing, out var definition)
var defined = definitions.TryGetValue(source.BaseDrawing, out var definition);
var separates = defined && CutOff.SeparatesSheet(source, definition,
request.Plate.BoundingBox(includeParts: false));
nodes.Add(new(true, defined
? new(definition.Axis, definition.Position, definition.StartLimit, definition.EndLimit) : null,
source.BoundingBox, null, null));
source.BoundingBox, null, null, separates));
placements.Add(new(source, index, source.Location, source.Rotation, source.LeadInsLocked,
cut, OwnedProgramCopy.Copy(source.Program, token))
{ IsCutOff = true });
{ IsCutOff = true, SeparatesSheet = separates });
continue;
}
var clean = ExecutionMotionReader.ReadSupported(source.BaseDrawing.Program, Vector.Zero, null, token);
@@ -382,6 +385,10 @@ public static class CuttingPlanService
{
var part = snapshot.Placements[violation.Part];
var prerequisite = snapshot.Placements[violation.Prerequisite];
if (part.SeparatesSheet)
return new(part.SourceOrdinal, part.SourcePart, prerequisite.SourceOrdinal, prerequisite.SourcePart, null,
$"{subject} cuts the sheet-separating cutoff (part {part.SourceOrdinal + 1}) before part "
+ $"{prerequisite.SourceOrdinal + 1}; every part and other cutoff must be cut before the sheet is separated.");
var reason = prerequisite.IsCutOff ? "the cutoff crossing it" : "the part nested in its cutout";
return new(part.SourceOrdinal, part.SourcePart, prerequisite.SourceOrdinal, prerequisite.SourcePart, null,
$"{subject} cuts part {part.SourceOrdinal + 1} before {reason} (part {prerequisite.SourceOrdinal + 1}).");
+18 -6
View File
@@ -34,16 +34,27 @@ namespace OpenNest.Engine.Sequencing
private static List<Part> OrderCutOffsFirst(List<Part> ordered, Plate plate)
{
var cuts = ordered.Select((part, index) => (Part: part, Index: index))
.Where(item => item.Part.BaseDrawing.IsCutOff).ToList();
if (cuts.Count == 0)
return ordered;
var definitions = new Dictionary<Drawing, CutOff>(ReferenceEqualityComparer.Instance);
foreach (var cutOff in plate.CutOffs)
definitions[cutOff.Drawing] = cutOff;
var bounds = plate.BoundingBox(includeParts: false);
// A cutoff that splits the whole sheet goes after every part and other cutoff, so the
// head never travels back across it. It passes through no part.
var separating = ordered.Where(part => part.BaseDrawing.IsCutOff
&& definitions.TryGetValue(part.BaseDrawing, out var definition)
&& CutOff.SeparatesSheet(part, definition, bounds)).ToList();
if (separating.Count > 0)
{
var last = new HashSet<Part>(separating, ReferenceEqualityComparer.Instance);
ordered = ordered.Where(part => !last.Contains(part)).ToList();
}
var cuts = ordered.Select((part, index) => (Part: part, Index: index))
.Where(item => item.Part.BaseDrawing.IsCutOff).ToList();
if (cuts.Count == 0)
return ordered.Concat(separating).ToList();
var emitted = new bool[ordered.Count];
var result = new List<Part>(ordered.Count);
for (var i = 0; i < ordered.Count; i++)
@@ -75,6 +86,7 @@ namespace OpenNest.Engine.Sequencing
}
}
result.AddRange(separating);
return result;
}
}