Compare commits

...

13 Commits

Author SHA1 Message Date
aj 5a9a06a6a0 feat: allow re-selecting parts with existing lead-ins and use magenta preview
Remove LeadInsLocked guard so parts can be re-selected for lead-in
re-placement. Change preview color from yellow to magenta for better
visibility against the cyan contour highlight.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:37:31 -04:00
aj c1f1c829dc fix: flip ComputeNormal for CCW arcs on concave contour features
CCW arcs (e.g. the top of a U-slot) had the radial normal pointing
into the part material instead of into the scrap. This caused the
lead-in preview to flip sides on concave features.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 17:37:26 -04:00
aj e8fe01aea2 feat: highlight hovered contour during lead-in placement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:30:40 -04:00
aj 7b7d2cd8d1 feat: track hovered contour during lead-in mouse move
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:28:55 -04:00
aj 6ca0e9da92 feat: gray overlay on all parts when ActionLeadIn is active
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:25:26 -04:00
aj bcaa4a03ee feat: show post processor config dialog before save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 12:08:44 -04:00
aj 54c6f1bc89 feat: add PostProcessorConfigForm with PropertyGrid
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 12:07:15 -04:00
aj 429e4b63e1 feat: add PropertyGrid attributes to CincinnatiPostConfig
Decorate all properties with [Category], [DisplayName], and [Description]
attributes for use in the WinForms PropertyGrid config dialog. Reorder
properties to match category grouping (1. Output through B. Libraries)
and replace property-level XML doc comments with the attribute descriptions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 12:05:16 -04:00
aj 159b54a1ec feat: add IConfigurablePostProcessor interface and implement in Cincinnati post
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 12:02:07 -04:00
aj 568539d5b1 fix: offset inline feature coordinates by part location for G90 absolute mode
Part.Program stores coordinates relative to the part's own origin, but
the Cincinnati post processor emits G90 (absolute positioning). Inline
features were writing part-relative coordinates directly without adding
Part.Location, producing incorrect output. Sub-program mode was
unaffected because it uses G92 to set up local coordinate systems.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:50:43 -04:00
aj d7fa4bef43 feat: implement tab support in ContourCuttingStrategy
When TabsEnabled is set, trims the end of each contour using a circle
centered at the lead-in point with radius equal to the tab size. The
uncut gap between the trim point and the contour start keeps the part
connected to the sheet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:40:29 -04:00
aj 7c58cfa749 fix: correct lead-in approach angle formula mirroring pierce point
The offset direction (start→pierce) is reversed from the approach
direction (pierce→start), so the old formula produced 180°−angle
instead of the requested angle. Invisible at the 90° default but
caused 45° to render as 135°.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:01:18 -04:00
aj 525cbc6f12 fix: draw cut direction arrows as chevron lines instead of filled triangles
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 08:52:33 -04:00
15 changed files with 548 additions and 230 deletions
@@ -1,4 +1,5 @@
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
namespace OpenNest.CNC.CuttingStrategy
@@ -59,9 +60,18 @@ namespace OpenNest.CNC.CuttingStrategy
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
var reindexed = cutout.ReindexAt(closestPt, entity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
{
var trimmed = TrimShapeForTab(reindexed, closestPt, Parameters.TabConfig.Size);
result.Codes.AddRange(ConvertShapeToMoves(trimmed, closestPt));
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
}
else
{
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
result.Codes.AddRange(leadOut.Generate(closestPt, normal, winding));
}
currentPoint = closestPt;
}
@@ -80,9 +90,18 @@ namespace OpenNest.CNC.CuttingStrategy
result.Codes.AddRange(leadIn.Generate(perimeterPt, normal, winding));
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
// TODO: MicrotabLeadOut — trim last cutting move by GapSize
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
if (Parameters.TabsEnabled && Parameters.TabConfig != null)
{
var trimmed = TrimShapeForTab(reindexed, perimeterPt, Parameters.TabConfig.Size);
result.Codes.AddRange(ConvertShapeToMoves(trimmed, perimeterPt));
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
}
else
{
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
result.Codes.AddRange(leadOut.Generate(perimeterPt, normal, winding));
}
}
// Convert to incremental mode to match the convention used by
@@ -150,6 +169,12 @@ namespace OpenNest.CNC.CuttingStrategy
{
// Radial direction from center to point
normal = point.AngleFrom(arc.Center);
// For CCW arcs the radial points the wrong way — flip it.
// CW arcs are convex features (corners) where radial = outward.
// CCW arcs are concave features (slots) where radial = inward.
if (arc.Rotation == RotationType.CCW)
normal += System.Math.PI;
}
else if (entity is Circle circle)
{
@@ -238,6 +263,70 @@ namespace OpenNest.CNC.CuttingStrategy
};
}
private static Shape TrimShapeForTab(Shape shape, Vector center, double tabSize)
{
var tabCircle = new Circle(center, tabSize);
var entities = new List<Entity>(shape.Entities);
// Trim end: walk backward removing entities inside the tab circle
while (entities.Count > 0)
{
var entity = entities[entities.Count - 1];
if (entity.Intersects(tabCircle, out var pts) && pts.Count > 0)
{
// Find intersection furthest from center (furthest along path from end)
var best = pts[0];
var bestDist = best.DistanceTo(center);
for (var j = 1; j < pts.Count; j++)
{
var dist = pts[j].DistanceTo(center);
if (dist > bestDist)
{
best = pts[j];
bestDist = dist;
}
}
if (entity is Line line)
{
var (first, _) = line.SplitAt(best);
entities.RemoveAt(entities.Count - 1);
if (first != null)
entities.Add(first);
}
else if (entity is Arc arc)
{
var (first, _) = arc.SplitAt(best);
entities.RemoveAt(entities.Count - 1);
if (first != null)
entities.Add(first);
}
break;
}
// No intersection — entity is entirely inside circle, remove it
if (EntityStartPoint(entity).DistanceTo(center) <= tabSize + Tolerance.Epsilon)
{
entities.RemoveAt(entities.Count - 1);
continue;
}
break;
}
var result = new Shape();
result.Entities.AddRange(entities);
return result;
}
private static Vector EntityStartPoint(Entity entity)
{
if (entity is Line line) return line.StartPoint;
if (entity is Arc arc) return arc.StartPoint();
return Vector.Zero;
}
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display)
{
var moves = new List<ICode>();
@@ -23,7 +23,7 @@ namespace OpenNest.CNC.CuttingStrategy
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
{
var approachAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle);
var approachAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle);
return new Vector(
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
@@ -16,7 +16,7 @@ namespace OpenNest.CNC.CuttingStrategy
{
var piercePoint = GetPiercePoint(contourStartPoint, contourNormalAngle);
var secondAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle1);
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
var midPoint = new Vector(
contourStartPoint.X + Length2 * System.Math.Cos(secondAngle),
contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle));
@@ -31,7 +31,7 @@ namespace OpenNest.CNC.CuttingStrategy
public override Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle)
{
var secondAngle = contourNormalAngle + Angle.HalfPI - Angle.ToRadians(ApproachAngle1);
var secondAngle = contourNormalAngle - Angle.HalfPI + Angle.ToRadians(ApproachAngle1);
var midX = contourStartPoint.X + Length2 * System.Math.Cos(secondAngle);
var midY = contourStartPoint.Y + Length2 * System.Math.Sin(secondAngle);
@@ -0,0 +1,9 @@
namespace OpenNest
{
public interface IConfigurablePostProcessor : IPostProcessor
{
object Config { get; }
void SaveConfig();
}
}
@@ -23,6 +23,12 @@ public sealed class FeatureContext
public string LibraryFile { get; set; } = "";
public double CutDistance { get; set; }
public double SheetDiagonal { get; set; }
/// <summary>
/// Part location on the plate. Added to all output X/Y coordinates
/// so part-relative programs become plate-absolute under G90.
/// </summary>
public Vector PartLocation { get; set; } = Vector.Zero;
}
/// <summary>
@@ -51,12 +57,13 @@ public sealed class CincinnatiFeatureWriter
var currentPos = Vector.Zero;
var lastFeedVar = "";
var kerfEmitted = false;
var offset = ctx.PartLocation;
// Find the pierce point from the first rapid move
var piercePoint = FindPiercePoint(ctx.Codes);
// 1. Rapid to pierce point (with line number if configured)
WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint);
WriteRapidToPierce(writer, ctx.FeatureNumber, piercePoint, offset);
// 2. Part name comment on first feature of each part
if (ctx.IsFirstFeatureOfPart && !string.IsNullOrEmpty(ctx.PartName))
@@ -105,7 +112,7 @@ public sealed class CincinnatiFeatureWriter
kerfEmitted = true;
}
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y)}");
sb.Append($"G1 X{_fmt.FormatCoord(linear.EndPoint.X + offset.X)} Y{_fmt.FormatCoord(linear.EndPoint.Y + offset.Y)}");
// Feedrate — etch always uses process feedrate
var feedVar = ctx.IsEtch ? "#148" : GetLinearFeedVariable(linear.Layer);
@@ -131,7 +138,7 @@ public sealed class CincinnatiFeatureWriter
// G2 = CW, G3 = CCW
var gCode = arc.Rotation == RotationType.CW ? "G2" : "G3";
sb.Append($"{gCode} X{_fmt.FormatCoord(arc.EndPoint.X)} Y{_fmt.FormatCoord(arc.EndPoint.Y)}");
sb.Append($"{gCode} X{_fmt.FormatCoord(arc.EndPoint.X + offset.X)} Y{_fmt.FormatCoord(arc.EndPoint.Y + offset.Y)}");
// Convert absolute center to incremental I/J
var i = arc.CenterPoint.X - currentPos.X;
@@ -188,14 +195,14 @@ public sealed class CincinnatiFeatureWriter
return Vector.Zero;
}
private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint)
private void WriteRapidToPierce(TextWriter writer, int featureNumber, Vector piercePoint, Vector offset)
{
var sb = new StringBuilder();
if (_config.UseLineNumbers)
sb.Append($"N{featureNumber} ");
sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X)} Y{_fmt.FormatCoord(piercePoint.Y)}");
sb.Append($"G0 X{_fmt.FormatCoord(piercePoint.X + offset.X)} Y{_fmt.FormatCoord(piercePoint.Y + offset.Y)}");
writer.WriteLine(sb.ToString());
}
+148 -185
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.ComponentModel;
namespace OpenNest.Posts.Cincinnati
{
@@ -92,205 +93,159 @@ namespace OpenNest.Posts.Cincinnati
/// </summary>
public sealed class CincinnatiPostConfig
{
/// <summary>
/// Gets or sets the configuration name/identifier.
/// Default: "CL940"
/// </summary>
[Category("1. Output")]
[DisplayName("Configuration Name")]
[Description("Configuration name/identifier (e.g. CL940).")]
public string ConfigurationName { get; set; } = "CL940";
/// <summary>
/// Gets or sets the units for posted output.
/// Default: Units.Inches
/// </summary>
[Category("1. Output")]
[DisplayName("Posted Units")]
[Description("Units for posted output (Inches or Millimeters).")]
public Units PostedUnits { get; set; } = Units.Inches;
/// <summary>
/// Gets or sets the decimal accuracy for numeric output.
/// Default: 4
/// </summary>
[Category("1. Output")]
[DisplayName("Decimal Accuracy")]
[Description("Number of decimal places for numeric output.")]
public int PostedAccuracy { get; set; } = 4;
/// <summary>
/// Gets or sets how coordinate positioning is handled between parts.
/// Default: CoordinateMode.G92
/// </summary>
public CoordinateMode CoordModeBetweenParts { get; set; } = CoordinateMode.G92;
/// <summary>
/// Gets or sets whether to use subprograms for sheet operations.
/// Default: true
/// </summary>
public bool UseSheetSubprograms { get; set; } = true;
/// <summary>
/// Gets or sets the starting subprogram number for sheet operations.
/// Default: 101
/// </summary>
public int SheetSubprogramStart { get; set; } = 101;
/// <summary>
/// Gets or sets whether to use M98 sub-programs for part geometry.
/// When enabled, each unique part geometry is written as a reusable sub-program
/// called via M98, reducing output size for nests with repeated parts.
/// Default: false
/// </summary>
public bool UsePartSubprograms { get; set; } = false;
/// <summary>
/// Gets or sets the starting sub-program number for part geometry sub-programs.
/// Default: 200
/// </summary>
public int PartSubprogramStart { get; set; } = 200;
/// <summary>
/// Gets or sets the subprogram number for variable declarations.
/// Default: 100
/// </summary>
public int VariableDeclarationSubprogram { get; set; } = 100;
/// <summary>
/// Gets or sets how G89 parameters are provided.
/// Default: G89Mode.LibraryFile
/// </summary>
public G89Mode ProcessParameterMode { get; set; } = G89Mode.LibraryFile;
/// <summary>
/// Gets or sets the default assist gas when Nest.AssistGas is empty.
/// Default: "O2"
/// </summary>
public string DefaultAssistGas { get; set; } = "O2";
/// <summary>
/// Gets or sets the gas used for etch operations.
/// Independent of the cutting assist gas — etch typically requires a specific gas.
/// Default: "N2"
/// </summary>
public string DefaultEtchGas { get; set; } = "N2";
/// <summary>
/// Gets or sets the material-to-library mapping for cut operations.
/// Each entry maps (material, thickness, gas) to a G89 library file.
/// </summary>
public List<MaterialLibraryEntry> MaterialLibraries { get; set; } = new();
/// <summary>
/// Gets or sets the gas-to-library mapping for etch operations.
/// Each entry maps a gas type to a G89 etch library file.
/// </summary>
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
/// <summary>
/// Gets or sets whether to use exact stop mode (G61).
/// Default: false
/// </summary>
public bool UseExactStopMode { get; set; } = false;
/// <summary>
/// Gets or sets where kerf compensation is applied.
/// Default: KerfMode.ControllerSide
/// </summary>
public KerfMode KerfCompensation { get; set; } = KerfMode.ControllerSide;
/// <summary>
/// Gets or sets the default side for kerf compensation.
/// Default: KerfSide.Left
/// </summary>
public KerfSide DefaultKerfSide { get; set; } = KerfSide.Left;
/// <summary>
/// Gets or sets how M47 is used in interior cuts.
/// Default: M47Mode.Always
/// </summary>
public M47Mode InteriorM47 { get; set; } = M47Mode.Always;
/// <summary>
/// Gets or sets how M47 is used in exterior cuts.
/// Default: M47Mode.Always
/// </summary>
public M47Mode ExteriorM47 { get; set; } = M47Mode.Always;
/// <summary>
/// Gets or sets the safety head raise distance (in machine units).
/// Default: 2000
/// </summary>
public int? SafetyHeadraiseDistance { get; set; } = 2000;
/// <summary>
/// Gets or sets the distance threshold for M47 override.
/// Default: null
/// </summary>
public double? M47OverrideDistanceThreshold { get; set; } = null;
/// <summary>
/// Gets or sets whether to use anti-dive functionality.
/// Default: true
/// </summary>
public bool UseAntiDive { get; set; } = true;
/// <summary>
/// Gets or sets whether to use smart rapids optimization.
/// Default: false
/// </summary>
public bool UseSmartRapids { get; set; } = false;
/// <summary>
/// Gets or sets when pallet exchange occurs.
/// Default: PalletMode.EndOfSheet
/// </summary>
public PalletMode PalletExchange { get; set; } = PalletMode.EndOfSheet;
/// <summary>
/// Gets or sets whether to use line numbers in output.
/// Default: true
/// </summary>
[Category("1. Output")]
[DisplayName("Use Line Numbers")]
[Description("Include line numbers in output.")]
public bool UseLineNumbers { get; set; } = true;
/// <summary>
/// Gets or sets the starting line number for features.
/// Default: 1
/// </summary>
[Category("1. Output")]
[DisplayName("Feature Line Number Start")]
[Description("Starting line number for features.")]
public int FeatureLineNumberStart { get; set; } = 1;
/// <summary>
/// Gets or sets whether to use speed/gas commands.
/// Default: false
/// </summary>
[Category("2. Subprograms")]
[DisplayName("Use Sheet Subprograms")]
[Description("Use subprograms for sheet operations.")]
public bool UseSheetSubprograms { get; set; } = true;
[Category("2. Subprograms")]
[DisplayName("Sheet Subprogram Start")]
[Description("Starting subprogram number for sheet operations.")]
public int SheetSubprogramStart { get; set; } = 101;
[Category("2. Subprograms")]
[DisplayName("Use Part Subprograms")]
[Description("Use M98 sub-programs for part geometry. Reduces output size for repeated parts.")]
public bool UsePartSubprograms { get; set; } = false;
[Category("2. Subprograms")]
[DisplayName("Part Subprogram Start")]
[Description("Starting sub-program number for part geometry sub-programs.")]
public int PartSubprogramStart { get; set; } = 200;
[Category("2. Subprograms")]
[DisplayName("Variable Declaration Subprogram")]
[Description("Subprogram number for variable declarations.")]
public int VariableDeclarationSubprogram { get; set; } = 100;
[Category("3. Positioning")]
[DisplayName("Coordinate Mode Between Parts")]
[Description("How coordinate positioning is handled between parts (G92, G91, or G53).")]
public CoordinateMode CoordModeBetweenParts { get; set; } = CoordinateMode.G92;
[Category("4. Process")]
[DisplayName("Process Parameter Mode")]
[Description("How G89 parameters are provided (LibraryFile or Explicit).")]
public G89Mode ProcessParameterMode { get; set; } = G89Mode.LibraryFile;
[Category("4. Process")]
[DisplayName("Default Assist Gas")]
[Description("Default assist gas when Nest.AssistGas is empty.")]
public string DefaultAssistGas { get; set; } = "O2";
[Category("4. Process")]
[DisplayName("Default Etch Gas")]
[Description("Gas used for etch operations.")]
public string DefaultEtchGas { get; set; } = "N2";
[Category("4. Process")]
[DisplayName("Use Exact Stop Mode")]
[Description("Enable exact stop mode (G61).")]
public bool UseExactStopMode { get; set; } = false;
[Category("4. Process")]
[DisplayName("Use Speed/Gas Commands")]
[Description("Enable speed/gas commands in output.")]
public bool UseSpeedGas { get; set; } = false;
/// <summary>
/// Gets or sets the feedrate percentage for lead-in moves.
/// Default: 0.5 (50%)
/// </summary>
[Category("4. Process")]
[DisplayName("Use Anti-Dive")]
[Description("Enable anti-dive functionality.")]
public bool UseAntiDive { get; set; } = true;
[Category("4. Process")]
[DisplayName("Use Smart Rapids")]
[Description("Enable smart rapids optimization.")]
public bool UseSmartRapids { get; set; } = false;
[Category("5. Kerf")]
[DisplayName("Kerf Compensation")]
[Description("Where kerf compensation is applied (ControllerSide or PreApplied).")]
public KerfMode KerfCompensation { get; set; } = KerfMode.ControllerSide;
[Category("5. Kerf")]
[DisplayName("Default Kerf Side")]
[Description("Default side for kerf compensation (Left or Right).")]
public KerfSide DefaultKerfSide { get; set; } = KerfSide.Left;
[Category("6. M47 (Optional Stop)")]
[DisplayName("Interior M47")]
[Description("How M47 is used in interior cuts.")]
public M47Mode InteriorM47 { get; set; } = M47Mode.Always;
[Category("6. M47 (Optional Stop)")]
[DisplayName("Exterior M47")]
[Description("How M47 is used in exterior cuts.")]
public M47Mode ExteriorM47 { get; set; } = M47Mode.Always;
[Category("6. M47 (Optional Stop)")]
[DisplayName("M47 Override Distance Threshold")]
[Description("Distance threshold for M47 override. Null = no override.")]
public double? M47OverrideDistanceThreshold { get; set; } = null;
[Category("7. Safety")]
[DisplayName("Safety Headraise Distance")]
[Description("Safety head raise distance in machine units. Null = disabled.")]
public int? SafetyHeadraiseDistance { get; set; } = 2000;
[Category("8. Pallet")]
[DisplayName("Pallet Exchange")]
[Description("When pallet exchange occurs (None, EndOfSheet, or StartAndEnd).")]
public PalletMode PalletExchange { get; set; } = PalletMode.EndOfSheet;
[Category("9. Feedrates")]
[DisplayName("Lead-In Feedrate %")]
[Description("Feedrate percentage for lead-in moves (e.g. 0.5 = 50%).")]
public double LeadInFeedratePercent { get; set; } = 0.5;
/// <summary>
/// Gets or sets the feedrate percentage for lead-in arc-to-line moves.
/// Default: 0.5 (50%)
/// </summary>
[Category("9. Feedrates")]
[DisplayName("Lead-In Arc Line 2 Feedrate %")]
[Description("Feedrate percentage for lead-in arc-to-line moves.")]
public double LeadInArcLine2FeedratePercent { get; set; } = 0.5;
/// <summary>
/// Gets or sets the feedrate percentage for lead-out moves.
/// Default: 0.5 (50%)
/// </summary>
[Category("9. Feedrates")]
[DisplayName("Lead-Out Feedrate %")]
[Description("Feedrate percentage for lead-out moves.")]
public double LeadOutFeedratePercent { get; set; } = 0.5;
/// <summary>
/// Gets or sets the feedrate multiplier for circular cuts.
/// Default: 0.8 (80%)
/// </summary>
[Category("9. Feedrates")]
[DisplayName("Circle Feedrate Multiplier")]
[Description("Feedrate multiplier for circular cuts (e.g. 0.8 = 80%).")]
public double CircleFeedrateMultiplier { get; set; } = 0.8;
/// <summary>
/// Gets or sets the arc feedrate calculation mode.
/// Default: ArcFeedrateMode.None
/// </summary>
[Category("9. Feedrates")]
[DisplayName("Arc Feedrate Mode")]
[Description("Arc feedrate calculation mode (None, Percentages, or Variables).")]
public ArcFeedrateMode ArcFeedrate { get; set; } = ArcFeedrateMode.None;
/// <summary>
/// Gets or sets the radius-based arc feedrate ranges.
/// Ranges are matched from smallest MaxRadius to largest.
/// </summary>
[Category("9. Feedrates")]
[DisplayName("Arc Feedrate Ranges")]
[Description("Radius-based arc feedrate ranges. Matched from smallest to largest MaxRadius.")]
public List<ArcFeedrateRange> ArcFeedrateRanges { get; set; } = new()
{
new() { MaxRadius = 0.125, FeedratePercent = 0.25, VariableNumber = 123 },
@@ -298,17 +253,25 @@ namespace OpenNest.Posts.Cincinnati
new() { MaxRadius = 4.500, FeedratePercent = 0.80, VariableNumber = 125 }
};
/// <summary>
/// Gets or sets the variable number for sheet width.
/// Default: 110
/// </summary>
[Category("A. Variables")]
[DisplayName("Sheet Width Variable")]
[Description("Variable number for sheet width.")]
public int SheetWidthVariable { get; set; } = 110;
/// <summary>
/// Gets or sets the variable number for sheet length.
/// Default: 111
/// </summary>
[Category("A. Variables")]
[DisplayName("Sheet Length Variable")]
[Description("Variable number for sheet length.")]
public int SheetLengthVariable { get; set; } = 111;
[Category("B. Libraries")]
[DisplayName("Material Libraries")]
[Description("Material-to-library mapping for cut operations. Maps (material, thickness, gas) to a G89 library file.")]
public List<MaterialLibraryEntry> MaterialLibraries { get; set; } = new();
[Category("B. Libraries")]
[DisplayName("Etch Libraries")]
[Description("Gas-to-library mapping for etch operations.")]
public List<EtchLibraryEntry> EtchLibraries { get; set; } = new();
}
public class MaterialLibraryEntry
@@ -9,7 +9,7 @@ using OpenNest.CNC;
namespace OpenNest.Posts.Cincinnati
{
public sealed class CincinnatiPostProcessor : IPostProcessor
public sealed class CincinnatiPostProcessor : IConfigurablePostProcessor
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -23,6 +23,8 @@ namespace OpenNest.Posts.Cincinnati
public CincinnatiPostConfig Config { get; }
object IConfigurablePostProcessor.Config => Config;
public CincinnatiPostProcessor()
{
var configPath = GetConfigPath();
@@ -153,7 +153,8 @@ public sealed class CincinnatiSheetWriter
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
SheetDiagonal = sheetDiagonal,
PartLocation = part.Location
};
_featureWriter.Write(w, ctx);
@@ -240,7 +241,8 @@ public sealed class CincinnatiSheetWriter
IsEtch = isEtch,
LibraryFile = isEtch ? etchLibrary : cutLibrary,
CutDistance = cutDistance,
SheetDiagonal = sheetDiagonal
SheetDiagonal = sheetDiagonal,
PartLocation = part.Location
};
_featureWriter.Write(w, ctx);
@@ -121,6 +121,17 @@ public class CincinnatiPostProcessorTests
Assert.Equal("OpenNest", pp.Author);
}
[Fact]
public void Post_ImplementsIConfigurablePostProcessor()
{
var post = new CincinnatiPostProcessor(new CincinnatiPostConfig());
var configurable = post as IConfigurablePostProcessor;
Assert.NotNull(configurable);
Assert.NotNull(configurable.Config);
Assert.IsType<CincinnatiPostConfig>(configurable.Config);
}
[Fact]
public void Post_SkipsEmptyPlates()
{
@@ -280,6 +280,67 @@ public class CincinnatiSheetWriterTests
Assert.False(FeatureUtils.IsEtch(codes));
}
[Fact]
public void WriteSheet_InlineCoordinates_AreAbsoluteOnPlate()
{
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
// Part program is at origin: (0,0) to (2,0) to (2,2) to (0,2) to (0,0)
var pgm = new Program();
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(2, 0));
pgm.Codes.Add(new LinearMove(2, 2));
pgm.Codes.Add(new LinearMove(0, 2));
pgm.Codes.Add(new LinearMove(0, 0));
var plate = new Plate(48.0, 96.0);
// Place part at (10.5, 5.25) on the plate to produce non-integer coordinates
plate.Parts.Add(new Part(new Drawing("Square", pgm), new Vector(10.5, 5.25)));
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString();
// Under G90, coordinates must be plate-absolute (part coords + part location)
Assert.Contains("G0 X10.5 Y5.25", output); // rapid to pierce
Assert.Contains("G1 X12.5 Y5.25", output); // (2,0) + (10.5,5.25)
Assert.Contains("G1 X12.5 Y7.25", output); // (2,2) + (10.5,5.25)
Assert.Contains("G1 X10.5 Y7.25", output); // (0,2) + (10.5,5.25)
Assert.Contains("G1 X10.5 Y5.25", output); // (0,0) + (10.5,5.25)
}
[Fact]
public void WriteSheet_TwoPartsAtDifferentLocations_HaveDistinctAbsoluteCoords()
{
var config = new CincinnatiPostConfig { PostedAccuracy = 4 };
var pgm = new Program();
pgm.Codes.Add(new RapidMove(0, 0));
pgm.Codes.Add(new LinearMove(1, 0));
pgm.Codes.Add(new LinearMove(1, 1));
pgm.Codes.Add(new LinearMove(0, 0));
var drawing = new Drawing("Tri", pgm);
var plate = new Plate(48.0, 96.0);
plate.Parts.Add(new Part(drawing, new Vector(5.5, 3.25)));
plate.Parts.Add(new Part(drawing, new Vector(20.5, 10.25)));
var sb = new StringBuilder();
using var sw = new StringWriter(sb);
var sheetWriter = new CincinnatiSheetWriter(config, new ProgramVariableManager());
sheetWriter.Write(sw, plate, "TestNest", 1, 101, "", "");
var output = sb.ToString();
// First part at (5.5, 3.25)
Assert.Contains("G0 X5.5 Y3.25", output);
Assert.Contains("G1 X6.5 Y3.25", output);
// Second part at (20.5, 10.25)
Assert.Contains("G0 X20.5 Y10.25", output);
Assert.Contains("G1 X21.5 Y10.25", output);
}
private static Program CreateSimpleProgram()
{
var pgm = new Program();
+39 -8
View File
@@ -23,7 +23,10 @@ namespace OpenNest.Actions
private ContourType snapContourType;
private double snapNormal;
private bool hasSnap;
private ShapeInfo hoveredContour;
private ContextMenuStrip contextMenu;
private static readonly Brush grayOverlay = new SolidBrush(Color.FromArgb(160, 180, 180, 180));
private static readonly Pen highlightPen = new Pen(Color.Cyan, 2.5f);
public ActionLeadIn(PlateView plateView)
: base(plateView)
@@ -54,6 +57,7 @@ namespace OpenNest.Actions
profile = null;
contours = null;
hasSnap = false;
hoveredContour = null;
plateView.Invalidate();
}
@@ -77,6 +81,7 @@ namespace OpenNest.Actions
// Find closest contour and point
var bestDist = double.MaxValue;
hasSnap = false;
hoveredContour = null;
foreach (var info in contours)
{
@@ -91,6 +96,7 @@ namespace OpenNest.Actions
snapContourType = info.ContourType;
snapNormal = ContourCuttingStrategy.ComputeNormal(closest, entity, info.ContourType);
hasSnap = true;
hoveredContour = info;
}
}
@@ -134,6 +140,35 @@ namespace OpenNest.Actions
private void OnPaint(object sender, PaintEventArgs e)
{
var g = e.Graphics;
// Gray overlay on all parts except the selected one
foreach (var lp in plateView.LayoutParts)
{
if (lp == selectedLayoutPart)
continue;
if (lp.Path != null)
g.FillPath(grayOverlay, lp.Path);
}
// Highlight the hovered contour
if (hoveredContour != null && selectedPart != null)
{
using var contourPath = hoveredContour.Shape.GetGraphicsPath();
// Translate from local part space to world space, then apply view transform
using var contourMatrix = new Matrix();
contourMatrix.Translate((float)selectedPart.Location.X, (float)selectedPart.Location.Y);
contourMatrix.Multiply(plateView.Matrix, MatrixOrder.Append);
contourPath.Transform(contourMatrix);
var prevSmooth = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
g.DrawPath(highlightPen, contourPath);
g.SmoothingMode = prevSmooth;
}
if (!hasSnap || selectedPart == null)
return;
@@ -182,7 +217,6 @@ namespace OpenNest.Actions
var piercePoint = leadIn.GetPiercePoint(snapPoint, snapNormal);
var worldPierce = TransformToWorld(piercePoint);
var g = e.Graphics;
var oldSmooth = g.SmoothingMode;
g.SmoothingMode = SmoothingMode.AntiAlias;
@@ -190,12 +224,12 @@ namespace OpenNest.Actions
var pt1 = plateView.PointWorldToGraph(worldPierce);
var pt2 = plateView.PointWorldToGraph(worldSnap);
using var pen = new Pen(Color.Yellow, 2.0f / plateView.ViewScale);
g.DrawLine(pen, pt1, pt2);
using var previewPen = new Pen(Color.Magenta, 2.0f / plateView.ViewScale);
g.DrawLine(previewPen, pt1, pt2);
// Draw a small circle at the pierce point
var radius = 3.0f / plateView.ViewScale;
g.FillEllipse(Brushes.Yellow, pt1.X - radius, pt1.Y - radius, radius * 2, radius * 2);
g.FillEllipse(Brushes.Magenta, pt1.X - radius, pt1.Y - radius, radius * 2, radius * 2);
// Draw a small circle at the contour start point
g.FillEllipse(Brushes.Lime, pt2.X - radius, pt2.Y - radius, radius * 2, radius * 2);
@@ -215,10 +249,6 @@ namespace OpenNest.Actions
if (part.BaseDrawing.IsCutOff)
return;
// If part already has locked lead-ins, don't allow re-placement
if (part.LeadInsLocked)
return;
selectedLayoutPart = layoutPart;
selectedPart = part;
@@ -312,6 +342,7 @@ namespace OpenNest.Actions
profile = null;
contours = null;
hasSnap = false;
hoveredContour = null;
plateView.Invalidate();
}
+16 -20
View File
@@ -515,23 +515,22 @@ namespace OpenNest.Controls
private void DrawAllCutDirectionArrows(Graphics g)
{
using var pen = new Pen(Color.FromArgb(220, Color.DarkCyan), 1.5f);
using var brush = new SolidBrush(Color.FromArgb(220, Color.DarkCyan));
using var pen = new Pen(Color.FromArgb(220, Color.Black), 1.5f);
var arrowSpacingWorld = view.LengthGuiToWorld(60f);
var arrowSize = 5f;
var arrowSize = 6f;
for (var i = 0; i < view.Plate.Parts.Count; ++i)
{
var part = view.Plate.Parts[i];
var pgm = part.Program;
var pos = part.Location;
DrawProgramCutDirectionArrows(g, pgm, ref pos, pen, brush, arrowSpacingWorld, arrowSize);
DrawProgramCutDirectionArrows(g, pgm, ref pos, pen, arrowSpacingWorld, arrowSize);
}
}
private void DrawProgramCutDirectionArrows(Graphics g, Program pgm, ref Vector pos,
Pen pen, Brush brush, double spacing, float arrowSize)
Pen pen, double spacing, float arrowSize)
{
for (var i = 0; i < pgm.Length; ++i)
{
@@ -541,7 +540,7 @@ namespace OpenNest.Controls
{
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
DrawProgramCutDirectionArrows(g, subpgm.Program, ref pos, pen, brush, spacing, arrowSize);
DrawProgramCutDirectionArrows(g, subpgm.Program, ref pos, pen, spacing, arrowSize);
continue;
}
@@ -555,7 +554,7 @@ namespace OpenNest.Controls
{
var line = (LinearMove)code;
if (!line.Suppressed)
DrawLineDirectionArrows(g, pos, endpt, brush, spacing, arrowSize);
DrawLineDirectionArrows(g, pos, endpt, pen, spacing, arrowSize);
}
else if (code.Type == CodeType.ArcMove)
{
@@ -565,7 +564,7 @@ namespace OpenNest.Controls
var center = pgm.Mode == Mode.Incremental
? arc.CenterPoint + pos
: arc.CenterPoint;
DrawArcDirectionArrows(g, pos, endpt, center, arc.Rotation, brush, spacing, arrowSize);
DrawArcDirectionArrows(g, pos, endpt, center, arc.Rotation, pen, spacing, arrowSize);
}
}
@@ -574,7 +573,7 @@ namespace OpenNest.Controls
}
private void DrawLineDirectionArrows(Graphics g, Vector start, Vector end,
Brush brush, double spacing, float arrowSize)
Pen pen, double spacing, float arrowSize)
{
var dx = end.X - start.X;
var dy = end.Y - start.Y;
@@ -593,12 +592,12 @@ namespace OpenNest.Controls
var pt = new Vector(start.X + dirX * t, start.Y + dirY * t);
var screenPt = view.PointWorldToGraph(pt);
var angle = System.Math.Atan2(-dirY, dirX);
DrawArrowHead(g, brush, screenPt, angle, arrowSize);
DrawArrowHead(g, pen, screenPt, angle, arrowSize);
}
}
private void DrawArcDirectionArrows(Graphics g, Vector start, Vector end, Vector center,
RotationType rotation, Brush brush, double spacing, float arrowSize)
RotationType rotation, Pen pen, double spacing, float arrowSize)
{
var radius = center.DistanceTo(start);
if (radius < Tolerance.Epsilon) return;
@@ -644,11 +643,11 @@ namespace OpenNest.Controls
tangent = angle - System.Math.PI / 2;
var screenAngle = System.Math.Atan2(-System.Math.Sin(tangent), System.Math.Cos(tangent));
DrawArrowHead(g, brush, screenPt, screenAngle, arrowSize);
DrawArrowHead(g, pen, screenPt, screenAngle, arrowSize);
}
}
private static void DrawArrowHead(Graphics g, Brush brush, PointF tip, double angle, float size)
private static void DrawArrowHead(Graphics g, Pen pen, PointF tip, double angle, float size)
{
var sin = (float)System.Math.Sin(angle);
var cos = (float)System.Math.Cos(angle);
@@ -658,14 +657,11 @@ namespace OpenNest.Controls
var wingX = size * 0.5f * sin;
var wingY = -size * 0.5f * cos;
var points = new PointF[]
{
tip,
new PointF(tip.X + backX + wingX, tip.Y + backY + wingY),
new PointF(tip.X + backX - wingX, tip.Y + backY - wingY),
};
var wing1 = new PointF(tip.X + backX + wingX, tip.Y + backY + wingY);
var wing2 = new PointF(tip.X + backX - wingX, tip.Y + backY - wingY);
g.FillPolygon(brush, points);
g.DrawLine(pen, wing1, tip);
g.DrawLine(pen, wing2, tip);
}
private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen)
+7
View File
@@ -1040,6 +1040,13 @@ namespace OpenNest.Forms
if (postProcessor == null)
return;
if (postProcessor is IConfigurablePostProcessor configurable)
{
using var configForm = new PostProcessorConfigForm(configurable);
if (configForm.ShowDialog() != DialogResult.OK)
return;
}
var dialog = new SaveFileDialog();
dialog.Filter = "CNC File (*.cnc) | *.cnc";
dialog.FileName = activeForm.Nest.Name;
+98
View File
@@ -0,0 +1,98 @@
namespace OpenNest.Forms
{
partial class PostProcessorConfigForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
this.propertyGrid = new System.Windows.Forms.PropertyGrid();
this.bottomPanel = new OpenNest.Controls.BottomPanel();
this.okButton = new System.Windows.Forms.Button();
this.cancelButton = new System.Windows.Forms.Button();
this.bottomPanel.SuspendLayout();
this.SuspendLayout();
//
// propertyGrid
//
this.propertyGrid.Dock = System.Windows.Forms.DockStyle.Fill;
this.propertyGrid.Location = new System.Drawing.Point(0, 0);
this.propertyGrid.Name = "propertyGrid";
this.propertyGrid.Size = new System.Drawing.Size(484, 511);
this.propertyGrid.TabIndex = 0;
this.propertyGrid.ToolbarVisible = true;
this.propertyGrid.PropertySort = System.Windows.Forms.PropertySort.Categorized;
//
// bottomPanel
//
this.bottomPanel.Controls.Add(this.okButton);
this.bottomPanel.Controls.Add(this.cancelButton);
this.bottomPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.bottomPanel.Location = new System.Drawing.Point(0, 511);
this.bottomPanel.Name = "bottomPanel";
this.bottomPanel.Size = new System.Drawing.Size(484, 50);
this.bottomPanel.TabIndex = 1;
//
// okButton
//
this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.okButton.DialogResult = System.Windows.Forms.DialogResult.OK;
this.okButton.Location = new System.Drawing.Point(286, 11);
this.okButton.Name = "okButton";
this.okButton.Size = new System.Drawing.Size(90, 28);
this.okButton.TabIndex = 0;
this.okButton.Text = "OK";
this.okButton.UseVisualStyleBackColor = true;
this.okButton.Click += new System.EventHandler(this.okButton_Click);
//
// cancelButton
//
this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.cancelButton.Location = new System.Drawing.Point(382, 11);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(90, 28);
this.cancelButton.TabIndex = 1;
this.cancelButton.Text = "Cancel";
this.cancelButton.UseVisualStyleBackColor = true;
this.cancelButton.Click += new System.EventHandler(this.cancelButton_Click);
//
// PostProcessorConfigForm
//
this.AcceptButton = this.okButton;
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.CancelButton = this.cancelButton;
this.ClientSize = new System.Drawing.Size(484, 561);
this.Controls.Add(this.propertyGrid);
this.Controls.Add(this.bottomPanel);
this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "PostProcessorConfigForm";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Post Processor Settings";
this.bottomPanel.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.PropertyGrid propertyGrid;
private Controls.BottomPanel bottomPanel;
private System.Windows.Forms.Button okButton;
private System.Windows.Forms.Button cancelButton;
}
}
+42
View File
@@ -0,0 +1,42 @@
using System;
using System.Text.Json;
using System.Windows.Forms;
namespace OpenNest.Forms
{
public partial class PostProcessorConfigForm : Form
{
private readonly IConfigurablePostProcessor postProcessor;
private readonly string configBackup;
public PostProcessorConfigForm(IConfigurablePostProcessor postProcessor)
{
InitializeComponent();
this.postProcessor = postProcessor;
this.Text = postProcessor.Name + " Settings";
// Deep-clone config as JSON backup for cancel/restore
configBackup = JsonSerializer.Serialize(postProcessor.Config, postProcessor.Config.GetType());
propertyGrid.SelectedObject = postProcessor.Config;
}
private void okButton_Click(object sender, EventArgs e)
{
postProcessor.SaveConfig();
}
private void cancelButton_Click(object sender, EventArgs e)
{
// Restore config from backup
var original = JsonSerializer.Deserialize(configBackup, postProcessor.Config.GetType());
var properties = postProcessor.Config.GetType().GetProperties();
foreach (var prop in properties)
{
if (prop.CanWrite)
prop.SetValue(postProcessor.Config, prop.GetValue(original));
}
}
}
}