mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 10:11:35 -05:00
feat(posts): trigger the Cincinnati safety head raise by rapid length
The safety raise (M47 P<SafetyHeadraiseDistance>) used to fire only when the next part had a different drawing name, which has nothing to do with crash risk: a long rapid between copies of the same part got a plain M47, while a short hop between neighbouring different drawings got the full raise. Sheets are rarely flat (5/8" plate can be ~1.5" out of flat within supplier tolerance), and longer rapids cross more of that bow. Add SafetyHeadraiseRapidDistance (program units, default 12"): any rapid to the next pierce longer than it takes the safety raise, regardless of drawing. Unknown travel never does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
efa4844049
commit
abfc1a714a
11 files changed
+77
-51
No files matched your search
@@ -36,7 +36,6 @@ public class CincinnatiFeatureWriterTests
|
||||
PartName = "BRACKET",
|
||||
IsFirstFeatureOfPart = true,
|
||||
IsLastFeatureOnSheet = false,
|
||||
IsSafetyHeadraise = false,
|
||||
IsExteriorFeature = false,
|
||||
LibraryFile = "MILD10",
|
||||
CutDistance = 18.0,
|
||||
@@ -433,14 +432,32 @@ public class CincinnatiFeatureWriterTests
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.SafetyHeadraiseDistance = 2000;
|
||||
config.SafetyHeadraiseRapidDistance = 6;
|
||||
var ctx = SimpleContext();
|
||||
ctx.IsSafetyHeadraise = true;
|
||||
ctx.RapidDistance = 6.5;
|
||||
ctx.IsLastFeatureOnSheet = false;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.Contains("M47 P2000 (Safety Headraise)", output);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(6.0)]
|
||||
[InlineData(null)]
|
||||
public void SafetyHeadraise_SkipsRapidsNotLongerThanThreshold(double? rapidDistance)
|
||||
{
|
||||
var config = DefaultConfig();
|
||||
config.SafetyHeadraiseDistance = 2000;
|
||||
config.SafetyHeadraiseRapidDistance = 6;
|
||||
var ctx = SimpleContext();
|
||||
ctx.RapidDistance = rapidDistance;
|
||||
ctx.IsLastFeatureOnSheet = false;
|
||||
var output = WriteFeature(config, ctx);
|
||||
|
||||
Assert.DoesNotContain("Safety Headraise", output);
|
||||
Assert.Contains("M47", output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExteriorM47Mode_UsesExteriorConfig()
|
||||
{
|
||||
|
||||
@@ -81,17 +81,22 @@ public class CincinnatiPostRegressionTests
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void SafetyHeadraise_IsTheOneRaiseBeforeTheRapidToADifferentPart(bool partSubprograms)
|
||||
public void SafetyHeadraise_IsTheOneRaiseBeforeEachLongRapid(bool partSubprograms)
|
||||
{
|
||||
// The second A is a long rapid from the first despite sharing its drawing.
|
||||
var nest = SquaresNest(("A", new Vector(5, 5)), ("A", new Vector(20, 5)), ("B", new Vector(40, 30)));
|
||||
var b = nest.Plates[0].Parts[2];
|
||||
var parts = nest.Plates[0].Parts;
|
||||
var config = Config(partSubprograms);
|
||||
config.SafetyHeadraiseRapidDistance = 6;
|
||||
|
||||
var run = Run(Post(nest, partSubprograms));
|
||||
var run = Run(Post(nest, config));
|
||||
|
||||
Assert.Equal(0, run.DoubleRaises);
|
||||
Assert.Equal(0, run.HeadDownRapids);
|
||||
var safety = Assert.Single(run.PierceLog, p => p.Raise == "M47 P2000");
|
||||
Assert.True(Near(safety.Point, b.Location + new Vector(2, 2)), $"Safety raise before {safety.Point}");
|
||||
var safety = run.PierceLog.Where(p => p.Raise == "M47 P2000").ToList();
|
||||
Assert.Equal(2, safety.Count);
|
||||
Assert.True(Near(safety[0].Point, parts[1].Location + new Vector(2, 2)), $"Safety raise before {safety[0].Point}");
|
||||
Assert.True(Near(safety[1].Point, parts[2].Location + new Vector(2, 2)), $"Safety raise before {safety[1].Point}");
|
||||
Assert.All(run.PierceLog.Where(p => p.Raise != "M47 P2000"), p => Assert.Equal("M47", p.Raise));
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,8 @@ public class HoleCallOutputCharacterizationTests
|
||||
PalletExchange = PalletMode.None,
|
||||
KerfCompensation = KerfMode.PreApplied,
|
||||
UseAntiDive = false,
|
||||
// Pins plain raises; the safety raise has its own tests.
|
||||
SafetyHeadraiseRapidDistance = null,
|
||||
};
|
||||
|
||||
/// <param name="mapping">
|
||||
|
||||
@@ -18,12 +18,6 @@ public sealed class FeatureContext
|
||||
public bool IsFirstFeatureOfPart { get; set; }
|
||||
public bool IsLastFeatureOnSheet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True when the rapid after this feature goes to a different part, so its head
|
||||
/// raise is the safety head raise.
|
||||
/// </summary>
|
||||
public bool IsSafetyHeadraise { get; set; }
|
||||
|
||||
/// <summary>True when this feature cuts the part's exterior (selects ExteriorM47).</summary>
|
||||
public bool IsExteriorFeature { get; set; }
|
||||
public bool IsEtch { get; set; }
|
||||
@@ -32,8 +26,8 @@ public sealed class FeatureContext
|
||||
public double SheetDiagonal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rapid travel from this feature's end to the next pierce, used by
|
||||
/// <see cref="M47Mode.Auto"/>. Null when unknown.
|
||||
/// Rapid travel from this feature's end to the next pierce, used by the safety
|
||||
/// head raise and <see cref="M47Mode.Auto"/>. Null when unknown.
|
||||
/// </summary>
|
||||
public double? RapidDistance { get; set; }
|
||||
|
||||
@@ -332,12 +326,7 @@ public sealed class CincinnatiFeatureWriter
|
||||
|
||||
private void WriteM47(TextWriter writer, FeatureContext ctx)
|
||||
{
|
||||
var line = CincinnatiHeadRaise.Line(
|
||||
_config,
|
||||
ctx.IsSafetyHeadraise,
|
||||
ctx.IsExteriorFeature,
|
||||
ctx.RapidDistance
|
||||
);
|
||||
var line = CincinnatiHeadRaise.Line(_config, ctx.IsExteriorFeature, ctx.RapidDistance);
|
||||
if (line != null)
|
||||
writer.WriteLine(line);
|
||||
}
|
||||
|
||||
@@ -5,19 +5,13 @@ namespace OpenNest.Posts.Cincinnati;
|
||||
/// </summary>
|
||||
internal static class CincinnatiHeadRaise
|
||||
{
|
||||
/// <param name="isSafetyHeadraise">True when the rapid goes to a different part.</param>
|
||||
/// <param name="isExterior">True when the feature just cut is a part exterior.</param>
|
||||
/// <param name="rapidDistance">Travel to the next pierce; null when unknown.</param>
|
||||
/// <returns>The block to write, or null for no head raise.</returns>
|
||||
internal static string Line(
|
||||
CincinnatiPostConfig config,
|
||||
bool isSafetyHeadraise,
|
||||
bool isExterior,
|
||||
double? rapidDistance
|
||||
)
|
||||
internal static string Line(CincinnatiPostConfig config, bool isExterior, double? rapidDistance)
|
||||
{
|
||||
if (isSafetyHeadraise && config.SafetyHeadraiseDistance.HasValue)
|
||||
return $"M47 P{config.SafetyHeadraiseDistance.Value} (Safety Headraise)";
|
||||
if (IsSafetyRapid(config, rapidDistance))
|
||||
return SafetyLine(config);
|
||||
|
||||
var mode = isExterior ? config.ExteriorM47 : config.InteriorM47;
|
||||
return mode switch
|
||||
@@ -29,6 +23,19 @@ internal static class CincinnatiHeadRaise
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True when the rapid is long enough to cross warped plate, so it takes the
|
||||
/// safety head raise. Unknown travel never does.
|
||||
/// </summary>
|
||||
internal static bool IsSafetyRapid(CincinnatiPostConfig config, double? rapidDistance) =>
|
||||
config.SafetyHeadraiseDistance.HasValue
|
||||
&& config.SafetyHeadraiseRapidDistance is { } threshold
|
||||
&& rapidDistance is { } distance
|
||||
&& distance > threshold;
|
||||
|
||||
internal static string SafetyLine(CincinnatiPostConfig config) =>
|
||||
$"M47 P{config.SafetyHeadraiseDistance.Value} (Safety Headraise)";
|
||||
|
||||
// Unknown travel, or no threshold configured, always gets the full raise.
|
||||
private static bool IsLongRapid(CincinnatiPostConfig config, double? rapidDistance) =>
|
||||
config.M47OverrideDistanceThreshold is not { } threshold
|
||||
|
||||
@@ -17,9 +17,9 @@ internal static class CincinnatiHoleCallWriter
|
||||
/// Unmapped calls fall back to the call's own id.
|
||||
/// </param>
|
||||
/// <param name="partName">Part-name comment to emit when the call opens a part, else null.</param>
|
||||
/// <param name="isSafetyHeadraise">
|
||||
/// True when the call is a part's first feature after another part, matching the
|
||||
/// feature writer's safety head raise.
|
||||
/// <param name="rapidDistance">
|
||||
/// Travel from the call to the next pierce, which selects the safety head raise;
|
||||
/// null when unknown.
|
||||
/// </param>
|
||||
internal static void Write(
|
||||
TextWriter w,
|
||||
@@ -31,7 +31,7 @@ internal static class CincinnatiHoleCallWriter
|
||||
CoordinateFormatter fmt,
|
||||
IReadOnlyDictionary<Program, int> holeSubprograms,
|
||||
string partName = null,
|
||||
bool isSafetyHeadraise = false
|
||||
double? rapidDistance = null
|
||||
)
|
||||
{
|
||||
var postSubNum =
|
||||
@@ -66,8 +66,8 @@ internal static class CincinnatiHoleCallWriter
|
||||
if (isLastFeature)
|
||||
return;
|
||||
|
||||
if (isSafetyHeadraise && config.SafetyHeadraiseDistance.HasValue)
|
||||
w.WriteLine($"M47 P{config.SafetyHeadraiseDistance.Value} (Safety Headraise)");
|
||||
if (CincinnatiHeadRaise.IsSafetyRapid(config, rapidDistance))
|
||||
w.WriteLine(CincinnatiHeadRaise.SafetyLine(config));
|
||||
else
|
||||
w.WriteLine("M47");
|
||||
}
|
||||
|
||||
@@ -89,7 +89,6 @@ public sealed class CincinnatiPartSubprogramWriter
|
||||
PartName = drawingName,
|
||||
IsFirstFeatureOfPart = false,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsSafetyHeadraise = false,
|
||||
IsExteriorFeature = exterior[i],
|
||||
IsEtch = isEtch,
|
||||
LibraryFile = isEtch ? etchLibrary : cutLibrary,
|
||||
|
||||
@@ -180,10 +180,17 @@ namespace OpenNest.Posts.Cincinnati
|
||||
[Category("7. Safety")]
|
||||
[DisplayName("Safety Headraise Distance")]
|
||||
[Description(
|
||||
"Safety head raise distance in machine units, posted as M47 P<distance> before the rapid to a different part. Null = disabled."
|
||||
"Safety head raise distance in machine units, posted as M47 P<distance> before rapids longer than the safety headraise rapid distance. Null = disabled."
|
||||
)]
|
||||
public int? SafetyHeadraiseDistance { get; set; } = 2000;
|
||||
|
||||
[Category("7. Safety")]
|
||||
[DisplayName("Safety Headraise Rapid Distance")]
|
||||
[Description(
|
||||
"Rapids to the next pierce longer than this get the safety head raise instead of the M47 mode's raise, so the head clears warped plate. Program units. Null = disabled."
|
||||
)]
|
||||
public double? SafetyHeadraiseRapidDistance { get; set; } = 12.0;
|
||||
|
||||
[Category("8. Pallet")]
|
||||
[DisplayName("Pallet Exchange")]
|
||||
[Description("When pallet exchange occurs (None, EndOfSheet, or StartAndEnd).")]
|
||||
|
||||
@@ -210,9 +210,8 @@ public sealed class CincinnatiSheetWriter
|
||||
var next = i + 1 < items.Count ? items[i + 1] : null;
|
||||
var isLastFeature = next == null;
|
||||
|
||||
// A head raise belongs before the rapid that leaves this item, so the safety
|
||||
// raise goes on a part's last item when a different part follows.
|
||||
var isSafetyHeadraise = next != null && next.Part.BaseDrawing.Name != partName;
|
||||
// A head raise belongs before the rapid that leaves this item; its travel
|
||||
// decides whether that raise is the safety raise.
|
||||
double? rapidDistance =
|
||||
next == null
|
||||
? null
|
||||
@@ -228,7 +227,6 @@ public sealed class CincinnatiSheetWriter
|
||||
item,
|
||||
i,
|
||||
partName,
|
||||
isSafetyHeadraise,
|
||||
rapidDistance,
|
||||
isLastFeature
|
||||
);
|
||||
@@ -250,7 +248,7 @@ public sealed class CincinnatiSheetWriter
|
||||
_fmt,
|
||||
_holeSubprograms,
|
||||
isFirstFeatureOfPart ? partName : null,
|
||||
isSafetyHeadraise
|
||||
rapidDistance
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -262,7 +260,6 @@ public sealed class CincinnatiSheetWriter
|
||||
PartName = partName,
|
||||
IsFirstFeatureOfPart = isFirstFeatureOfPart,
|
||||
IsLastFeatureOnSheet = isLastFeature,
|
||||
IsSafetyHeadraise = isSafetyHeadraise,
|
||||
IsExteriorFeature = item.IsExterior,
|
||||
IsEtch = item.IsEtch,
|
||||
LibraryFile = item.IsEtch ? etchLibrary : cutLibrary,
|
||||
@@ -296,7 +293,6 @@ public sealed class CincinnatiSheetWriter
|
||||
SheetItem item,
|
||||
int index,
|
||||
string partName,
|
||||
bool isSafetyHeadraise,
|
||||
double? rapidDistance,
|
||||
bool isLastPart
|
||||
)
|
||||
@@ -325,7 +321,7 @@ public sealed class CincinnatiSheetWriter
|
||||
|
||||
// One head raise covers the rapid back and the rapid on. The part is complete,
|
||||
// so the raise follows the exterior M47 mode.
|
||||
var raise = CincinnatiHeadRaise.Line(_config, isSafetyHeadraise, true, rapidDistance);
|
||||
var raise = CincinnatiHeadRaise.Line(_config, true, rapidDistance);
|
||||
if (raise != null)
|
||||
w.WriteLine(raise);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"ExteriorM47": "Always",
|
||||
"M47OverrideDistanceThreshold": null,
|
||||
"SafetyHeadraiseDistance": 2000,
|
||||
"SafetyHeadraiseRapidDistance": 12.0,
|
||||
"PalletExchange": "EndOfSheet",
|
||||
"LeadInFeedratePercent": 0.5,
|
||||
"LeadInArcLine2FeedratePercent": 0.5,
|
||||
|
||||
@@ -115,8 +115,11 @@ Every raise is written at the end of the feature it follows, before the rapid
|
||||
that leaves it, and there is exactly one raise per rapid. The block is chosen
|
||||
in this order (`CincinnatiHeadRaise`):
|
||||
|
||||
1. `M47 P<SafetyHeadraiseDistance> (Safety Headraise)` when the next feature
|
||||
belongs to a part with a different drawing name and the distance is set.
|
||||
1. `M47 P<SafetyHeadraiseDistance> (Safety Headraise)` when the rapid travel to
|
||||
the next pierce exceeds `SafetyHeadraiseRapidDistance` (program units, default
|
||||
12) and both settings are set. Long rapids cross more of a warped sheet, so
|
||||
the head goes higher; which part or drawing comes next does not matter.
|
||||
Unknown travel (a hole call's rapid inside a part sub-program) never takes it.
|
||||
2. Otherwise the M47 mode for the feature just cut: `ExteriorM47` after a
|
||||
part's exterior, `InteriorM47` after anything else.
|
||||
- `Always`: `M47`.
|
||||
@@ -187,9 +190,9 @@ M47 ; head raise unless this is the last feature (§3.
|
||||
|
||||
`<hole>` is the hole center in the calling program's frame: the part location
|
||||
plus the call offset in a sheet sub-program, or the call offset alone inside a
|
||||
part sub-program (whose frame is already the part's). A part that opens with a
|
||||
hole call takes its safety head raise on that call's `M47`, as an ordinary
|
||||
first feature would.
|
||||
part sub-program (whose frame is already the part's). A hole call followed by
|
||||
a long rapid takes the safety head raise on that call's `M47`, as an ordinary
|
||||
feature would.
|
||||
|
||||
G52 specifies the new origin in the current work coordinate system and — per
|
||||
§1.52 — "does not move the cutting nozzle". The hole sub-program is written
|
||||
|
||||
Reference in new issue
Block a user