diff --git a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs index e003f8ae..b7e8ef70 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiFeatureWriterTests.cs @@ -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() { diff --git a/OpenNest.Tests/Cincinnati/CincinnatiPostRegressionTests.cs b/OpenNest.Tests/Cincinnati/CincinnatiPostRegressionTests.cs index bfa9964f..1153a4fd 100644 --- a/OpenNest.Tests/Cincinnati/CincinnatiPostRegressionTests.cs +++ b/OpenNest.Tests/Cincinnati/CincinnatiPostRegressionTests.cs @@ -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)); } diff --git a/OpenNest.Tests/Cincinnati/HoleCallOutputCharacterizationTests.cs b/OpenNest.Tests/Cincinnati/HoleCallOutputCharacterizationTests.cs index 006d035e..8a142df2 100644 --- a/OpenNest.Tests/Cincinnati/HoleCallOutputCharacterizationTests.cs +++ b/OpenNest.Tests/Cincinnati/HoleCallOutputCharacterizationTests.cs @@ -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, }; /// diff --git a/Posts/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs b/Posts/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs index 0209644a..0f6ab78e 100644 --- a/Posts/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs +++ b/Posts/OpenNest.Posts.Cincinnati/CincinnatiFeatureWriter.cs @@ -18,12 +18,6 @@ public sealed class FeatureContext public bool IsFirstFeatureOfPart { get; set; } public bool IsLastFeatureOnSheet { get; set; } - /// - /// True when the rapid after this feature goes to a different part, so its head - /// raise is the safety head raise. - /// - public bool IsSafetyHeadraise { get; set; } - /// True when this feature cuts the part's exterior (selects ExteriorM47). public bool IsExteriorFeature { get; set; } public bool IsEtch { get; set; } @@ -32,8 +26,8 @@ public sealed class FeatureContext public double SheetDiagonal { get; set; } /// - /// Rapid travel from this feature's end to the next pierce, used by - /// . Null when unknown. + /// Rapid travel from this feature's end to the next pierce, used by the safety + /// head raise and . Null when unknown. /// 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); } diff --git a/Posts/OpenNest.Posts.Cincinnati/CincinnatiHeadRaise.cs b/Posts/OpenNest.Posts.Cincinnati/CincinnatiHeadRaise.cs index 173f82d7..171fbf21 100644 --- a/Posts/OpenNest.Posts.Cincinnati/CincinnatiHeadRaise.cs +++ b/Posts/OpenNest.Posts.Cincinnati/CincinnatiHeadRaise.cs @@ -5,19 +5,13 @@ namespace OpenNest.Posts.Cincinnati; /// internal static class CincinnatiHeadRaise { - /// True when the rapid goes to a different part. /// True when the feature just cut is a part exterior. /// Travel to the next pierce; null when unknown. /// The block to write, or null for no head raise. - 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 }; } + /// + /// True when the rapid is long enough to cross warped plate, so it takes the + /// safety head raise. Unknown travel never does. + /// + 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 diff --git a/Posts/OpenNest.Posts.Cincinnati/CincinnatiHoleCallWriter.cs b/Posts/OpenNest.Posts.Cincinnati/CincinnatiHoleCallWriter.cs index 8a077081..bce1177b 100644 --- a/Posts/OpenNest.Posts.Cincinnati/CincinnatiHoleCallWriter.cs +++ b/Posts/OpenNest.Posts.Cincinnati/CincinnatiHoleCallWriter.cs @@ -17,9 +17,9 @@ internal static class CincinnatiHoleCallWriter /// Unmapped calls fall back to the call's own id. /// /// Part-name comment to emit when the call opens a part, else null. - /// - /// True when the call is a part's first feature after another part, matching the - /// feature writer's safety head raise. + /// + /// Travel from the call to the next pierce, which selects the safety head raise; + /// null when unknown. /// internal static void Write( TextWriter w, @@ -31,7 +31,7 @@ internal static class CincinnatiHoleCallWriter CoordinateFormatter fmt, IReadOnlyDictionary 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"); } diff --git a/Posts/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs b/Posts/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs index f409ab2c..ac7275e4 100644 --- a/Posts/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs +++ b/Posts/OpenNest.Posts.Cincinnati/CincinnatiPartSubprogramWriter.cs @@ -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, diff --git a/Posts/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs b/Posts/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs index 0d7fd7e6..d71b3a3e 100644 --- a/Posts/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs +++ b/Posts/OpenNest.Posts.Cincinnati/CincinnatiPostConfig.cs @@ -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 before the rapid to a different part. Null = disabled." + "Safety head raise distance in machine units, posted as M47 P 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).")] diff --git a/Posts/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs b/Posts/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs index 959f5f6b..a532bafb 100644 --- a/Posts/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs +++ b/Posts/OpenNest.Posts.Cincinnati/CincinnatiSheetWriter.cs @@ -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); diff --git a/Posts/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.json b/Posts/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.json index e16ad7f6..34c4d1cc 100644 --- a/Posts/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.json +++ b/Posts/OpenNest.Posts.Cincinnati/OpenNest.Posts.Cincinnati.json @@ -20,6 +20,7 @@ "ExteriorM47": "Always", "M47OverrideDistanceThreshold": null, "SafetyHeadraiseDistance": 2000, + "SafetyHeadraiseRapidDistance": 12.0, "PalletExchange": "EndOfSheet", "LeadInFeedratePercent": 0.5, "LeadInArcLine2FeedratePercent": 0.5, diff --git a/docs/cincinnati-post-output.md b/docs/cincinnati-post-output.md index ab77a575..3e640b42 100644 --- a/docs/cincinnati-post-output.md +++ b/docs/cincinnati-post-output.md @@ -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 (Safety Headraise)` when the next feature - belongs to a part with a different drawing name and the distance is set. +1. `M47 P (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. `` 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