mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-01 08:01:34 -05:00
feat(clamps): give clamps their own clearance from parts
A clamp rail can now carry its own Clearance, the gap parts keep from every clamp footprint, independent of part spacing. Unset, it keeps the sheet's bottom edge spacing as before. Clamp blocks drawn under the sheet can use close to zero so the laser only stays off them. Plate.ClampClearance and NestPlateStock.ClampClearance resolve it once; the engines, interactive fill/pack/push, the layout check, Expander walls and automatic cut-off placement all read it. The interactive push now holds clamps at their square clearance keep-out (the layout check's shape) instead of the part spacing, so parts no longer tuck into its corners; the clamped fill characterization digests are re-pinned after checking the new layouts validate. Persisted as an optional `clearance` on the .nest clamp rail and the machine clamp settings; edited in Nest > Clamps and the machine's Clamps tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
afdf196754
commit
0719476618
32 files changed
+546
-152
No files matched your search
@@ -196,7 +196,7 @@ public static class AutomaticCutOffPlanner
|
|||||||
var y = ySign * horizontalDistance.Value;
|
var y = ySign * horizontalDistance.Value;
|
||||||
var limited = plate.CutOffs.Any(c => c?.Axis == CutOffAxis.Horizontal && Near(c.Position.Y, y));
|
var limited = plate.CutOffs.Any(c => c?.Axis == CutOffAxis.Horizontal && Near(c.Position.Y, y));
|
||||||
var clamp = clamps.FirstOrDefault(zone =>
|
var clamp = clamps.FirstOrDefault(zone =>
|
||||||
y >= zone.Bounds.Bottom - plate.EdgeSpacing.Bottom && y <= zone.Bounds.Top + plate.EdgeSpacing.Bottom);
|
y >= zone.Bounds.Bottom - plate.ClampClearance && y <= zone.Bounds.Top + plate.ClampClearance);
|
||||||
if (limited || clamp != null)
|
if (limited || clamp != null)
|
||||||
{
|
{
|
||||||
diagnostics.Add(new AutomaticCutOffDiagnostic(
|
diagnostics.Add(new AutomaticCutOffDiagnostic(
|
||||||
@@ -460,7 +460,7 @@ public static class AutomaticCutOffPlanner
|
|||||||
{
|
{
|
||||||
diagnostics.Add(new AutomaticCutOffDiagnostic(
|
diagnostics.Add(new AutomaticCutOffDiagnostic(
|
||||||
AutomaticCutOffDiagnosticCode.ClampBlockedCut,
|
AutomaticCutOffDiagnosticCode.ClampBlockedCut,
|
||||||
$"The proposed cut crosses clamp '{blocked.Id}' (including bottom-edge clearance); it was skipped.",
|
$"The proposed cut crosses clamp '{blocked.Id}' (including its clearance); it was skipped.",
|
||||||
X: x));
|
X: x));
|
||||||
if (isSeparator)
|
if (isSeparator)
|
||||||
WarnNoSeparator(x);
|
WarnNoSeparator(x);
|
||||||
@@ -522,7 +522,7 @@ public static class AutomaticCutOffPlanner
|
|||||||
// Share the exact placement-frame predicate with candidate exclusion and final
|
// Share the exact placement-frame predicate with candidate exclusion and final
|
||||||
// certification. Infeasible gaps still use nominal lines through AddCandidate.
|
// certification. Infeasible gaps still use nominal lines through AddCandidate.
|
||||||
bool CrossesClamp(ClampZone zone, double x) =>
|
bool CrossesClamp(ClampZone zone, double x) =>
|
||||||
x >= zone.Bounds.Left - plate.EdgeSpacing.Bottom && x <= zone.Bounds.Right + plate.EdgeSpacing.Bottom;
|
x >= zone.Bounds.Left - plate.ClampClearance && x <= zone.Bounds.Right + plate.ClampClearance;
|
||||||
|
|
||||||
void WarnNoSeparator(double x) => diagnostics.Add(new AutomaticCutOffDiagnostic(
|
void WarnNoSeparator(double x) => diagnostics.Add(new AutomaticCutOffDiagnostic(
|
||||||
AutomaticCutOffDiagnosticCode.NoSafeTailSeparator,
|
AutomaticCutOffDiagnosticCode.NoSafeTailSeparator,
|
||||||
@@ -548,8 +548,8 @@ public static class AutomaticCutOffPlanner
|
|||||||
Require(plate.Outline == null, "Automatic cut-offs need a rectangular sheet; this plate has an outline.", nameof(plate));
|
Require(plate.Outline == null, "Automatic cut-offs need a rectangular sheet; this plate has an outline.", nameof(plate));
|
||||||
Require(Nonnegative(plate.PartSpacing), "Part spacing must be finite and nonnegative.", nameof(plate));
|
Require(Nonnegative(plate.PartSpacing), "Part spacing must be finite and nonnegative.", nameof(plate));
|
||||||
Require(plate.ClampZones != null && (plate.ClampZones.Count == 0 ||
|
Require(plate.ClampZones != null && (plate.ClampZones.Count == 0 ||
|
||||||
Nonnegative(plate.EdgeSpacing.Bottom)),
|
Nonnegative(plate.ClampClearance)),
|
||||||
"Clamp zones and bottom-edge clearance must be finite and valid.", nameof(plate));
|
"Clamp zones and clamp clearance must be finite and valid.", nameof(plate));
|
||||||
Require(Nonnegative(settings.PartClearance) && Nonnegative(settings.MinSegmentLength) &&
|
Require(Nonnegative(settings.PartClearance) && Nonnegative(settings.MinSegmentLength) &&
|
||||||
Nonnegative(settings.Overtravel), "Cut-off settings must be finite and nonnegative.", nameof(settings));
|
Nonnegative(settings.Overtravel), "Cut-off settings must be finite and nonnegative.", nameof(settings));
|
||||||
Require(!settings.LockHeadHeight || double.IsFinite(settings.HeadLockOvercut) && settings.HeadLockOvercut > 0,
|
Require(!settings.LockHeadHeight || double.IsFinite(settings.HeadLockOvercut) && settings.HeadLockOvercut > 0,
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ namespace OpenNest
|
|||||||
double width,
|
double width,
|
||||||
double depth,
|
double depth,
|
||||||
IEnumerable<double> centers,
|
IEnumerable<double> centers,
|
||||||
double removalMinutes = DefaultRemovalMinutes
|
double removalMinutes = DefaultRemovalMinutes,
|
||||||
|
double? clearance = null
|
||||||
)
|
)
|
||||||
: this(
|
: this(
|
||||||
width,
|
width,
|
||||||
depth,
|
depth,
|
||||||
(centers ?? throw new ArgumentNullException(nameof(centers)))
|
(centers ?? throw new ArgumentNullException(nameof(centers)))
|
||||||
.Select(c => new RailClamp(ClampEdge.OriginSide, c)),
|
.Select(c => new RailClamp(ClampEdge.OriginSide, c)),
|
||||||
removalMinutes
|
removalMinutes,
|
||||||
|
clearance
|
||||||
)
|
)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
@@ -41,7 +43,8 @@ namespace OpenNest
|
|||||||
double width,
|
double width,
|
||||||
double depth,
|
double depth,
|
||||||
IEnumerable<RailClamp> clamps,
|
IEnumerable<RailClamp> clamps,
|
||||||
double removalMinutes = DefaultRemovalMinutes
|
double removalMinutes = DefaultRemovalMinutes,
|
||||||
|
double? clearance = null
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(clamps);
|
ArgumentNullException.ThrowIfNull(clamps);
|
||||||
@@ -60,6 +63,11 @@ namespace OpenNest
|
|||||||
nameof(removalMinutes),
|
nameof(removalMinutes),
|
||||||
"Clamp removal time must be finite and zero or more."
|
"Clamp removal time must be finite and zero or more."
|
||||||
);
|
);
|
||||||
|
if (clearance is double c && (!double.IsFinite(c) || c < 0))
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(clearance),
|
||||||
|
"Clamp clearance must be finite and zero or more."
|
||||||
|
);
|
||||||
|
|
||||||
var sorted = new SortedSet<RailClamp>();
|
var sorted = new SortedSet<RailClamp>();
|
||||||
foreach (var clamp in clamps)
|
foreach (var clamp in clamps)
|
||||||
@@ -77,6 +85,7 @@ namespace OpenNest
|
|||||||
Width = width;
|
Width = width;
|
||||||
Depth = depth;
|
Depth = depth;
|
||||||
RemovalMinutes = removalMinutes;
|
RemovalMinutes = removalMinutes;
|
||||||
|
Clearance = clearance;
|
||||||
Clamps = sorted.ToArray();
|
Clamps = sorted.ToArray();
|
||||||
Centers = Clamps.Where(c => c.Edge == ClampEdge.OriginSide).Select(c => c.Center).ToArray();
|
Centers = Clamps.Where(c => c.Edge == ClampEdge.OriginSide).Select(c => c.Center).ToArray();
|
||||||
}
|
}
|
||||||
@@ -93,6 +102,14 @@ namespace OpenNest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double RemovalMinutes { get; }
|
public double RemovalMinutes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The gap parts keep from every clamp footprint, independent of part spacing; null
|
||||||
|
/// keeps the sheet's bottom edge spacing (D6), the clearance before clamps had their
|
||||||
|
/// own. A clamp drawn as a support block under the sheet can use close to zero, so the
|
||||||
|
/// laser only stays off it. See <see cref="Plate.ClampClearance"/>.
|
||||||
|
/// </summary>
|
||||||
|
public double? Clearance { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Distinct clamps ordered by edge, then center. A clamp's position in this list
|
/// Distinct clamps ordered by edge, then center. A clamp's position in this list
|
||||||
/// gives its <c>clamp-N</c> id, so a rail with only origin-side clamps keeps the ids
|
/// gives its <c>clamp-N</c> id, so a rail with only origin-side clamps keeps the ids
|
||||||
@@ -103,9 +120,9 @@ namespace OpenNest
|
|||||||
/// <summary>Centers of the origin-side clamps, ascending; see <see cref="Clamps"/> for all.</summary>
|
/// <summary>Centers of the origin-side clamps, ascending; see <see cref="Clamps"/> for all.</summary>
|
||||||
public IReadOnlyList<double> Centers { get; }
|
public IReadOnlyList<double> Centers { get; }
|
||||||
|
|
||||||
/// <summary>A copy with the same size and removal time and these clamps.</summary>
|
/// <summary>A copy with the same size, removal time and clearance and these clamps.</summary>
|
||||||
public ClampRail WithClamps(IEnumerable<RailClamp> clamps) =>
|
public ClampRail WithClamps(IEnumerable<RailClamp> clamps) =>
|
||||||
new(Width, Depth, clamps, RemovalMinutes);
|
new(Width, Depth, clamps, RemovalMinutes, Clearance);
|
||||||
|
|
||||||
/// <summary>The <c>clamp-N</c> id of the clamp at <paramref name="index"/> in <see cref="Clamps"/>.</summary>
|
/// <summary>The <c>clamp-N</c> id of the clamp at <paramref name="index"/> in <see cref="Clamps"/>.</summary>
|
||||||
public static string IdOf(int index) => $"clamp-{index + 1}";
|
public static string IdOf(int index) => $"clamp-{index + 1}";
|
||||||
@@ -225,20 +242,22 @@ namespace OpenNest
|
|||||||
&& Width.Equals(other.Width)
|
&& Width.Equals(other.Width)
|
||||||
&& Depth.Equals(other.Depth)
|
&& Depth.Equals(other.Depth)
|
||||||
&& RemovalMinutes.Equals(other.RemovalMinutes)
|
&& RemovalMinutes.Equals(other.RemovalMinutes)
|
||||||
|
&& Nullable.Equals(Clearance, other.Clearance)
|
||||||
&& Clamps.SequenceEqual(other.Clamps);
|
&& Clamps.SequenceEqual(other.Clamps);
|
||||||
|
|
||||||
public override bool Equals(object obj) => Equals(obj as ClampRail);
|
public override bool Equals(object obj) => Equals(obj as ClampRail);
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
var hash = HashCode.Combine(Width, Depth, RemovalMinutes, Clamps.Count);
|
var hash = HashCode.Combine(Width, Depth, RemovalMinutes, Clearance, Clamps.Count);
|
||||||
foreach (var clamp in Clamps)
|
foreach (var clamp in Clamps)
|
||||||
hash = HashCode.Combine(hash, clamp);
|
hash = HashCode.Combine(hash, clamp);
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string ToString() =>
|
public override string ToString() =>
|
||||||
$"ClampRail {Width:R} x {Depth:R} at [{string.Join(", ", Clamps.Select(Describe))}]";
|
$"ClampRail {Width:R} x {Depth:R} at [{string.Join(", ", Clamps.Select(Describe))}]"
|
||||||
|
+ (Clearance is double clearance ? $" clearance {clearance:R}" : "");
|
||||||
|
|
||||||
private static string Describe(RailClamp clamp) =>
|
private static string Describe(RailClamp clamp) =>
|
||||||
clamp.Edge == ClampEdge.OriginSide
|
clamp.Edge == ClampEdge.OriginSide
|
||||||
|
|||||||
@@ -193,6 +193,14 @@ namespace OpenNest
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ClampRail ClampRail { get; private set; }
|
public ClampRail ClampRail { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The gap part material keeps from every clamp zone: the rail's own
|
||||||
|
/// <see cref="OpenNest.ClampRail.Clearance"/>, else the bottom edge spacing (D6), never
|
||||||
|
/// negative. Part spacing never applies to clamps.
|
||||||
|
/// </summary>
|
||||||
|
public double ClampClearance =>
|
||||||
|
System.Math.Max(0, ClampRail?.Clearance ?? EdgeSpacing.Bottom);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The machine support bed this sheet rests on (its nest's, see
|
/// The machine support bed this sheet rests on (its nest's, see
|
||||||
/// <see cref="Nest.SetSupportBed"/>), or null for none. Display, analysis and support
|
/// <see cref="Nest.SetSupportBed"/>), or null for none. Display, analysis and support
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public sealed class ClampRailEditor
|
|||||||
Width = initial.Width;
|
Width = initial.Width;
|
||||||
Depth = initial.Depth;
|
Depth = initial.Depth;
|
||||||
RemovalMinutes = initial.RemovalMinutes;
|
RemovalMinutes = initial.RemovalMinutes;
|
||||||
|
Clearance = initial.Clearance;
|
||||||
_clamps.AddRange(initial.Clamps.Order());
|
_clamps.AddRange(initial.Clamps.Order());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +36,9 @@ public sealed class ClampRailEditor
|
|||||||
/// length, so it is never converted between units.</summary>
|
/// length, so it is never converted between units.</summary>
|
||||||
public double RemovalMinutes { get; set; }
|
public double RemovalMinutes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>The gap parts keep from every clamp; null uses each sheet's bottom edge spacing.</summary>
|
||||||
|
public double? Clearance { get; set; }
|
||||||
|
|
||||||
/// <summary>Whether saved machine defaults give new nests these clamps.</summary>
|
/// <summary>Whether saved machine defaults give new nests these clamps.</summary>
|
||||||
public bool ApplyToNewNests { get; set; }
|
public bool ApplyToNewNests { get; set; }
|
||||||
|
|
||||||
@@ -91,6 +95,7 @@ public sealed class ClampRailEditor
|
|||||||
Width = converted.Width;
|
Width = converted.Width;
|
||||||
Depth = converted.Depth;
|
Depth = converted.Depth;
|
||||||
RemovalMinutes = converted.RemovalMinutes;
|
RemovalMinutes = converted.RemovalMinutes;
|
||||||
|
Clearance = converted.Clearance;
|
||||||
ApplyToNewNests = settings.Enabled;
|
ApplyToNewNests = settings.Enabled;
|
||||||
_clamps.Clear();
|
_clamps.Clear();
|
||||||
_clamps.AddRange(converted.Clamps.Distinct().Order());
|
_clamps.AddRange(converted.Clamps.Distinct().Order());
|
||||||
@@ -99,10 +104,10 @@ public sealed class ClampRailEditor
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The edited rail, or null when no clamp is listed. Throws
|
/// The edited rail, or null when no clamp is listed. Throws
|
||||||
/// <see cref="ArgumentOutOfRangeException"/> for a non-positive width or depth, or a
|
/// <see cref="ArgumentOutOfRangeException"/> for a non-positive width or depth, or a
|
||||||
/// negative/non-finite removal time.
|
/// negative/non-finite removal time or clearance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ClampRail? BuildRail() =>
|
public ClampRail? BuildRail() =>
|
||||||
_clamps.Count == 0 ? null : new ClampRail(Width, Depth, _clamps, RemovalMinutes);
|
_clamps.Count == 0 ? null : new ClampRail(Width, Depth, _clamps, RemovalMinutes, Clearance);
|
||||||
|
|
||||||
/// <summary>The dialog's OK: makes the edited rail the nest's, re-resolving every plate.</summary>
|
/// <summary>The dialog's OK: makes the edited rail the nest's, re-resolving every plate.</summary>
|
||||||
public void ApplyTo(Nest nest)
|
public void ApplyTo(Nest nest)
|
||||||
@@ -128,6 +133,11 @@ public sealed class ClampRailEditor
|
|||||||
nameof(RemovalMinutes),
|
nameof(RemovalMinutes),
|
||||||
"Clamp removal time must be finite and zero or more."
|
"Clamp removal time must be finite and zero or more."
|
||||||
);
|
);
|
||||||
|
if (Clearance is double clearance && (!double.IsFinite(clearance) || clearance < 0))
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(Clearance),
|
||||||
|
"Clamp clearance must be finite and zero or more."
|
||||||
|
);
|
||||||
var settings = new ClampSettings
|
var settings = new ClampSettings
|
||||||
{
|
{
|
||||||
Enabled = ApplyToNewNests && _clamps.Count > 0,
|
Enabled = ApplyToNewNests && _clamps.Count > 0,
|
||||||
@@ -135,6 +145,7 @@ public sealed class ClampRailEditor
|
|||||||
Width = Width,
|
Width = Width,
|
||||||
Depth = Depth,
|
Depth = Depth,
|
||||||
RemovalMinutes = RemovalMinutes,
|
RemovalMinutes = RemovalMinutes,
|
||||||
|
Clearance = Clearance,
|
||||||
};
|
};
|
||||||
settings.SetClamps(_clamps);
|
settings.SetClamps(_clamps);
|
||||||
return settings;
|
return settings;
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ public sealed class ClampSettings
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double RemovalMinutes { get; set; } = ClampRail.DefaultRemovalMinutes;
|
public double RemovalMinutes { get; set; } = ClampRail.DefaultRemovalMinutes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The gap parts keep from every clamp, in <see cref="Units"/>; null uses each sheet's
|
||||||
|
/// bottom edge spacing (<see cref="ClampRail.Clearance"/>).
|
||||||
|
/// </summary>
|
||||||
|
public double? Clearance { get; set; }
|
||||||
|
|
||||||
/// <summary>Origin-side clamp centers along X, measured from the plate origin.</summary>
|
/// <summary>Origin-side clamp centers along X, measured from the plate origin.</summary>
|
||||||
public IReadOnlyList<double> Centers { get; set; } = Array.Empty<double>();
|
public IReadOnlyList<double> Centers { get; set; } = Array.Empty<double>();
|
||||||
|
|
||||||
@@ -133,6 +139,7 @@ public sealed class ClampSettings
|
|||||||
Width = rail?.Width ?? ConvertDefault(DefaultWidth, units),
|
Width = rail?.Width ?? ConvertDefault(DefaultWidth, units),
|
||||||
Depth = rail?.Depth ?? ConvertDefault(DefaultDepth, units),
|
Depth = rail?.Depth ?? ConvertDefault(DefaultDepth, units),
|
||||||
RemovalMinutes = rail?.RemovalMinutes ?? ClampRail.DefaultRemovalMinutes,
|
RemovalMinutes = rail?.RemovalMinutes ?? ClampRail.DefaultRemovalMinutes,
|
||||||
|
Clearance = rail?.Clearance,
|
||||||
};
|
};
|
||||||
if (rail != null)
|
if (rail != null)
|
||||||
settings.SetClamps(rail.Clamps);
|
settings.SetClamps(rail.Clamps);
|
||||||
@@ -150,11 +157,12 @@ public sealed class ClampSettings
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
var converted = ConvertedTo(units);
|
var converted = ConvertedTo(units);
|
||||||
return new ClampRail(converted.Width, converted.Depth, converted.Clamps, converted.RemovalMinutes);
|
return new ClampRail(converted.Width, converted.Depth, converted.Clamps, converted.RemovalMinutes,
|
||||||
|
converted.Clearance);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A copy with width, depth and centers expressed in <paramref name="units"/>
|
/// A copy with width, depth, clearance and centers expressed in <paramref name="units"/>
|
||||||
/// (inches and millimeters convert at 25.4); <see cref="Enabled"/> and
|
/// (inches and millimeters convert at 25.4); <see cref="Enabled"/> and
|
||||||
/// <see cref="RemovalMinutes"/> (a time, not a length) are kept unscaled.
|
/// <see cref="RemovalMinutes"/> (a time, not a length) are kept unscaled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -168,6 +176,7 @@ public sealed class ClampSettings
|
|||||||
Width = Width * scale,
|
Width = Width * scale,
|
||||||
Depth = Depth * scale,
|
Depth = Depth * scale,
|
||||||
RemovalMinutes = RemovalMinutes,
|
RemovalMinutes = RemovalMinutes,
|
||||||
|
Clearance = Clearance * scale,
|
||||||
Centers = Centers.Select(c => c * scale).ToArray(),
|
Centers = Centers.Select(c => c * scale).ToArray(),
|
||||||
FarSideCenters = FarSideCenters.Select(c => c * scale).ToArray(),
|
FarSideCenters = FarSideCenters.Select(c => c * scale).ToArray(),
|
||||||
OriginEndCenters = OriginEndCenters.Select(c => c * scale).ToArray(),
|
OriginEndCenters = OriginEndCenters.Select(c => c * scale).ToArray(),
|
||||||
@@ -290,6 +299,15 @@ public sealed class ClampSettings
|
|||||||
ignored = true;
|
ignored = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Missing or null keeps the bottom edge spacing; anything else must be a length.
|
||||||
|
if (values.TryGetValue("clearance", out var clearance) && clearance.ValueKind != JsonValueKind.Null)
|
||||||
|
{
|
||||||
|
if (TryLength(clearance, out var value) && value >= 0)
|
||||||
|
settings.Clearance = value;
|
||||||
|
else
|
||||||
|
ignored = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (values.TryGetValue("centers", out var centers))
|
if (values.TryGetValue("centers", out var centers))
|
||||||
settings.Centers = ReadCenters(centers, ref ignored);
|
settings.Centers = ReadCenters(centers, ref ignored);
|
||||||
if (values.TryGetValue("farSideCenters", out var farSide))
|
if (values.TryGetValue("farSideCenters", out var farSide))
|
||||||
@@ -362,6 +380,8 @@ public sealed class ClampSettings
|
|||||||
["removalMinutes"] = RemovalMinutes,
|
["removalMinutes"] = RemovalMinutes,
|
||||||
["centers"] = Centers,
|
["centers"] = Centers,
|
||||||
};
|
};
|
||||||
|
if (Clearance is double clearance)
|
||||||
|
dto["clearance"] = clearance;
|
||||||
// Other edges are written only when used, so origin-side-only files stay unchanged.
|
// Other edges are written only when used, so origin-side-only files stay unchanged.
|
||||||
if (FarSideCenters.Count > 0)
|
if (FarSideCenters.Count > 0)
|
||||||
dto["farSideCenters"] = FarSideCenters;
|
dto["farSideCenters"] = FarSideCenters;
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public sealed class MachineClamps
|
|||||||
/// <summary>Minutes to remove one clamp and put it back after cutting; never scaled.</summary>
|
/// <summary>Minutes to remove one clamp and put it back after cutting; never scaled.</summary>
|
||||||
public double RemovalMinutes { get; set; } = ClampRail.DefaultRemovalMinutes;
|
public double RemovalMinutes { get; set; } = ClampRail.DefaultRemovalMinutes;
|
||||||
|
|
||||||
|
/// <summary>The gap parts keep from every clamp; null uses each sheet's bottom edge spacing.</summary>
|
||||||
|
public double? Clearance { get; set; }
|
||||||
|
|
||||||
/// <summary>Origin-side clamp centers along X, measured from the plate origin.</summary>
|
/// <summary>Origin-side clamp centers along X, measured from the plate origin.</summary>
|
||||||
public List<double> Centers { get; set; } = new();
|
public List<double> Centers { get; set; } = new();
|
||||||
|
|
||||||
@@ -49,6 +52,7 @@ public sealed class MachineClamps
|
|||||||
Width = converted.Width,
|
Width = converted.Width,
|
||||||
Depth = converted.Depth,
|
Depth = converted.Depth,
|
||||||
RemovalMinutes = converted.RemovalMinutes,
|
RemovalMinutes = converted.RemovalMinutes,
|
||||||
|
Clearance = converted.Clearance,
|
||||||
Centers = converted.Centers.ToList(),
|
Centers = converted.Centers.ToList(),
|
||||||
FarSideCenters = converted.FarSideCenters.ToList(),
|
FarSideCenters = converted.FarSideCenters.ToList(),
|
||||||
OriginEndCenters = converted.OriginEndCenters.ToList(),
|
OriginEndCenters = converted.OriginEndCenters.ToList(),
|
||||||
@@ -58,7 +62,8 @@ public sealed class MachineClamps
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rail as clamp settings in the machine's <paramref name="units"/>. An invalid size or
|
/// The rail as clamp settings in the machine's <paramref name="units"/>. An invalid size or
|
||||||
/// removal time falls back to the default, and an invalid center is dropped, as when
|
/// removal time falls back to the default, an invalid clearance to the bottom edge spacing,
|
||||||
|
/// and an invalid center is dropped, as when
|
||||||
/// <see cref="ClampSettings.Parse"/> reads a file.
|
/// <see cref="ClampSettings.Parse"/> reads a file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ClampSettings ToSettings(UnitSystem units)
|
public ClampSettings ToSettings(UnitSystem units)
|
||||||
@@ -70,6 +75,8 @@ public sealed class MachineClamps
|
|||||||
defaults.Depth = Depth;
|
defaults.Depth = Depth;
|
||||||
if (IsLength(RemovalMinutes) && RemovalMinutes >= 0)
|
if (IsLength(RemovalMinutes) && RemovalMinutes >= 0)
|
||||||
defaults.RemovalMinutes = RemovalMinutes;
|
defaults.RemovalMinutes = RemovalMinutes;
|
||||||
|
if (Clearance is double clearance && IsLength(clearance) && clearance >= 0)
|
||||||
|
defaults.Clearance = clearance;
|
||||||
defaults.Centers = Valid(Centers);
|
defaults.Centers = Valid(Centers);
|
||||||
defaults.FarSideCenters = Valid(FarSideCenters);
|
defaults.FarSideCenters = Valid(FarSideCenters);
|
||||||
defaults.OriginEndCenters = Valid(OriginEndCenters);
|
defaults.OriginEndCenters = Valid(OriginEndCenters);
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Engine.Fill;
|
||||||
|
using OpenNest.Engine.Jobs;
|
||||||
|
using OpenNest.Engine.Jobs.Adapters;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace OpenNest.Engine.Tests.Jobs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A clamp rail's own clearance replaces the bottom edge spacing as the gap parts keep from
|
||||||
|
/// clamps, in the stock engines read, the layout check and the interactive push; part spacing
|
||||||
|
/// never applies to clamps.
|
||||||
|
/// </summary>
|
||||||
|
public class ClampClearanceTests
|
||||||
|
{
|
||||||
|
// 24 x 24 sheet, 0.5 edge spacing, 1.0 part spacing; one clamp at X 6 on the front edge.
|
||||||
|
private const double Edge = 0.5;
|
||||||
|
private const double ClampTop = 0.625;
|
||||||
|
|
||||||
|
private static Plate RailPlate(double? clearance)
|
||||||
|
{
|
||||||
|
var plate = new Plate(new Size(24, 24))
|
||||||
|
{
|
||||||
|
EdgeSpacing = new Spacing(Edge, Edge),
|
||||||
|
PartSpacing = 1.0,
|
||||||
|
};
|
||||||
|
plate.SetClampRail(new ClampRail(1.5, ClampTop, new[] { 6.0 }, clearance: clearance));
|
||||||
|
return plate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Drawing Square(double size)
|
||||||
|
{
|
||||||
|
var program = new Program();
|
||||||
|
program.MoveTo(0, 0);
|
||||||
|
program.LineTo(size, 0);
|
||||||
|
program.LineTo(size, size);
|
||||||
|
program.LineTo(0, size);
|
||||||
|
program.LineTo(0, 0);
|
||||||
|
return new Drawing("square", program);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> Violations(Plate plate, Part part) =>
|
||||||
|
NestLayoutCheck.Validate(new() { (plate, new List<Part> { part }) },
|
||||||
|
new Dictionary<Drawing, (string, int)> { [part.BaseDrawing] = ("square", 1) });
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithoutItsOwnClearanceTheRailKeepsTheBottomEdgeSpacing()
|
||||||
|
{
|
||||||
|
var plate = RailPlate(clearance: null);
|
||||||
|
|
||||||
|
Assert.Equal(Edge, plate.ClampClearance);
|
||||||
|
Assert.Equal(Edge, DrawingJobMapper.FromPlate("s", plate, 1).ClampClearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StockCarriesTheRailsOwnClearance()
|
||||||
|
{
|
||||||
|
var plate = RailPlate(clearance: 0.05);
|
||||||
|
|
||||||
|
Assert.Equal(0.05, plate.ClampClearance);
|
||||||
|
Assert.Equal(0.05, DrawingJobMapper.FromPlate("s", plate, 1).ClampClearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LayoutCheckEnforcesTheClampClearanceNotTheEdgeSpacing()
|
||||||
|
{
|
||||||
|
// A part 0.1 above the clamp, straddling it in X.
|
||||||
|
var part = new Part(Square(3), new Vector(5, ClampTop + 0.1));
|
||||||
|
|
||||||
|
Assert.Contains(Violations(RailPlate(clearance: null), part), v => v.Contains("clamp zone"));
|
||||||
|
Assert.DoesNotContain(Violations(RailPlate(clearance: 0.05), part), v => v.Contains("clamp zone"));
|
||||||
|
Assert.Contains(Violations(RailPlate(clearance: 0.2), part), v => v.Contains("clamp zone"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PushStopsAtTheClampClearanceRatherThanThePartSpacing()
|
||||||
|
{
|
||||||
|
var plate = RailPlate(clearance: 0);
|
||||||
|
var mover = new Part(Square(3), new Vector(5, 8));
|
||||||
|
plate.Parts.Add(mover);
|
||||||
|
|
||||||
|
Compactor.Push(new List<Part> { mover }, plate, PushDirection.Down);
|
||||||
|
|
||||||
|
// Part spacing (1.0) and edge spacing (0.5) would both stop it higher.
|
||||||
|
Assert.InRange(mover.BoundingBox.Bottom, ClampTop - 1e-6, ClampTop + 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PushStillKeepsTheClampClearanceWhenItExceedsThePartSpacing()
|
||||||
|
{
|
||||||
|
var plate = RailPlate(clearance: 2);
|
||||||
|
var mover = new Part(Square(3), new Vector(5, 8));
|
||||||
|
plate.Parts.Add(mover);
|
||||||
|
|
||||||
|
Compactor.Push(new List<Part> { mover }, plate, PushDirection.Down);
|
||||||
|
|
||||||
|
Assert.InRange(mover.BoundingBox.Bottom, ClampTop + 2 - 0.01, ClampTop + 2 + 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -338,9 +338,9 @@ public class FillStackCharacterizationTests
|
|||||||
// --- interactive fill beside clamps ---
|
// --- interactive fill beside clamps ---
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("Fill", 40, "9B3EFF9B72243576")]
|
[InlineData("Fill", 40, "9BF5FC7FC47364A9")]
|
||||||
[InlineData("Strip", 40, "9B3EFF9B72243576")]
|
[InlineData("Strip", 40, "9BF5FC7FC47364A9")]
|
||||||
[InlineData("Vertical Remnant", 40, "039C0D607166D805")]
|
[InlineData("Vertical Remnant", 40, "9BF5FC7FC47364A9")]
|
||||||
[InlineData("Horizontal Remnant", 40, "C1E81564A620302A")]
|
[InlineData("Horizontal Remnant", 40, "C1E81564A620302A")]
|
||||||
public void FillItem_WithClampZones_PinnedLayout(
|
public void FillItem_WithClampZones_PinnedLayout(
|
||||||
string strategy,
|
string strategy,
|
||||||
|
|||||||
@@ -13,39 +13,32 @@ namespace OpenNest.Engine.Fill
|
|||||||
public static class Compactor
|
public static class Compactor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The plate's clamp zones as static push obstacles (C1). Unlike ordinary
|
/// The plate's clamp keep-outs (zones grown square by the clamp clearance) as static push
|
||||||
/// already-overlapping parts, a clamp stays a wall even when a selected member
|
/// obstacles (C1), held at zero spacing rather than the part spacing. Unlike ordinary already-overlapping parts,
|
||||||
/// straddles it: dropping it would let other clear members move into the zone.
|
/// a clamp stays a wall even when a selected member straddles it: dropping it would
|
||||||
/// Empty zone lists change every caller's arithmetic by nothing (C2).
|
/// let other clear members move into the zone. Empty zone lists change every
|
||||||
|
/// caller's arithmetic by nothing (C2).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static List<Part> ZoneObstacles(Plate plate) =>
|
private static ObstacleSet ZoneObstacles(Plate plate) =>
|
||||||
Jobs.InteractiveClamps.ObstacleParts(plate);
|
new(Jobs.InteractiveClamps.ObstacleParts(plate), 0);
|
||||||
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
|
||||||
{
|
|
||||||
var obstacleParts = ZoneObstacles(plate);
|
|
||||||
obstacleParts.InsertRange(
|
|
||||||
0,
|
|
||||||
plate.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
|
||||||
.ToList()
|
|
||||||
);
|
|
||||||
|
|
||||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
public static double Push(List<Part> movingParts, Plate plate, PushDirection direction) =>
|
||||||
}
|
Push(movingParts, plate, SpatialQuery.DirectionToOffset(direction, 1.0));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
/// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static double Push(List<Part> movingParts, Plate plate, double angle)
|
public static double Push(List<Part> movingParts, Plate plate, double angle) =>
|
||||||
{
|
Push(movingParts, plate, new Vector(System.Math.Cos(angle), System.Math.Sin(angle)));
|
||||||
var obstacleParts = ZoneObstacles(plate);
|
|
||||||
obstacleParts.InsertRange(
|
|
||||||
0,
|
|
||||||
plate.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
|
||||||
.ToList()
|
|
||||||
);
|
|
||||||
|
|
||||||
var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle));
|
private static double Push(List<Part> movingParts, Plate plate, Vector direction)
|
||||||
return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction);
|
{
|
||||||
|
var obstacleParts = plate.Parts
|
||||||
|
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return Push(movingParts, plate.WorkArea(), direction,
|
||||||
|
new ObstacleSet(obstacleParts, plate.PartSpacing), ZoneObstacles(plate));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -57,21 +50,20 @@ namespace OpenNest.Engine.Fill
|
|||||||
Box workArea,
|
Box workArea,
|
||||||
double partSpacing,
|
double partSpacing,
|
||||||
Vector direction
|
Vector direction
|
||||||
|
) => Push(movingParts, workArea, direction, new ObstacleSet(obstacleParts, partSpacing));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pushes movingParts to the first contact with the work-area edge or an obstacle,
|
||||||
|
/// keeping each obstacle set's own spacing.
|
||||||
|
/// </summary>
|
||||||
|
private static double Push(
|
||||||
|
List<Part> movingParts,
|
||||||
|
Box workArea,
|
||||||
|
Vector direction,
|
||||||
|
params ObstacleSet[] obstacleSets
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var opposite = -direction;
|
var opposite = -direction;
|
||||||
|
|
||||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
|
||||||
var obstacleSpacingBoxes = new Box[obstacleParts.Count];
|
|
||||||
var obstacleEntities = new List<Entity>[obstacleParts.Count];
|
|
||||||
var halfSpacing = System.Math.Max(0, partSpacing) / 2;
|
|
||||||
|
|
||||||
for (var i = 0; i < obstacleParts.Count; i++)
|
|
||||||
{
|
|
||||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
|
||||||
obstacleSpacingBoxes[i] = SpacingBounds(obstacleBoxes[i], halfSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
var distance = double.MaxValue;
|
var distance = double.MaxValue;
|
||||||
|
|
||||||
foreach (var moving in movingParts)
|
foreach (var moving in movingParts)
|
||||||
@@ -82,15 +74,64 @@ namespace OpenNest.Engine.Fill
|
|||||||
else if (edgeDist < distance)
|
else if (edgeDist < distance)
|
||||||
distance = edgeDist;
|
distance = edgeDist;
|
||||||
|
|
||||||
|
foreach (var set in obstacleSets)
|
||||||
|
distance = set.Travel(moving, direction, opposite, distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance < double.MaxValue && distance > 0)
|
||||||
|
{
|
||||||
|
var offset = direction * distance;
|
||||||
|
foreach (var moving in movingParts)
|
||||||
|
moving.Offset(offset);
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obstacles kept at one spacing, with their bounds and spacing-offset contours
|
||||||
|
/// cached for one push.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ObstacleSet
|
||||||
|
{
|
||||||
|
private readonly List<Part> parts;
|
||||||
|
private readonly Box[] spacingBoxes;
|
||||||
|
private readonly List<Entity>[] entities;
|
||||||
|
private readonly double halfSpacing;
|
||||||
|
|
||||||
|
public ObstacleSet(List<Part> parts, double spacing)
|
||||||
|
{
|
||||||
|
this.parts = parts;
|
||||||
|
Spacing = System.Math.Max(0, spacing);
|
||||||
|
halfSpacing = Spacing / 2;
|
||||||
|
Boxes = new Box[parts.Count];
|
||||||
|
spacingBoxes = new Box[parts.Count];
|
||||||
|
entities = new List<Entity>[parts.Count];
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
{
|
||||||
|
Boxes[i] = parts[i].BoundingBox;
|
||||||
|
spacingBoxes[i] = SpacingBounds(Boxes[i], halfSpacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Spacing { get; }
|
||||||
|
|
||||||
|
public Box[] Boxes { get; }
|
||||||
|
|
||||||
|
/// <summary>The shorter of <paramref name="distance"/> and how far
|
||||||
|
/// <paramref name="moving"/> travels before it comes within the spacing.</summary>
|
||||||
|
public double Travel(Part moving, Vector direction, Vector opposite, double distance)
|
||||||
|
{
|
||||||
var movingBox = moving.BoundingBox;
|
var movingBox = moving.BoundingBox;
|
||||||
List<Entity> movingEntities = null;
|
List<Entity> movingEntities = null;
|
||||||
|
|
||||||
// Check if any obstacle is inside the moving part — only then
|
// Check if any obstacle is inside the moving part — only then
|
||||||
// do we need cutout entities on the moving part.
|
// do we need cutout entities on the moving part.
|
||||||
var needCutouts = false;
|
var needCutouts = false;
|
||||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
for (var i = 0; i < Boxes.Length; i++)
|
||||||
{
|
{
|
||||||
if (movingBox.Contains(obstacleBoxes[i]))
|
if (movingBox.Contains(Boxes[i]))
|
||||||
{
|
{
|
||||||
needCutouts = true;
|
needCutouts = true;
|
||||||
break;
|
break;
|
||||||
@@ -100,9 +141,9 @@ namespace OpenNest.Engine.Fill
|
|||||||
// Broad-phase bounds must enclose the spacing-offset contours.
|
// Broad-phase bounds must enclose the spacing-offset contours.
|
||||||
// Raw bounds can miss near passes and overestimate the safe travel.
|
// Raw bounds can miss near passes and overestimate the safe travel.
|
||||||
var movingSpacingBox = SpacingBounds(movingBox, halfSpacing);
|
var movingSpacingBox = SpacingBounds(movingBox, halfSpacing);
|
||||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
for (var i = 0; i < Boxes.Length; i++)
|
||||||
{
|
{
|
||||||
var obstacleSpacingBox = obstacleSpacingBoxes[i];
|
var obstacleSpacingBox = spacingBoxes[i];
|
||||||
var reverseGap = SpatialQuery.DirectionalGap(
|
var reverseGap = SpatialQuery.DirectionalGap(
|
||||||
movingSpacingBox,
|
movingSpacingBox,
|
||||||
obstacleSpacingBox,
|
obstacleSpacingBox,
|
||||||
@@ -143,33 +184,25 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
// A moving part can be inside an obstacle's cutout. Omitting that
|
// A moving part can be inside an obstacle's cutout. Omitting that
|
||||||
// loop would let it cross the inner wall before seeing the perimeter.
|
// loop would let it cross the inner wall before seeing the perimeter.
|
||||||
obstacleEntities[i] ??=
|
entities[i] ??=
|
||||||
halfSpacing > 0
|
halfSpacing > 0
|
||||||
? PartGeometry.GetOffsetPartEntities(obstacleParts[i], halfSpacing)
|
? PartGeometry.GetOffsetPartEntities(parts[i], halfSpacing)
|
||||||
: PartGeometry.GetPartEntities(obstacleParts[i]);
|
: PartGeometry.GetPartEntities(parts[i]);
|
||||||
|
|
||||||
// Contacts left by a previous push only block directions that would
|
// Contacts left by a previous push only block directions that would
|
||||||
// push material into material; the kernel classifies them.
|
// push material into material; the kernel classifies them.
|
||||||
var d = SpatialQuery.DirectionalDistance(
|
var d = SpatialQuery.DirectionalDistance(
|
||||||
movingEntities,
|
movingEntities,
|
||||||
obstacleEntities[i],
|
entities[i],
|
||||||
direction
|
direction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (d < distance)
|
if (d < distance)
|
||||||
distance = d;
|
distance = d;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (distance < double.MaxValue && distance > 0)
|
|
||||||
{
|
|
||||||
var offset = direction * distance;
|
|
||||||
foreach (var moving in movingParts)
|
|
||||||
moving.Offset(offset);
|
|
||||||
return distance;
|
return distance;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Box SpacingBounds(Box box, double spacing)
|
private static Box SpacingBounds(Box box, double spacing)
|
||||||
@@ -228,19 +261,17 @@ namespace OpenNest.Engine.Fill
|
|||||||
PushDirection direction
|
PushDirection direction
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var obstacleParts = ZoneObstacles(plate);
|
var obstacleParts = plate.Parts
|
||||||
obstacleParts.InsertRange(
|
.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
||||||
0,
|
.ToList();
|
||||||
plate.Parts.Where(p => !movingParts.Contains(p) && !IntersectsAny(p, movingParts))
|
var zones = ZoneObstacles(plate);
|
||||||
.ToList()
|
|
||||||
);
|
|
||||||
|
|
||||||
return PushBoundingBox(
|
return PushBoundingBox(
|
||||||
movingParts,
|
movingParts,
|
||||||
obstacleParts,
|
|
||||||
plate.WorkArea(),
|
plate.WorkArea(),
|
||||||
plate.PartSpacing,
|
direction,
|
||||||
direction
|
(BoundingBoxes(obstacleParts), plate.PartSpacing),
|
||||||
|
(zones.Boxes, zones.Spacing)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,12 +281,23 @@ namespace OpenNest.Engine.Fill
|
|||||||
Box workArea,
|
Box workArea,
|
||||||
double partSpacing,
|
double partSpacing,
|
||||||
PushDirection direction
|
PushDirection direction
|
||||||
|
) => PushBoundingBox(movingParts, workArea, direction, (BoundingBoxes(obstacleParts), partSpacing));
|
||||||
|
|
||||||
|
private static Box[] BoundingBoxes(List<Part> parts)
|
||||||
|
{
|
||||||
|
var boxes = new Box[parts.Count];
|
||||||
|
for (var i = 0; i < parts.Count; i++)
|
||||||
|
boxes[i] = parts[i].BoundingBox;
|
||||||
|
return boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double PushBoundingBox(
|
||||||
|
List<Part> movingParts,
|
||||||
|
Box workArea,
|
||||||
|
PushDirection direction,
|
||||||
|
params (Box[] Boxes, double Spacing)[] obstacleSets
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var obstacleBoxes = new Box[obstacleParts.Count];
|
|
||||||
for (var i = 0; i < obstacleParts.Count; i++)
|
|
||||||
obstacleBoxes[i] = obstacleParts[i].BoundingBox;
|
|
||||||
|
|
||||||
var opposite = SpatialQuery.OppositeDirection(direction);
|
var opposite = SpatialQuery.OppositeDirection(direction);
|
||||||
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
var isHorizontal = SpatialQuery.IsHorizontalDirection(direction);
|
||||||
var distance = double.MaxValue;
|
var distance = double.MaxValue;
|
||||||
@@ -270,29 +312,32 @@ namespace OpenNest.Engine.Fill
|
|||||||
|
|
||||||
var movingBox = moving.BoundingBox;
|
var movingBox = moving.BoundingBox;
|
||||||
|
|
||||||
for (var i = 0; i < obstacleBoxes.Length; i++)
|
foreach (var (obstacleBoxes, spacing) in obstacleSets)
|
||||||
{
|
{
|
||||||
var reverseGap = SpatialQuery.DirectionalGap(
|
for (var i = 0; i < obstacleBoxes.Length; i++)
|
||||||
movingBox,
|
{
|
||||||
obstacleBoxes[i],
|
var reverseGap = SpatialQuery.DirectionalGap(
|
||||||
opposite
|
movingBox,
|
||||||
);
|
obstacleBoxes[i],
|
||||||
if (reverseGap > 0)
|
opposite
|
||||||
continue;
|
);
|
||||||
|
if (reverseGap > 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
var perpOverlap = isHorizontal
|
var perpOverlap = isHorizontal
|
||||||
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
|
? movingBox.IsHorizontalTo(obstacleBoxes[i], out _)
|
||||||
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
|
: movingBox.IsVerticalTo(obstacleBoxes[i], out _);
|
||||||
|
|
||||||
if (!perpOverlap)
|
if (!perpOverlap)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction);
|
||||||
var d = gap - partSpacing - 0.002;
|
var d = gap - spacing - 0.002;
|
||||||
if (d < 0)
|
if (d < 0)
|
||||||
d = 0;
|
d = 0;
|
||||||
if (d < distance)
|
if (d < distance)
|
||||||
distance = d;
|
distance = d;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,15 +463,17 @@ namespace OpenNest.Engine.Fill
|
|||||||
if (parts.Count < 2)
|
if (parts.Count < 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var obstacles = Jobs.InteractiveClamps.ObstacleParts(plate);
|
var obstacles = plate.Parts.Where(part => !parts.Contains(part)
|
||||||
obstacles.AddRange(plate.Parts.Where(part => !parts.Contains(part)
|
&& !part.BaseDrawing.IsCutOff).ToList();
|
||||||
&& !part.BaseDrawing.IsCutOff));
|
var zones = Jobs.InteractiveClamps.ObstacleParts(plate);
|
||||||
|
|
||||||
for (var i = 0; i < maxIterations; i++)
|
for (var i = 0; i < maxIterations; i++)
|
||||||
{
|
{
|
||||||
var moved = 0.0;
|
var moved = 0.0;
|
||||||
moved += Push(parts, obstacles, workArea, plate.PartSpacing, PushDirection.Left);
|
foreach (var direction in new[] { PushDirection.Left, PushDirection.Down })
|
||||||
moved += Push(parts, obstacles, workArea, plate.PartSpacing, PushDirection.Down);
|
moved += Push(parts, workArea, SpatialQuery.DirectionToOffset(direction, 1.0),
|
||||||
|
new ObstacleSet(obstacles, plate.PartSpacing),
|
||||||
|
new ObstacleSet(zones, 0));
|
||||||
|
|
||||||
if (moved < threshold)
|
if (moved < threshold)
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ namespace OpenNest.Engine.Fill
|
|||||||
var movers = new List<Part>(selected);
|
var movers = new List<Part>(selected);
|
||||||
var walls = plate.Parts.Where(p => !movers.Contains(p)).ToList();
|
var walls = plate.Parts.Where(p => !movers.Contains(p)).ToList();
|
||||||
var clampStart = movers.Count + walls.Count;
|
var clampStart = movers.Count + walls.Count;
|
||||||
// Clamp walls include the bottom-edge clearance envelope.
|
// Clamp walls include the clamp clearance envelope.
|
||||||
var clampBoxes = Jobs.InteractiveClamps.ClearanceRectangles(plate);
|
var clampBoxes = Jobs.InteractiveClamps.ClearanceRectangles(plate);
|
||||||
walls.AddRange(Jobs.InteractiveClamps.WallParts(plate));
|
walls.AddRange(Jobs.InteractiveClamps.WallParts(plate));
|
||||||
if (clampBoxes.Count != walls.Count + movers.Count - clampStart)
|
if (clampBoxes.Count != walls.Count + movers.Count - clampStart)
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ internal static class InteractiveClamps
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Placement keep-outs inflated by the full bottom-edge clearance (D6) - the same
|
/// Placement keep-outs inflated by the full <see cref="Plate.ClampClearance"/> - the same
|
||||||
/// convention the Rectangles lane blocks and engines honour, so fill and pack produce
|
/// convention the Rectangles lane blocks and engines honour, so fill and pack produce
|
||||||
/// placements with at least that clearance from the raw zone.
|
/// placements with at least that clearance from the raw zone.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyList<Box> ClearanceRectangles(Plate plate) =>
|
public static IReadOnlyList<Box> ClearanceRectangles(Plate plate) =>
|
||||||
InflateZones(plate, System.Math.Max(0, plate.EdgeSpacing.Bottom));
|
InflateZones(plate, plate.ClampClearance);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The validator's exact C1 rectangle (NestLayoutCheck: bottom spacing less the shared
|
/// The validator's exact C1 rectangle (NestLayoutCheck: clamp clearance less the shared
|
||||||
/// spacing slack). Used by the service safety net so flush engine-clear placements are
|
/// spacing slack). Used by the service safety net so flush engine-clear placements are
|
||||||
/// never filtered by rounding.
|
/// never filtered by rounding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyList<Box> ValidationKeepOuts(Plate plate) =>
|
public static IReadOnlyList<Box> ValidationKeepOuts(Plate plate) =>
|
||||||
InflateZones(plate, System.Math.Max(0, plate.EdgeSpacing.Bottom - NestTolerances.SpacingSlack));
|
InflateZones(plate, System.Math.Max(0, plate.ClampClearance - NestTolerances.SpacingSlack));
|
||||||
|
|
||||||
private static IReadOnlyList<Box> InflateZones(Plate plate, double clearance)
|
private static IReadOnlyList<Box> InflateZones(Plate plate, double clearance)
|
||||||
{
|
{
|
||||||
@@ -61,19 +61,18 @@ internal static class InteractiveClamps
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static obstacle parts for the directional push kernels. The kernel enforces
|
/// Static obstacle parts for the directional push kernels: each zone grown square by
|
||||||
/// part-spacing between every obstacle pair, so the rectangles carry only the
|
/// <see cref="Plate.ClampClearance"/>, the layout check's keep-out, which the kernels then
|
||||||
/// clearance the kernel would otherwise miss: max(0, bottom spacing - part spacing).
|
/// hold at zero spacing (never the part spacing). A round spacing offset would let a part
|
||||||
/// Final material clearance from a raw zone is then at least max(bottom, partSpacing),
|
/// tuck into the keep-out's corners.
|
||||||
/// never below the C1 keep-out.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static List<Part> ObstacleParts(Plate plate) =>
|
public static List<Part> ObstacleParts(Plate plate) =>
|
||||||
ObstaclePartsFor(ZoneRectangles(plate), System.Math.Max(0, plate.EdgeSpacing.Bottom - plate.PartSpacing));
|
ObstaclePartsFor(ZoneRectangles(plate), plate.ClampClearance);
|
||||||
|
|
||||||
/// <summary>Expander walls use the bottom-edge clearance as a fixed floor,
|
/// <summary>Expander walls use the clamp clearance as a fixed floor,
|
||||||
/// independent of the separation target or its final achieved spacing.</summary>
|
/// independent of the separation target or its final achieved spacing.</summary>
|
||||||
public static List<Part> WallParts(Plate plate) =>
|
public static List<Part> WallParts(Plate plate) =>
|
||||||
ObstaclePartsFor(ZoneRectangles(plate), System.Math.Max(0, plate.EdgeSpacing.Bottom));
|
ObstaclePartsFor(ZoneRectangles(plate), plate.ClampClearance);
|
||||||
|
|
||||||
private static List<Part> ObstaclePartsFor(IReadOnlyList<Box> rectangles, double inflate)
|
private static List<Part> ObstaclePartsFor(IReadOnlyList<Box> rectangles, double inflate)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -241,8 +241,8 @@ public static class NestLayoutCheck
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// C1/C5: no part material may enter a clamp zone inflated by the plate's bottom-edge
|
/// C1/C5: no part material may enter a clamp zone inflated by the plate's clamp
|
||||||
/// spacing (D6) - the same clearance the part owes the sheet edge, enforced with the
|
/// clearance (the rail's own, else the bottom-edge spacing, D6), enforced with the
|
||||||
/// shared spacing slack so exact engine placements are not reddened by rounding.
|
/// shared spacing slack so exact engine placements are not reddened by rounding.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void ValidateClamps(
|
private static void ValidateClamps(
|
||||||
@@ -252,7 +252,7 @@ public static class NestLayoutCheck
|
|||||||
List<string> result
|
List<string> result
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var inflateBy = InflationFor(plate.EdgeSpacing.Bottom);
|
var inflateBy = InflationFor(plate.ClampClearance);
|
||||||
|
|
||||||
// Plate zones are stored in sheet coordinates; parts are in the placement frame.
|
// Plate zones are stored in sheet coordinates; parts are in the placement frame.
|
||||||
foreach (var zone in ClampZoneFrame.ToPlacement(plate))
|
foreach (var zone in ClampZoneFrame.ToPlacement(plate))
|
||||||
@@ -394,6 +394,7 @@ public static class NestLayoutCheck
|
|||||||
&& expected.EdgeSpacing.Right.IsEqualTo(actual.EdgeSpacing.Right)
|
&& expected.EdgeSpacing.Right.IsEqualTo(actual.EdgeSpacing.Right)
|
||||||
&& expected.EdgeSpacing.Top.IsEqualTo(actual.EdgeSpacing.Top)
|
&& expected.EdgeSpacing.Top.IsEqualTo(actual.EdgeSpacing.Top)
|
||||||
&& expected.EdgeSpacing.Bottom.IsEqualTo(actual.EdgeSpacing.Bottom)
|
&& expected.EdgeSpacing.Bottom.IsEqualTo(actual.EdgeSpacing.Bottom)
|
||||||
|
&& expected.ClampClearance.IsEqualTo(actual.ClampClearance)
|
||||||
&& expected.Quadrant == actual.Quadrant
|
&& expected.Quadrant == actual.Quadrant
|
||||||
&& Equals(expected.Outline, actual.Outline)
|
&& Equals(expected.Outline, actual.Outline)
|
||||||
&& SameZones(expected.ClampZones, actual.ClampZones)
|
&& SameZones(expected.ClampZones, actual.ClampZones)
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ public sealed class NestPlateStock
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal ClampRail ClampRail { get; init; }
|
internal ClampRail ClampRail { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The gap part material keeps from every clamp zone, as <see cref="Plate.ClampClearance"/>:
|
||||||
|
/// the rail's own clearance, else the bottom edge spacing (D6). Never part spacing.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public double ClampClearance => System.Math.Max(0, ClampRail?.Clearance ?? EdgeSpacing.Bottom);
|
||||||
|
|
||||||
/// <summary>Generic per-physical-sheet cost; null selects area scoring.</summary>
|
/// <summary>Generic per-physical-sheet cost; null selects area scoring.</summary>
|
||||||
public double? Cost { get; }
|
public double? Cost { get; }
|
||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ internal static class RepeatedPlateGrouping
|
|||||||
&& Bits(a.EdgeSpacing.Right) == Bits(b.EdgeSpacing.Right)
|
&& Bits(a.EdgeSpacing.Right) == Bits(b.EdgeSpacing.Right)
|
||||||
&& Bits(a.EdgeSpacing.Top) == Bits(b.EdgeSpacing.Top)
|
&& Bits(a.EdgeSpacing.Top) == Bits(b.EdgeSpacing.Top)
|
||||||
&& Bits(a.EdgeSpacing.Bottom) == Bits(b.EdgeSpacing.Bottom)
|
&& Bits(a.EdgeSpacing.Bottom) == Bits(b.EdgeSpacing.Bottom)
|
||||||
|
&& Bits(a.ClampClearance) == Bits(b.ClampClearance)
|
||||||
&& a.Quantity == b.Quantity
|
&& a.Quantity == b.Quantity
|
||||||
&& Equals(a.Outline, b.Outline)
|
&& Equals(a.Outline, b.Outline)
|
||||||
&& a.Cost.HasValue == b.Cost.HasValue
|
&& a.Cost.HasValue == b.Cost.HasValue
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ internal enum DiscSeed
|
|||||||
/// seeds the sheet before the greedy pass fills the rest.
|
/// seeds the sheet before the greedy pass fills the rest.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Every disc keeps the part spacing plus <see cref="Margin"/> from every other disc, and
|
/// <remarks>Every disc keeps the part spacing plus <see cref="Margin"/> from every other disc, and
|
||||||
/// its radius from the work-area edges and from every clamp zone grown by the bottom-edge
|
/// its radius from the work-area edges and from every clamp zone grown by the stock's clamp
|
||||||
/// clearance. Radii are <see cref="Item.Radius"/>, which covers the layout check's outward flattening,
|
/// clearance. Radii are <see cref="Item.Radius"/>, which covers the layout check's outward flattening,
|
||||||
/// so packing is never more lenient than the check. Deterministic.</remarks>
|
/// so packing is never more lenient than the check. Deterministic.</remarks>
|
||||||
internal static class DiscPacker
|
internal static class DiscPacker
|
||||||
@@ -145,9 +145,9 @@ internal static class DiscPacker
|
|||||||
{
|
{
|
||||||
this.stock = stock;
|
this.stock = stock;
|
||||||
work = stock.WorkArea;
|
work = stock.WorkArea;
|
||||||
// The layout check grows each zone by the bottom-edge clearance as a box, square
|
// The layout check grows each zone by the clamp clearance as a box, square
|
||||||
// corners included, so discs keep their radius from that grown box.
|
// corners included, so discs keep their radius from that grown box.
|
||||||
var clearance = System.Math.Max(0, stock.EdgeSpacing.Bottom);
|
var clearance = stock.ClampClearance;
|
||||||
clamps = stock.ClampZones.Select(z => z.Bounds.Offset(clearance)).ToArray();
|
clamps = stock.ClampZones.Select(z => z.Bounds.Offset(clearance)).ToArray();
|
||||||
Gap = stock.PartSpacing + Margin;
|
Gap = stock.PartSpacing + Margin;
|
||||||
// Any disc within reach of a new one lies in the adjacent cells.
|
// Any disc within reach of a new one lies in the adjacent cells.
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ internal sealed class BarStock
|
|||||||
/// Free runs of the bar's length. A clamp zone whose clearance-expanded cross extent
|
/// Free runs of the bar's length. A clamp zone whose clearance-expanded cross extent
|
||||||
/// reaches the bar's row blocks its clearance-expanded axis interval for the whole row:
|
/// reaches the bar's row blocks its clearance-expanded axis interval for the whole row:
|
||||||
/// parts in a bar span its width, so none can pass beside a clamp. Clearance is the
|
/// parts in a bar span its width, so none can pass beside a clamp. Clearance is the
|
||||||
/// stock's bottom edge spacing, as in the other engines; part spacing is not added.
|
/// stock's clamp clearance, as in the other engines; part spacing is not added.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static IReadOnlyList<BarSegment> FreeRuns(NestPlateStock stock, Box work, bool alongX)
|
private static IReadOnlyList<BarSegment> FreeRuns(NestPlateStock stock, Box work, bool alongX)
|
||||||
{
|
{
|
||||||
@@ -165,7 +165,7 @@ internal sealed class BarStock
|
|||||||
var end = alongX ? work.Right : work.Top;
|
var end = alongX ? work.Right : work.Top;
|
||||||
var crossLow = alongX ? work.Bottom : work.Left;
|
var crossLow = alongX ? work.Bottom : work.Left;
|
||||||
var crossHigh = alongX ? work.Top : work.Right;
|
var crossHigh = alongX ? work.Top : work.Right;
|
||||||
var clearance = System.Math.Max(0, stock.EdgeSpacing.Bottom);
|
var clearance = stock.ClampClearance;
|
||||||
var blocked = new List<(double Low, double High)>();
|
var blocked = new List<(double Low, double High)>();
|
||||||
foreach (var zone in stock.ClampZones)
|
foreach (var zone in stock.ClampZones)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace OpenNest.Engine.NestingEngines.Irregular;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Static, clearance-inflated rectangles prepared once per stock per solve. Kept out of
|
/// Static, clearance-inflated rectangles prepared once per stock per solve. Kept out of
|
||||||
/// the part catalog and its spacing cache: clamp clearance is bottom edge spacing, not
|
/// the part catalog and its spacing cache: clamp clearance is the stock's clamp clearance, not
|
||||||
/// part spacing, and two stocks at the same part spacing may have different clamps.
|
/// part spacing, and two stocks at the same part spacing may have different clamps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class ClampObstacles
|
internal sealed class ClampObstacles
|
||||||
@@ -17,7 +17,7 @@ internal sealed class ClampObstacles
|
|||||||
public ClampObstacles(NestPlateStock stock)
|
public ClampObstacles(NestPlateStock stock)
|
||||||
{
|
{
|
||||||
rectangles = new Orientation[stock.ClampZones.Count];
|
rectangles = new Orientation[stock.ClampZones.Count];
|
||||||
var clearance = System.Math.Max(0, stock.EdgeSpacing.Bottom);
|
var clearance = stock.ClampClearance;
|
||||||
for (var i = 0; i < rectangles.Length; i++)
|
for (var i = 0; i < rectangles.Length; i++)
|
||||||
{
|
{
|
||||||
var box = stock.ClampZones[i].Bounds;
|
var box = stock.ClampZones[i].Bounds;
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ internal static class SheetPacker
|
|||||||
/// Blocks every clamp keep-out into the sheet's free list before packing. In the
|
/// Blocks every clamp keep-out into the sheet's free list before packing. In the
|
||||||
/// inflated-box model an item at position p covers material [p, p + size] plus a trailing
|
/// inflated-box model an item at position p covers material [p, p + size] plus a trailing
|
||||||
/// part-spacing tail; blocking bin-local [zone - clearance + s, zone + clearance] per axis
|
/// part-spacing tail; blocking bin-local [zone - clearance + s, zone + clearance] per axis
|
||||||
/// therefore means exactly: item material keeps at least the bottom-edge clearance from
|
/// therefore means exactly: item material keeps at least the clamp clearance from
|
||||||
/// every clamp side, without paying any extra part-spacing tax there. When s exceeds the
|
/// every clamp side, without paying any extra part-spacing tax there. When s exceeds the
|
||||||
/// inflated span the interval inverts (decision D11): block a zero-extent line at the
|
/// inflated span the interval inverts (decision D11): block a zero-extent line at the
|
||||||
/// clamp edge facing the sheet interior — at (zone max + clearance) when the zone center
|
/// clamp edge facing the sheet interior — at (zone max + clearance) when the zone center
|
||||||
@@ -109,7 +109,7 @@ internal static class SheetPacker
|
|||||||
if (stock.ClampZones.Count == 0)
|
if (stock.ClampZones.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var clearance = System.Math.Max(0, stock.EdgeSpacing.Bottom);
|
var clearance = stock.ClampClearance;
|
||||||
foreach (var zone in stock.ClampZones)
|
foreach (var zone in stock.ClampZones)
|
||||||
{
|
{
|
||||||
var b = zone.Bounds; // placement frame -> bin-local packing coordinates
|
var b = zone.Bounds; // placement frame -> bin-local packing coordinates
|
||||||
|
|||||||
@@ -519,4 +519,17 @@ public class ClampRailPersistenceTests
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RailClearanceRoundTripsAndIsOmittedWhenUnset()
|
||||||
|
{
|
||||||
|
var nest = RailNest(withEmptyTail: false);
|
||||||
|
nest.SetClampRail(new ClampRail(1.5, 0.625, MachineCenters, clearance: 0.05));
|
||||||
|
|
||||||
|
var reopened = RoundTrip(nest);
|
||||||
|
|
||||||
|
Assert.Equal(0.05, reopened.PlateDefaults.ClampRail.Clearance);
|
||||||
|
Assert.Equal(0.05, Assert.Single(reopened.Plates).ClampClearance);
|
||||||
|
Assert.Null(RoundTrip(RailNest(withEmptyTail: false)).PlateDefaults.ClampRail.Clearance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -274,6 +274,7 @@ namespace OpenNest.IO
|
|||||||
double? removalMinutes = TryNumber(rail, "removalMinutes", out var minutes)
|
double? removalMinutes = TryNumber(rail, "removalMinutes", out var minutes)
|
||||||
? minutes
|
? minutes
|
||||||
: null;
|
: null;
|
||||||
|
double? clearance = TryNumber(rail, "clearance", out var gap) ? gap : null;
|
||||||
return new ClampRailDto
|
return new ClampRailDto
|
||||||
{
|
{
|
||||||
Width = width,
|
Width = width,
|
||||||
@@ -283,6 +284,7 @@ namespace OpenNest.IO
|
|||||||
OriginEndCenters = others[1],
|
OriginEndCenters = others[1],
|
||||||
FarEndCenters = others[2],
|
FarEndCenters = others[2],
|
||||||
RemovalMinutes = removalMinutes,
|
RemovalMinutes = removalMinutes,
|
||||||
|
Clearance = clearance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +331,8 @@ namespace OpenNest.IO
|
|||||||
WriteCenters(writer, OtherEdgeKeys[2], value.FarEndCenters);
|
WriteCenters(writer, OtherEdgeKeys[2], value.FarEndCenters);
|
||||||
if (value.RemovalMinutes is double removalMinutes)
|
if (value.RemovalMinutes is double removalMinutes)
|
||||||
writer.WriteNumber("removalMinutes", removalMinutes);
|
writer.WriteNumber("removalMinutes", removalMinutes);
|
||||||
|
if (value.Clearance is double clearance)
|
||||||
|
writer.WriteNumber("clearance", clearance);
|
||||||
writer.WriteEndObject();
|
writer.WriteEndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +420,12 @@ namespace OpenNest.IO
|
|||||||
/// files and readers fall back to <see cref="OpenNest.ClampRail.DefaultRemovalMinutes"/>.
|
/// files and readers fall back to <see cref="OpenNest.ClampRail.DefaultRemovalMinutes"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public double? RemovalMinutes { get; init; }
|
public double? RemovalMinutes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The gap parts keep from every clamp (<see cref="OpenNest.ClampRail.Clearance"/>).
|
||||||
|
/// Omitted when the rail uses the sheet's bottom edge spacing, as older files do.
|
||||||
|
/// </summary>
|
||||||
|
public double? Clearance { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record DrawingDto
|
public record DrawingDto
|
||||||
|
|||||||
@@ -527,7 +527,8 @@ namespace OpenNest.IO
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The saved machine clamp rail, or null when absent, empty or malformed (a bad rail
|
/// The saved machine clamp rail, or null when absent, empty or malformed (a bad rail
|
||||||
/// is ignored rather than failing the nest; each plate's saved zones still load). A
|
/// is ignored rather than failing the nest; each plate's saved zones still load). A
|
||||||
/// missing or invalid removal-minutes value falls back to <see cref="ClampRail.DefaultRemovalMinutes"/>.
|
/// missing or invalid removal-minutes value falls back to <see cref="ClampRail.DefaultRemovalMinutes"/>,
|
||||||
|
/// and a missing or invalid clearance to the sheet's bottom edge spacing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <summary>The saved support bed, or null when absent or invalid (a bad bed never fails the nest).</summary>
|
/// <summary>The saved support bed, or null when absent or invalid (a bad bed never fails the nest).</summary>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -602,7 +603,8 @@ namespace OpenNest.IO
|
|||||||
var minutes = dto.RemovalMinutes is double m && double.IsFinite(m) && m >= 0
|
var minutes = dto.RemovalMinutes is double m && double.IsFinite(m) && m >= 0
|
||||||
? m
|
? m
|
||||||
: ClampRail.DefaultRemovalMinutes;
|
: ClampRail.DefaultRemovalMinutes;
|
||||||
return new ClampRail(dto.Width, dto.Depth, clamps, minutes);
|
double? clearance = dto.Clearance is double c && double.IsFinite(c) && c >= 0 ? c : null;
|
||||||
|
return new ClampRail(dto.Width, dto.Depth, clamps, minutes, clearance);
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ namespace OpenNest.IO
|
|||||||
OriginEndCenters = CentersOn(rail, ClampEdge.OriginEnd),
|
OriginEndCenters = CentersOn(rail, ClampEdge.OriginEnd),
|
||||||
FarEndCenters = CentersOn(rail, ClampEdge.FarEnd),
|
FarEndCenters = CentersOn(rail, ClampEdge.FarEnd),
|
||||||
RemovalMinutes = rail.RemovalMinutes,
|
RemovalMinutes = rail.RemovalMinutes,
|
||||||
|
Clearance = rail.Clearance,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static List<double> CentersOn(ClampRail rail, ClampEdge edge) =>
|
private static List<double> CentersOn(ClampRail rail, ClampEdge edge) =>
|
||||||
|
|||||||
@@ -495,4 +495,39 @@ public class ClampRailTests
|
|||||||
Assert.Null(plate.RailClampAt(new Vector(36, 0.7)));
|
Assert.Null(plate.RailClampAt(new Vector(36, 0.7)));
|
||||||
Assert.Equal("clamp-3", plate.RailClampAt(new Vector(36, 0.7), tolerance: 0.1)?.Id);
|
Assert.Equal("clamp-3", plate.RailClampAt(new Vector(36, 0.7), tolerance: 0.1)?.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(-0.1)]
|
||||||
|
[InlineData(double.NaN)]
|
||||||
|
[InlineData(double.PositiveInfinity)]
|
||||||
|
public void ClearanceMustBeFiniteAndZeroOrMore(double clearance)
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => new ClampRail(1.5, 0.625, new[] { 6.0 }, clearance: clearance));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearanceIsPartOfTheRailAndSurvivesNewClamps()
|
||||||
|
{
|
||||||
|
var rail = new ClampRail(1.5, 0.625, new[] { 6.0 }, clearance: 0.05);
|
||||||
|
|
||||||
|
Assert.Equal(0.05, rail.Clearance);
|
||||||
|
Assert.Equal(0.05, rail.WithClamps(new[] { new RailClamp(ClampEdge.FarSide, 9) }).Clearance);
|
||||||
|
Assert.NotEqual(rail, new ClampRail(1.5, 0.625, new[] { 6.0 }));
|
||||||
|
Assert.Equal(rail, new ClampRail(1.5, 0.625, new[] { 6.0 }, clearance: 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlateClampClearanceIsTheRailsOwnElseTheBottomEdgeSpacing()
|
||||||
|
{
|
||||||
|
var plate = Sheet(60, 120);
|
||||||
|
plate.EdgeSpacing = new Spacing(0.25, 0.5, 0.25, 0.25);
|
||||||
|
plate.PartSpacing = 1;
|
||||||
|
Assert.Equal(0.5, plate.ClampClearance);
|
||||||
|
|
||||||
|
plate.SetClampRail(Machine());
|
||||||
|
Assert.Equal(0.5, plate.ClampClearance);
|
||||||
|
|
||||||
|
plate.SetClampRail(new ClampRail(1.5, 0.625, MachineCenters, clearance: 0));
|
||||||
|
Assert.Equal(0, plate.ClampClearance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -405,4 +405,30 @@ public class ClampSettingsTests : IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearanceRoundTripsConvertsAndReachesTheRail()
|
||||||
|
{
|
||||||
|
var settings = Machine();
|
||||||
|
settings.Clearance = 0.05;
|
||||||
|
|
||||||
|
var read = ClampSettings.Parse(settings.ToJson(), out var status);
|
||||||
|
|
||||||
|
Assert.Equal(ClampSettingsStatus.Ok, status);
|
||||||
|
Assert.Equal(0.05, read.Clearance);
|
||||||
|
Assert.Equal(0.05 * 25.4, read.ConvertedTo(Units.Millimeters).Clearance!.Value, 9);
|
||||||
|
Assert.Equal(0.05, read.RailFor(Units.Inches)!.Clearance);
|
||||||
|
Assert.Equal(0.05, ClampSettings.FromRail(read.RailFor(Units.Inches), Units.Inches, true).Clearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MissingClearanceKeepsTheBottomEdgeSpacingAndABadOneIsIgnored()
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain("clearance", Machine().ToJson());
|
||||||
|
Assert.Null(ClampSettings.Parse(Machine().ToJson(), out _).RailFor(Units.Inches)!.Clearance);
|
||||||
|
|
||||||
|
var read = ClampSettings.Parse("{\"enabled\": true, \"centers\": [6], \"clearance\": -1}", out var status);
|
||||||
|
Assert.Equal(ClampSettingsStatus.Partial, status);
|
||||||
|
Assert.Null(read.Clearance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -227,4 +227,19 @@ public class MachineClampsTests : IDisposable
|
|||||||
Assert.Equal(new[] { 12.0, 24.0 }, rail.Clamps.Select(c => System.Math.Round(c.Center, 9)));
|
Assert.Equal(new[] { 12.0, 24.0 }, rail.Clamps.Select(c => System.Math.Round(c.Center, 9)));
|
||||||
Assert.Equal(new[] { 304.8, 609.6 }, machine.Clamps!.Centers.Select(c => System.Math.Round(c, 9)));
|
Assert.Equal(new[] { 304.8, 609.6 }, machine.Clamps!.Centers.Select(c => System.Math.Round(c, 9)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearanceConvertsWithTheMachineAndAnInvalidOneKeepsTheEdgeSpacing()
|
||||||
|
{
|
||||||
|
var legacy = Legacy();
|
||||||
|
legacy.Clearance = 0.1;
|
||||||
|
Assert.Equal(0.1 * 25.4, MachineClamps.FromSettings(legacy, UnitSystem.Millimeters).Clearance!.Value, 9);
|
||||||
|
|
||||||
|
var clamps = Rail();
|
||||||
|
clamps.Clearance = 0.05;
|
||||||
|
Assert.Equal(0.05, clamps.ToSettings(UnitSystem.Inches).Clearance);
|
||||||
|
|
||||||
|
clamps.Clearance = double.NaN;
|
||||||
|
Assert.Null(clamps.ToSettings(UnitSystem.Inches).Clearance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -455,7 +455,8 @@ namespace OpenNest.Actions
|
|||||||
{
|
{
|
||||||
rail = list.Count == 0
|
rail = list.Count == 0
|
||||||
? null
|
? null
|
||||||
: new ClampRail(ClampWidth, ClampDepth, list, Rail?.RemovalMinutes ?? Defaults.RemovalMinutes);
|
: new ClampRail(ClampWidth, ClampDepth, list, Rail?.RemovalMinutes ?? Defaults.RemovalMinutes,
|
||||||
|
Rail != null ? Rail.Clearance : Defaults.Clearance);
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public sealed class ClampsForm : Form
|
|||||||
private readonly NumericUpDown widthBox;
|
private readonly NumericUpDown widthBox;
|
||||||
private readonly NumericUpDown depthBox;
|
private readonly NumericUpDown depthBox;
|
||||||
private readonly NumericUpDown removalMinutesBox;
|
private readonly NumericUpDown removalMinutesBox;
|
||||||
|
private readonly CheckBox ownClearanceBox;
|
||||||
|
private readonly NumericUpDown clearanceBox;
|
||||||
private readonly NumericUpDown centerBox;
|
private readonly NumericUpDown centerBox;
|
||||||
private readonly ComboBox edgeBox;
|
private readonly ComboBox edgeBox;
|
||||||
private readonly ListBox centersList;
|
private readonly ListBox centersList;
|
||||||
@@ -46,8 +48,8 @@ public sealed class ClampsForm : Form
|
|||||||
|
|
||||||
Text = "Clamps";
|
Text = "Clamps";
|
||||||
StartPosition = FormStartPosition.CenterParent;
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
ClientSize = new Size(480, 470);
|
ClientSize = new Size(480, 500);
|
||||||
MinimumSize = new Size(440, 460);
|
MinimumSize = new Size(440, 490);
|
||||||
MinimizeBox = false;
|
MinimizeBox = false;
|
||||||
MaximizeBox = false;
|
MaximizeBox = false;
|
||||||
ShowInTaskbar = false;
|
ShowInTaskbar = false;
|
||||||
@@ -73,9 +75,10 @@ public sealed class ClampsForm : Form
|
|||||||
Padding = new Padding(0, 0, 0, 8),
|
Padding = new Padding(0, 0, 0, 8),
|
||||||
Text =
|
Text =
|
||||||
"Fixed clamps on the plate edges, each centered a distance from the origin "
|
"Fixed clamps on the plate edges, each centered a distance from the origin "
|
||||||
+ "along its edge. Auto Nest keeps parts clear of each clamp by the plate's "
|
+ "along its edge. Parts keep the clamp clearance from each clamp (the plate's "
|
||||||
+ "bottom edge spacing; clamps that do not reach a smaller plate are left off "
|
+ "bottom edge spacing unless set below; part spacing never applies); clamps "
|
||||||
+ "it. Plate > Edit Clamps moves and adds clamps on the plate view.",
|
+ "that do not reach a smaller plate are left off it. Plate > Edit Clamps moves "
|
||||||
|
+ "and adds clamps on the plate view.",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -86,6 +89,19 @@ public sealed class ClampsForm : Form
|
|||||||
AddRow(size, "Clamp width (along the edge):", widthBox, unit);
|
AddRow(size, "Clamp width (along the edge):", widthBox, unit);
|
||||||
AddRow(size, "Clamp depth (into the plate):", depthBox, unit);
|
AddRow(size, "Clamp depth (into the plate):", depthBox, unit);
|
||||||
AddRow(size, "Minutes to remove and replace one clamp:", removalMinutesBox, "min");
|
AddRow(size, "Minutes to remove and replace one clamp:", removalMinutesBox, "min");
|
||||||
|
ownClearanceBox = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Clearance from parts:",
|
||||||
|
AutoSize = true,
|
||||||
|
Checked = Editor.Clearance.HasValue,
|
||||||
|
Padding = new Padding(0, 3, 6, 0),
|
||||||
|
};
|
||||||
|
clearanceBox = NewLengthBox(Editor.Clearance ?? 0, minimum: 0m);
|
||||||
|
clearanceBox.Enabled = ownClearanceBox.Checked;
|
||||||
|
ownClearanceBox.CheckedChanged += (_, _) => clearanceBox.Enabled = ownClearanceBox.Checked;
|
||||||
|
size.Controls.Add(ownClearanceBox);
|
||||||
|
size.Controls.Add(clearanceBox);
|
||||||
|
size.Controls.Add(new Label { Text = unit, AutoSize = true, Padding = new Padding(0, 6, 0, 0) });
|
||||||
layout.Controls.Add(size);
|
layout.Controls.Add(size);
|
||||||
|
|
||||||
var centersGroup = new GroupBox
|
var centersGroup = new GroupBox
|
||||||
@@ -226,6 +242,7 @@ public sealed class ClampsForm : Form
|
|||||||
Editor.Width = (double)widthBox.Value;
|
Editor.Width = (double)widthBox.Value;
|
||||||
Editor.Depth = (double)depthBox.Value;
|
Editor.Depth = (double)depthBox.Value;
|
||||||
Editor.RemovalMinutes = (double)removalMinutesBox.Value;
|
Editor.RemovalMinutes = (double)removalMinutesBox.Value;
|
||||||
|
Editor.Clearance = ownClearanceBox.Checked ? (double)clearanceBox.Value : null;
|
||||||
Editor.ApplyToNewNests = applyToNewNestsBox.Checked;
|
Editor.ApplyToNewNests = applyToNewNestsBox.Checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +285,8 @@ public sealed class ClampsForm : Form
|
|||||||
widthBox.Value = ToBoxValue(widthBox, Editor.Width);
|
widthBox.Value = ToBoxValue(widthBox, Editor.Width);
|
||||||
depthBox.Value = ToBoxValue(depthBox, Editor.Depth);
|
depthBox.Value = ToBoxValue(depthBox, Editor.Depth);
|
||||||
removalMinutesBox.Value = ToBoxValue(removalMinutesBox, Editor.RemovalMinutes);
|
removalMinutesBox.Value = ToBoxValue(removalMinutesBox, Editor.RemovalMinutes);
|
||||||
|
ownClearanceBox.Checked = Editor.Clearance.HasValue;
|
||||||
|
clearanceBox.Value = ToBoxValue(clearanceBox, Editor.Clearance ?? 0);
|
||||||
applyToNewNestsBox.Checked = Editor.ApplyToNewNests;
|
applyToNewNestsBox.Checked = Editor.ApplyToNewNests;
|
||||||
RefreshCenters();
|
RefreshCenters();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ namespace OpenNest.Forms
|
|||||||
Width = shown.Width,
|
Width = shown.Width,
|
||||||
Depth = shown.Depth,
|
Depth = shown.Depth,
|
||||||
RemovalMinutes = shown.RemovalMinutes,
|
RemovalMinutes = shown.RemovalMinutes,
|
||||||
|
Clearance = shown.Clearance,
|
||||||
};
|
};
|
||||||
|
|
||||||
void Set(Action<MachineClamps> apply)
|
void Set(Action<MachineClamps> apply)
|
||||||
@@ -328,6 +329,7 @@ namespace OpenNest.Forms
|
|||||||
CreateLabeledNumeric(shown.Depth, v => Set(c => c.Depth = v), 4, LengthUnitSuffix, 1000, 0.125m));
|
CreateLabeledNumeric(shown.Depth, v => Set(c => c.Depth = v), 4, LengthUnitSuffix, 1000, 0.125m));
|
||||||
AddField(layout, ref row, "Removal time:",
|
AddField(layout, ref row, "Removal time:",
|
||||||
CreateLabeledNumeric(shown.RemovalMinutes, v => Set(c => c.RemovalMinutes = v), 2, _ => "min per clamp", 1000, 0.5m));
|
CreateLabeledNumeric(shown.RemovalMinutes, v => Set(c => c.RemovalMinutes = v), 2, _ => "min per clamp", 1000, 0.5m));
|
||||||
|
AddField(layout, ref row, "Clearance:", CreateClampClearance(shown.Clearance, Set));
|
||||||
|
|
||||||
var centersGroup = new GroupBox
|
var centersGroup = new GroupBox
|
||||||
{
|
{
|
||||||
@@ -1017,6 +1019,37 @@ namespace OpenNest.Forms
|
|||||||
return numeric;
|
return numeric;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unchecked keeps each sheet's bottom edge spacing (a null clearance), as before clamps
|
||||||
|
// had their own.
|
||||||
|
private Control CreateClampClearance(double? clearance, Action<Action<MachineClamps>> set)
|
||||||
|
{
|
||||||
|
var row = new FlowLayoutPanel { AutoSize = true, WrapContents = false, Anchor = AnchorStyles.Left };
|
||||||
|
var ownBox = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Own clearance from parts",
|
||||||
|
AutoSize = true,
|
||||||
|
Checked = clearance.HasValue,
|
||||||
|
Margin = new Padding(0, 5, 6, 0),
|
||||||
|
};
|
||||||
|
var numeric = CreateNumericBox(clearance ?? 0, v => set(c => c.Clearance = ownBox.Checked ? v : null),
|
||||||
|
4, 1000, 0.0625m);
|
||||||
|
numeric.Enabled = ownBox.Checked;
|
||||||
|
ownBox.CheckedChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
numeric.Enabled = ownBox.Checked;
|
||||||
|
set(c => c.Clearance = ownBox.Checked ? (double)numeric.Value : null);
|
||||||
|
};
|
||||||
|
row.Controls.Add(ownBox);
|
||||||
|
row.Controls.Add(numeric);
|
||||||
|
row.Controls.Add(new Label
|
||||||
|
{
|
||||||
|
AutoSize = true,
|
||||||
|
Margin = new Padding(6, 6, 0, 0),
|
||||||
|
Text = LengthUnitSuffix(null),
|
||||||
|
});
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
private Control CreateLabeledNumeric(
|
private Control CreateLabeledNumeric(
|
||||||
double value,
|
double value,
|
||||||
Action<double> setter,
|
Action<double> setter,
|
||||||
|
|||||||
@@ -64,7 +64,16 @@ A nest can carry the machine's fixed clamp rail: identical rectangular clamps
|
|||||||
center measured from the plate origin along that edge. The edges are named from
|
center measured from the plate origin along that edge. The edges are named from
|
||||||
the origin: the origin side and far side run along X, the origin end and far
|
the origin: the origin side and far side run along X, the origin end and far
|
||||||
end along Y. **Nest > Clamps...** edits the current nest's rail as a list, and
|
end along Y. **Nest > Clamps...** edits the current nest's rail as a list, and
|
||||||
**Plate > Edit Clamps** edits it on the plate view (below). **OK** applies it to every plate of the nest (**Cancel** discards
|
**Plate > Edit Clamps** edits it on the plate view (below).
|
||||||
|
|
||||||
|
Parts keep the rail's **clamp clearance** from every clamp, never the part spacing.
|
||||||
|
Leave **Clearance from parts** unchecked to use each sheet's bottom edge spacing
|
||||||
|
(the behavior before clamps had their own clearance); check it to set a gap of its
|
||||||
|
own, such as close to zero for a support block drawn under the sheet, where the only
|
||||||
|
need is that the laser does not cut or pierce over it. The same clearance governs
|
||||||
|
Auto Nest's engines, interactive fill, pack and push, the layout check and automatic
|
||||||
|
cut-off placement; Check Overlaps and pre-post verification still flag only parts
|
||||||
|
or cuts actually on a clamp. **OK** applies it to every plate of the nest (**Cancel** discards
|
||||||
the edits); removing every center removes the clamps. **Load Machine Defaults**
|
the edits); removing every center removes the clamps. **Load Machine Defaults**
|
||||||
only fills the dialog with the clamps of the nest's machine (**Nest > Machine**).
|
only fills the dialog with the clamps of the nest's machine (**Nest > Machine**).
|
||||||
**Save as Machine Defaults** writes the clamps, and whether new nests receive them,
|
**Save as Machine Defaults** writes the clamps, and whether new nests receive them,
|
||||||
@@ -75,14 +84,16 @@ refuses to open while nesting or a plate action is running.
|
|||||||
|
|
||||||
Clamps are physical fixtures of a machine, so each machine keeps its own rail on the
|
Clamps are physical fixtures of a machine, so each machine keeps its own rail on the
|
||||||
**Clamps** tab of **Tools > Machine Configuration**: whether new nests get it, the
|
**Clamps** tab of **Tools > Machine Configuration**: whether new nests get it, the
|
||||||
clamp width and depth, the minutes to remove one clamp, and the clamp centers, all in
|
clamp width and depth, the minutes to remove one clamp, the clamp clearance and the
|
||||||
|
clamp centers, all in
|
||||||
the machine's units. New and BOM import give a new nest the rail of the machine they
|
the machine's units. New and BOM import give a new nest the rail of the machine they
|
||||||
assign it (the **Tools > Nest Defaults** machine, or the first by name), converted to
|
assign it (the **Tools > Nest Defaults** machine, or the first by name), converted to
|
||||||
the nest's units. A nest keeps its own copy: editing a machine's clamps, or assigning a
|
the nest's units. A nest keeps its own copy: editing a machine's clamps, or assigning a
|
||||||
nest another machine, never moves the clamps of an existing nest; use **Load Machine
|
nest another machine, never moves the clamps of an existing nest; use **Load Machine
|
||||||
Defaults** for that. Clamps are off by default: a machine without clamps, or with them
|
Defaults** for that. Clamps are off by default: a machine without clamps, or with them
|
||||||
switched off or without a center, gives new nests none. A stored size or removal time
|
switched off or without a center, gives new nests none. A stored size or removal time
|
||||||
that is not valid falls back to the default and an invalid center is dropped.
|
that is not valid falls back to the default, an invalid clearance to the bottom edge
|
||||||
|
spacing, and an invalid center is dropped.
|
||||||
|
|
||||||
Before machines held clamps, one `%APPDATA%\OpenNest\clamps.json` (or the server's
|
Before machines held clamps, one `%APPDATA%\OpenNest\clamps.json` (or the server's
|
||||||
shared clamp settings) served every machine. At startup, while no machine has clamps of
|
shared clamp settings) served every machine. At startup, while no machine has clamps of
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Only real parts contribute to the occupied envelope; existing cutoff parts do no
|
|||||||
|
|
||||||
Cutoff trimming measures clearance from the part contour, excluding tagged lead-in and lead-out moves when building the closed outline. Leads must not turn a recessed outline into a convex hull and leave oversized gaps elsewhere. Genuinely open contours (a tab gap, for example) still use the conservative hull/bounds fallback, but the hull is built from the material contour only: a cut-off may cross a lead, and hulling a lead's pierce point stopped cut-offs far short of the part. With a nonzero clearance, a line passing exactly through a corner of the clearance outline is trimmed to the outline like any other line. Automatic strip lines sit on a regular lattice and often land on such corners; they once fell back to the part's bounding box there, and the strip search preferred those lines because the box fallback measured as less cutting. The conservative curve/offset approximation can leave a small extra gap (about 0.0003 in on a straight edge at the default 0.02-in clearance), not an extra quarter inch.
|
Cutoff trimming measures clearance from the part contour, excluding tagged lead-in and lead-out moves when building the closed outline. Leads must not turn a recessed outline into a convex hull and leave oversized gaps elsewhere. Genuinely open contours (a tab gap, for example) still use the conservative hull/bounds fallback, but the hull is built from the material contour only: a cut-off may cross a lead, and hulling a lead's pierce point stopped cut-offs far short of the part. With a nonzero clearance, a line passing exactly through a corner of the clearance outline is trimmed to the outline like any other line. Automatic strip lines sit on a regular lattice and often land on such corners; they once fell back to the part's bounding box there, and the strip search preferred those lines because the box fallback measured as less cutting. The conservative curve/offset approximation can leave a small extra gap (about 0.0003 in on a straight edge at the default 0.02-in clearance), not an extra quarter inch.
|
||||||
|
|
||||||
If no safe separator fits inside the sheet, there is no retained-tail claim and the nominal cuts may cover the full sheet length. A proposed tail is not reported as separated unless its generated program spans the full sheet width. An active clamp blocks any proposed full-width line whose X coordinate falls in the clamp's whole footprint plus bottom-edge clearance, including a nominal grid line or an equivalent existing line. A tolerance-close existing line is tested at its own X as well as the proposed X, because that existing line is what would be certified. A same-line manual cut with different limits still blocks the whole plan, even on a clamp. Blocked lines are skipped with a clamp diagnostic; a blocked tail also reports `NoSafeTailSeparator` rather than claiming separation. Removed clamps on a sheet are absent from its active zones and do not block cuts. Stored zones are relative to the sheet's lower-left corner; the planner maps them into the plate's quadrant frame (the same frame as part and cut-off coordinates) before comparing, so the rule holds in all four quadrants. Obstructed or minimum-length-filtered lines produce diagnostics; they are not proof of a completed partition.
|
If no safe separator fits inside the sheet, there is no retained-tail claim and the nominal cuts may cover the full sheet length. A proposed tail is not reported as separated unless its generated program spans the full sheet width. An active clamp blocks any proposed full-width line whose X coordinate falls in the clamp's whole footprint plus its clamp clearance, including a nominal grid line or an equivalent existing line. A tolerance-close existing line is tested at its own X as well as the proposed X, because that existing line is what would be certified. A same-line manual cut with different limits still blocks the whole plan, even on a clamp. Blocked lines are skipped with a clamp diagnostic; a blocked tail also reports `NoSafeTailSeparator` rather than claiming separation. Removed clamps on a sheet are absent from its active zones and do not block cuts. Stored zones are relative to the sheet's lower-left corner; the planner maps them into the plate's quadrant frame (the same frame as part and cut-off coordinates) before comparing, so the rule holds in all four quadrants. Obstructed or minimum-length-filtered lines produce diagnostics; they are not proof of a completed partition.
|
||||||
|
|
||||||
For a 120-by-81-inch sheet whose parts extend through 80 inches, the fixed grid would put skeleton lines at 35 and 70 inches, followed by a separator beyond 80 inches at the required margin. With a 17.5-inch minimum strip, the planner can move those two lines toward the origin to shorten their trimmed cut segments. The separator does not move, and there is no line at 105 inches through the retained tail.
|
For a 120-by-81-inch sheet whose parts extend through 80 inches, the fixed grid would put skeleton lines at 35 and 70 inches, followed by a separator beyond 80 inches at the required margin. With a 17.5-inch minimum strip, the planner can move those two lines toward the origin to shorten their trimmed cut segments. The separator does not move, and there is no line at 105 inches through the retained tail.
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ For a 96-by-48-inch sheet with parts through Y 23 and a horizontal cut-off at Y
|
|||||||
|
|
||||||
The preview searches line positions together to minimize the total emitted strip-line cutting length after part clearance and minimum-segment filtering. Equal lengths favor fewer pierces, then less total movement from the fixed grid. The number of new lines in each span stays at the fixed-grid count; it never adds cuts to chase a shorter path. For example, a part edge at 34.75 inches can make a 35-inch line run beside much of the part, while a small shift toward the origin lets that line cross the part and skip its material. The preview reports the measured strip-cut length and the corresponding fixed-grid length, including when Minimum strip equals spacing; a `ShiftedCut` notice identifies moved lines. Because the lines are optimized together, an individual line can cut slightly more while the total falls; its notice reports that local increase rather than a negative saving.
|
The preview searches line positions together to minimize the total emitted strip-line cutting length after part clearance and minimum-segment filtering. Equal lengths favor fewer pierces, then less total movement from the fixed grid. The number of new lines in each span stays at the fixed-grid count; it never adds cuts to chase a shorter path. For example, a part edge at 34.75 inches can make a 35-inch line run beside much of the part, while a small shift toward the origin lets that line cross the part and skip its material. The preview reports the measured strip-cut length and the corresponding fixed-grid length, including when Minimum strip equals spacing; a `ShiftedCut` notice identifies moved lines. Because the lines are optimized together, an individual line can cut slightly more while the total falls; its notice reports that local increase rather than a negative saving.
|
||||||
|
|
||||||
Existing full-width vertical cut-offs inside the used span become fixed strip boundaries; the planner fills only gaps longer than the spacing, never moving or duplicating the manual cut. A partial manual cut is not a boundary and its position is avoided. Active clamps exclude candidate positions including bottom-edge clearance. If a gap cannot place all its lines outside clamps, it falls back to nominal lines and reports any blocked cuts under the existing guard. An unchanged rerun adds nothing. The desktop defaults to shifting; API callers that omit `MinimumStripLength` retain the fixed-grid behavior.
|
Existing full-width vertical cut-offs inside the used span become fixed strip boundaries; the planner fills only gaps longer than the spacing, never moving or duplicating the manual cut. A partial manual cut is not a boundary and its position is avoided. Active clamps exclude candidate positions including their clamp clearance. If a gap cannot place all its lines outside clamps, it falls back to nominal lines and reports any blocked cuts under the existing guard. An unchanged rerun adds nothing. The desktop defaults to shifting; API callers that omit `MinimumStripLength` retain the fixed-grid behavior.
|
||||||
|
|
||||||
## Limits and operator review
|
## Limits and operator review
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,12 @@ the saved `clamps` load either way. A plate that follows a rail other than the n
|
|||||||
saves it in its own optional `clampRail` (same shape), so its removal state and
|
saves it in its own optional `clampRail` (same shape), so its removal state and
|
||||||
time survive a reload; plates following the default rail omit it.
|
time survive a reload; plates following the default rail omit it.
|
||||||
|
|
||||||
|
The rail's optional `clearance` is the gap parts keep from every clamp footprint,
|
||||||
|
separate from part spacing, so a clamp drawn as a support block under the sheet can
|
||||||
|
use close to zero. It is omitted when the rail uses the sheet's bottom edge
|
||||||
|
spacing, which is also what a missing, negative or non-finite value (and an older
|
||||||
|
reader) uses.
|
||||||
|
|
||||||
An empty plate (no parts, no cut-offs) is omitted from the file unless that
|
An empty plate (no parts, no cut-offs) is omitted from the file unless that
|
||||||
would lose clamp zones: it is omitted like any other empty plate only when a
|
would lose clamp zones: it is omitted like any other empty plate only when a
|
||||||
new plate from the saved defaults would recreate it (same size, quadrant, edge
|
new plate from the saved defaults would recreate it (same size, quadrant, edge
|
||||||
|
|||||||
@@ -185,8 +185,9 @@ obstacle, so a plug-in that ignores the outline is caught as any other invalid l
|
|||||||
## Rectangular clamp zones
|
## Rectangular clamp zones
|
||||||
|
|
||||||
Whole-job Rectangles and Irregular exclude each stock's rectangular clamp zones,
|
Whole-job Rectangles and Irregular exclude each stock's rectangular clamp zones,
|
||||||
expanded on all sides by its bottom edge spacing. Part spacing is not added to clamp
|
expanded on all sides by its clamp clearance (`NestPlateStock.ClampClearance`: the rail's
|
||||||
clearance. Default runs and validates both lanes as usual. Empty clamp collections
|
own `Clearance`, else the bottom edge spacing). Part spacing is not added to clamp
|
||||||
|
clearance; interactive fills, pushes and the layout check use the same clearance. Default runs and validates both lanes as usual. Empty clamp collections
|
||||||
leave the existing packing path unchanged; clamps never become requested or placed parts.
|
leave the existing packing path unchanged; clamps never become requested or placed parts.
|
||||||
|
|
||||||
Every `Plate`, including plates the engine builds from stock, stores clamp zones relative
|
Every `Plate`, including plates the engine builds from stock, stores clamp zones relative
|
||||||
@@ -258,7 +259,7 @@ reporting progress, and Default discards it at almost no cost. Holes are not use
|
|||||||
rings come from the cutout pre-pass, which hands the engine each nested chain as its outer frame.
|
rings come from the cutout pre-pass, which hands the engine each nested chain as its outer frame.
|
||||||
|
|
||||||
Each disc goes, lower priority number first and then largest first, to the best feasible center
|
Each disc goes, lower priority number first and then largest first, to the best feasible center
|
||||||
tangent to two of: the work-area walls, placed discs, and clamp zones grown by the bottom-edge
|
tangent to two of: the work-area walls, placed discs, and clamp zones grown by the clamp
|
||||||
clearance (square-cornered, as the layout check grows them). Discs keep the part spacing plus
|
clearance (square-cornered, as the layout check grows them). Discs keep the part spacing plus
|
||||||
twice `NestTolerances.ValidationOutline` between them. Bottom-left fills rows and left-bottom
|
twice `NestTolerances.ValidationOutline` between them. Bottom-left fills rows and left-bottom
|
||||||
fills columns; each also runs seeded with a hexagonal lattice of the type with the most remaining
|
fills columns; each also runs seeded with a hexagonal lattice of the type with the most remaining
|
||||||
|
|||||||
Reference in new issue
Block a user