Compare commits

...

7 Commits

Author SHA1 Message Date
aj 134771aa23 feat: add Draw Cut Direction view option and extract PlateRenderer
Add a "Draw Cut Direction" toggle to the View menu that draws small
arrowheads along cutting paths to indicate the direction of travel.
Arrows are placed on both linear and arc moves, spaced ~60px apart,
and correctly follow CW/CCW arc tangents.

Extract all rendering methods (~660 lines) from PlateView into a new
PlateRenderer class, reducing PlateView from 1640 to 979 lines.
PlateView retains input handling, selection, zoom, and part management.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:22:05 -04:00
aj 59a66173e1 fix: exempt scribe/etch contours from lead-ins and kerf
Scribe/etch lines were being treated as cut contours by
ContourCuttingStrategy, receiving lead-ins and kerf compensation.
Now they are separated before ShapeProfile construction and emitted
as plain moves with LayerType.Scribe preserved.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:56:07 -04:00
aj a2b7be44f8 fix: draw approach rapid directly to first pierce point, not part origin
The approach rapid from sheet origin was drawing to part.Location (the
program coordinate origin) then a second rapid to the actual first
pierce point. This created a dog-leg through the part origin instead
of a single straight rapid to the lead-in. Also fixed PlateProcessor
using the original program's start point instead of the processed one
when the cutting strategy is applied on-the-fly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:38:53 -04:00
aj e94a556f23 feat: add Remove Lead-ins button to EditNestForm toolbar
Clears all manual lead-ins from every part on the active plate and
rebuilds the layout graphics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:36:01 -04:00
aj 428dbdb03c feat: persist cutting parameters and add pierce clearance UI
Save/restore cutting parameters as JSON in user settings so values
survive between sessions. Add pierce clearance numeric input to the
CuttingParametersForm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:35:51 -04:00
aj e860ca3f4a feat: add pierce clearance clamping for circle contour lead-ins
Scales down lead-ins that would place the pierce point too close to the
opposite wall of small holes. Uses quadratic solve to find the maximum
safe distance inside a clearance-reduced radius. Adds Scale() method to
all LeadIn types and applies clamping in both the strategy and the
interactive preview.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:35:41 -04:00
aj a399c89f58 fix: resolve rendering issues when applying lead-ins to parts
Three issues caused incorrect rendering after lead-in application:
- Rapid move entities from ToGeometry() were included in ShapeProfile
  contour detection, turning traversal paths into cutting moves
- Program created with Mode.Incremental default made the absolute-to-
  incremental conversion a no-op, leaving coordinates unconverted
- AddProgramSplit didn't call StartFigure() at rapid moves, causing
  GraphicsPath to draw implicit connecting lines between contours
- Part.Rotation returned 0 from the new program instead of the actual
  rotation, displacing the sequence label on rotated parts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 19:35:29 -04:00
25 changed files with 1206 additions and 521 deletions
@@ -11,6 +11,12 @@ namespace OpenNest.CNC.CuttingStrategy
{
var exitPoint = approachPoint;
var entities = partProgram.ToGeometry();
entities.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
// Separate scribe/etch entities — they don't get lead-ins or kerf
var scribeEntities = entities.FindAll(e => e.Layer == SpecialLayers.Scribe);
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
var profile = new ShapeProfile(entities);
// Find closest point on perimeter from exit point
@@ -21,10 +27,23 @@ namespace OpenNest.CNC.CuttingStrategy
var orderedCutouts = SequenceCutouts(profile.Cutouts, perimeterPoint);
orderedCutouts.Reverse();
// Build output program: cutouts first (farthest to nearest), perimeter last
var result = new Program();
// Build output program: scribe first, cutouts second, perimeter last
var result = new Program(Mode.Absolute);
var currentPoint = exitPoint;
// Emit scribe/etch contours first (no lead-ins, no kerf)
if (scribeEntities.Count > 0)
{
var scribeShapes = ShapeBuilder.GetShapes(scribeEntities);
foreach (var scribe in scribeShapes)
{
var startPt = GetShapeStartPoint(scribe);
result.Codes.Add(new RapidMove(startPt));
result.Codes.AddRange(ConvertShapeToMoves(scribe, startPt, LayerType.Scribe));
currentPoint = startPt;
}
}
foreach (var cutout in orderedCutouts)
{
var contourType = DetectContourType(cutout);
@@ -35,6 +54,9 @@ namespace OpenNest.CNC.CuttingStrategy
var leadIn = SelectLeadIn(contourType);
var leadOut = SelectLeadOut(contourType);
if (contourType == ContourType.ArcCircle && entity is Circle circle)
leadIn = ClampLeadInForCircle(leadIn, circle, closestPt, normal);
result.Codes.AddRange(leadIn.Generate(closestPt, normal, winding));
var reindexed = cutout.ReindexAt(closestPt, entity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
@@ -152,6 +174,50 @@ namespace OpenNest.CNC.CuttingStrategy
return area >= 0 ? RotationType.CCW : RotationType.CW;
}
private LeadIn ClampLeadInForCircle(LeadIn leadIn, Circle circle, Vector contourPoint, double normalAngle)
{
if (leadIn is NoLeadIn || Parameters.PierceClearance <= 0)
return leadIn;
var piercePoint = leadIn.GetPiercePoint(contourPoint, normalAngle);
var maxRadius = circle.Radius - Parameters.PierceClearance;
if (maxRadius <= 0)
return leadIn;
var distFromCenter = piercePoint.DistanceTo(circle.Center);
if (distFromCenter <= maxRadius)
return leadIn;
// Compute max distance from contourPoint toward piercePoint that stays
// inside a circle of radius maxRadius centered at circle.Center.
// Solve: |contourPoint + t*d - center|^2 = maxRadius^2
var currentDist = contourPoint.DistanceTo(piercePoint);
if (currentDist < Math.Tolerance.Epsilon)
return leadIn;
var dx = (piercePoint.X - contourPoint.X) / currentDist;
var dy = (piercePoint.Y - contourPoint.Y) / currentDist;
var vx = contourPoint.X - circle.Center.X;
var vy = contourPoint.Y - circle.Center.Y;
var b = 2.0 * (vx * dx + vy * dy);
var c = vx * vx + vy * vy - maxRadius * maxRadius;
var discriminant = b * b - 4.0 * c;
if (discriminant < 0)
return leadIn;
var t = (-b + System.Math.Sqrt(discriminant)) / 2.0;
if (t <= 0)
return leadIn;
var scale = t / currentDist;
if (scale >= 1.0)
return leadIn;
return leadIn.Scale(scale);
}
private LeadIn SelectLeadIn(ContourType contourType)
{
return contourType switch
@@ -172,7 +238,7 @@ namespace OpenNest.CNC.CuttingStrategy
};
}
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint, LayerType layer = LayerType.Display)
{
var moves = new List<ICode>();
@@ -180,15 +246,15 @@ namespace OpenNest.CNC.CuttingStrategy
{
if (entity is Line line)
{
moves.Add(new LinearMove(line.EndPoint));
moves.Add(new LinearMove(line.EndPoint) { Layer = layer });
}
else if (entity is Arc arc)
{
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW));
moves.Add(new ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW) { Layer = layer });
}
else if (entity is Circle circle)
{
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation));
moves.Add(new ArcMove(startPoint, circle.Center, circle.Rotation) { Layer = layer });
}
else
{
@@ -198,5 +264,14 @@ namespace OpenNest.CNC.CuttingStrategy
return moves;
}
private static Vector GetShapeStartPoint(Shape shape)
{
var first = shape.Entities[0];
if (first is Line line) return line.StartPoint;
if (first is Arc arc) return arc.StartPoint();
if (first is Circle circle) return new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
return Vector.Zero;
}
}
}
@@ -21,6 +21,8 @@ namespace OpenNest.CNC.CuttingStrategy
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
public double PierceClearance { get; set; } = 0.0625;
public Tab TabConfig { get; set; }
public bool TabsEnabled { get; set; }
@@ -32,5 +32,8 @@ namespace OpenNest.CNC.CuttingStrategy
arcCenterX + Radius * System.Math.Cos(contourNormalAngle),
arcCenterY + Radius * System.Math.Sin(contourNormalAngle));
}
public override LeadIn Scale(double factor) =>
new ArcLeadIn { Radius = Radius * factor };
}
}
@@ -45,5 +45,8 @@ namespace OpenNest.CNC.CuttingStrategy
arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle));
}
public override LeadIn Scale(double factor) =>
new CleanHoleLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, Kerf = Kerf };
}
}
@@ -9,5 +9,7 @@ namespace OpenNest.CNC.CuttingStrategy
RotationType winding = RotationType.CW);
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
public virtual LeadIn Scale(double factor) => this;
}
}
@@ -45,5 +45,8 @@ namespace OpenNest.CNC.CuttingStrategy
arcStartX + LineLength * System.Math.Cos(lineAngle),
arcStartY + LineLength * System.Math.Sin(lineAngle));
}
public override LeadIn Scale(double factor) =>
new LineArcLeadIn { LineLength = LineLength * factor, ArcRadius = ArcRadius * factor, ApproachAngle = ApproachAngle };
}
}
@@ -28,5 +28,8 @@ namespace OpenNest.CNC.CuttingStrategy
contourStartPoint.X + Length * System.Math.Cos(approachAngle),
contourStartPoint.Y + Length * System.Math.Sin(approachAngle));
}
public override LeadIn Scale(double factor) =>
new LineLeadIn { Length = Length * factor, ApproachAngle = ApproachAngle };
}
}
@@ -40,5 +40,8 @@ namespace OpenNest.CNC.CuttingStrategy
midX + Length1 * System.Math.Cos(firstAngle),
midY + Length1 * System.Math.Sin(firstAngle));
}
public override LeadIn Scale(double factor) =>
new LineLineLeadIn { Length1 = Length1 * factor, ApproachAngle1 = ApproachAngle1, Length2 = Length2 * factor, ApproachAngle2 = ApproachAngle2 };
}
}
+1 -1
View File
@@ -94,7 +94,7 @@ namespace OpenNest
/// </summary>
public double Rotation
{
get { return Program.Rotation; }
get { return HasManualLeadIns ? preLeadInRotation : Program.Rotation; }
}
/// <summary>
+1 -1
View File
@@ -44,7 +44,7 @@ namespace OpenNest.Engine
}
// Pierce point: program start point in plate space
var pierceLocal = GetProgramStartPoint(part.Program);
var pierceLocal = GetProgramStartPoint(processedProgram);
var piercePoint = ToPlateSpace(pierceLocal, part);
// Plan rapid from currentPoint to pierce point
+31
View File
@@ -2,6 +2,7 @@ using OpenNest.CNC.CuttingStrategy;
using OpenNest.Controls;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
@@ -148,6 +149,35 @@ namespace OpenNest.Actions
if (leadIn == null)
return;
// Clamp lead-in for circle contours so it stays inside the hole
if (snapContourType == ContourType.ArcCircle && snapEntity is Circle snapCircle
&& parameters.PierceClearance > 0)
{
var pierceCheck = leadIn.GetPiercePoint(snapPoint, snapNormal);
var distFromCenter = pierceCheck.DistanceTo(snapCircle.Center);
var maxRadius = snapCircle.Radius - parameters.PierceClearance;
if (maxRadius > 0 && distFromCenter > maxRadius)
{
var currentDist = snapPoint.DistanceTo(pierceCheck);
if (currentDist > Tolerance.Epsilon)
{
var dx = (pierceCheck.X - snapPoint.X) / currentDist;
var dy = (pierceCheck.Y - snapPoint.Y) / currentDist;
var vx = snapPoint.X - snapCircle.Center.X;
var vy = snapPoint.Y - snapCircle.Center.Y;
var b = 2.0 * (vx * dx + vy * dy);
var c = vx * vx + vy * vy - maxRadius * maxRadius;
var disc = b * b - 4.0 * c;
if (disc >= 0)
{
var t = (-b + System.Math.Sqrt(disc)) / 2.0;
if (t > 0 && t < currentDist)
leadIn = leadIn.Scale(t / currentDist);
}
}
}
}
// Get the pierce point (in local space)
var piercePoint = leadIn.GetPiercePoint(snapPoint, snapNormal);
var worldPierce = TransformToWorld(piercePoint);
@@ -219,6 +249,7 @@ namespace OpenNest.Actions
}
var entities = ConvertProgram.ToGeometry(cleanProgram);
entities.RemoveAll(e => e.Layer == SpecialLayers.Scribe);
profile = new ShapeProfile(entities);
contours = new List<ShapeInfo>();
+2 -2
View File
@@ -61,8 +61,8 @@ namespace OpenNest.Controls
e.Graphics.SmoothingMode = SmoothingMode.HighSpeed;
e.Graphics.TranslateTransform(origin.X, origin.Y);
DrawPlate(e.Graphics);
DrawParts(e.Graphics);
Renderer.DrawPlate(e.Graphics);
Renderer.DrawParts(e.Graphics);
e.Graphics.ResetTransform();
PaintMetadata(e.Graphics);
+691
View File
@@ -0,0 +1,691 @@
using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
namespace OpenNest.Controls
{
internal class PlateRenderer
{
private readonly PlateView view;
public PlateRenderer(PlateView view)
{
this.view = view;
}
public void DrawPlate(Graphics g)
{
var plate = view.Plate;
var plateRect = new RectangleF
{
Width = view.LengthWorldToGui(plate.Size.Length),
Height = view.LengthWorldToGui(plate.Size.Width)
};
var edgeSpacingRect = new RectangleF
{
Width = view.LengthWorldToGui(plate.Size.Length - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right),
Height = view.LengthWorldToGui(plate.Size.Width - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom)
};
switch (plate.Quadrant)
{
case 1:
plateRect.Location = view.PointWorldToGraph(0, 0);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left,
plate.EdgeSpacing.Bottom);
break;
case 2:
plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, 0);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left - plate.Size.Length,
plate.EdgeSpacing.Bottom);
break;
case 3:
plateRect.Location = view.PointWorldToGraph(-plate.Size.Length, -plate.Size.Width);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left - plate.Size.Length,
plate.EdgeSpacing.Bottom - plate.Size.Width);
break;
case 4:
plateRect.Location = view.PointWorldToGraph(0, -plate.Size.Width);
edgeSpacingRect.Location = view.PointWorldToGraph(
plate.EdgeSpacing.Left,
plate.EdgeSpacing.Bottom - plate.Size.Width);
break;
default:
return;
}
plateRect.Y -= plateRect.Height;
edgeSpacingRect.Y -= edgeSpacingRect.Height;
g.FillRectangle(view.ColorScheme.LayoutFillBrush, plateRect);
var viewBounds = view.GetViewBounds();
if (!edgeSpacingRect.Contains(viewBounds))
{
g.DrawRectangle(view.ColorScheme.EdgeSpacingPen,
edgeSpacingRect.X,
edgeSpacingRect.Y,
edgeSpacingRect.Width,
edgeSpacingRect.Height);
}
g.DrawRectangle(view.ColorScheme.LayoutOutlinePen,
plateRect.X,
plateRect.Y,
plateRect.Width,
plateRect.Height);
}
public void DrawParts(Graphics g)
{
var viewBounds = view.GetViewBounds();
var layoutParts = view.LayoutParts;
for (var i = 0; i < layoutParts.Count; ++i)
{
var part = layoutParts[i];
if (part.IsDirty)
part.Update(view);
var path = part.Path;
var pathBounds = path.GetBounds();
if (!pathBounds.IntersectsWith(viewBounds))
continue;
part.Draw(g, (i + 1).ToString());
DrawBendLines(g, part.BasePart);
DrawEtchMarks(g, part.BasePart);
DrawGrainWarning(g, part.BasePart);
}
var previewParts = view.PreviewParts;
var previewBrush = view.PreviewBrush;
var previewPen = view.PreviewPen;
for (var i = 0; i < previewParts.Count; i++)
{
var part = previewParts[i];
if (part.IsDirty)
part.Update(view);
var path = part.Path;
if (!path.GetBounds().IntersectsWith(viewBounds))
continue;
g.FillPath(previewBrush, path);
g.DrawPath(previewPen, path);
}
if (view.DrawOffset && view.Plate.PartSpacing > 0)
DrawOffsetGeometry(g);
if (view.DrawBounds)
{
var bounds = view.SelectedParts.Select(p => p.BasePart).ToList().GetBoundingBox();
DrawBox(g, bounds);
}
if (view.DrawRapid)
DrawRapids(g);
if (view.DrawPiercePoints)
DrawAllPiercePoints(g);
if (view.DrawCutDirection)
DrawAllCutDirectionArrows(g);
}
public void DrawCutOffs(Graphics g)
{
var plate = view.Plate;
if (plate?.CutOffs == null || plate.CutOffs.Count == 0)
return;
using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f);
using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f);
foreach (var cutoff in plate.CutOffs)
{
var program = cutoff.Drawing?.Program;
if (program == null || program.Codes.Count == 0)
continue;
var activePen = cutoff == view.SelectedCutOff ? selectedPen : pen;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
}
}
}
}
public void DrawActiveWorkArea(Graphics g)
{
var workArea = view.ActiveWorkArea;
if (workArea == null)
return;
var rect = new RectangleF
{
Location = view.PointWorldToGraph(workArea.Location),
Width = view.LengthWorldToGui(workArea.Width),
Height = view.LengthWorldToGui(workArea.Length)
};
rect.Y -= rect.Height;
using var pen = new Pen(Color.Red, 1.5f)
{
DashStyle = DashStyle.Dash
};
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
}
private static readonly Color[] PriorityFills =
{
Color.FromArgb(60, Color.LimeGreen),
Color.FromArgb(60, Color.Gold),
Color.FromArgb(60, Color.Salmon),
};
private static readonly Color[] PriorityBorders =
{
Color.FromArgb(180, Color.Green),
Color.FromArgb(180, Color.DarkGoldenrod),
Color.FromArgb(180, Color.DarkRed),
};
public void DrawDebugRemnants(Graphics g)
{
var remnants = view.DebugRemnants;
if (remnants == null || remnants.Count == 0)
return;
for (var i = 0; i < remnants.Count; i++)
{
var box = remnants[i];
var loc = view.PointWorldToGraph(box.Location);
var w = view.LengthWorldToGui(box.Width);
var h = view.LengthWorldToGui(box.Length);
var rect = new RectangleF(loc.X, loc.Y - h, w, h);
var priority = view.DebugRemnantPriorities != null && i < view.DebugRemnantPriorities.Count
? System.Math.Min(view.DebugRemnantPriorities[i], 2)
: 0;
using var brush = new SolidBrush(PriorityFills[priority]);
g.FillRectangle(brush, rect);
using var pen = new Pen(PriorityBorders[priority], 1.5f);
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
var label = $"P{priority} {box.Width:F1}x{box.Length:F1}";
using var font = new Font("Segoe UI", 8f);
using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString(label, font, Brushes.Black, rect, sf);
}
}
private void DrawBendLines(Graphics g, Part part)
{
if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
using var bendPen = new Pen(Color.Yellow, 1.5f)
{
DashStyle = System.Drawing.Drawing2D.DashStyle.Dash
};
foreach (var bend in part.BaseDrawing.Bends)
{
var start = bend.StartPoint;
var end = bend.EndPoint;
if (part.Rotation != 0)
{
start = start.Rotate(part.Rotation);
end = end.Rotate(part.Rotation);
}
start = start + part.Location;
end = end + part.Location;
var pt1 = view.PointWorldToGraph(start);
var pt2 = view.PointWorldToGraph(end);
g.DrawLine(bendPen, pt1, pt2);
}
}
private void DrawEtchMarks(Graphics g, Part part)
{
if (!view.ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
using var etchPen = new Pen(Color.Green, 1.5f);
var etchLength = 1.0;
foreach (var bend in part.BaseDrawing.Bends)
{
if (bend.Direction != BendDirection.Up)
continue;
var start = bend.StartPoint;
var end = bend.EndPoint;
if (part.Rotation != 0)
{
start = start.Rotate(part.Rotation);
end = end.Rotate(part.Rotation);
}
start = start + part.Location;
end = end + part.Location;
var length = bend.Length;
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
if (length < etchLength * 3.0)
{
var pt1 = view.PointWorldToGraph(start);
var pt2 = view.PointWorldToGraph(end);
g.DrawLine(etchPen, pt1, pt2);
}
else
{
var dx = System.Math.Cos(angle) * etchLength;
var dy = System.Math.Sin(angle) * etchLength;
var s1 = view.PointWorldToGraph(start);
var e1 = view.PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
g.DrawLine(etchPen, s1, e1);
var s2 = view.PointWorldToGraph(end);
var e2 = view.PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
g.DrawLine(etchPen, s2, e2);
}
}
}
private void DrawGrainWarning(Graphics g, Part part)
{
var plate = view.Plate;
if (!view.ShowBendLines || plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
var grainAngle = plate.GrainAngle;
var tolerance = Angle.ToRadians(5);
foreach (var bend in part.BaseDrawing.Bends)
{
var bendAngle = bend.LineAngle + part.Rotation;
bendAngle = bendAngle % System.Math.PI;
if (bendAngle < 0) bendAngle += System.Math.PI;
var grainNormalized = grainAngle % System.Math.PI;
if (grainNormalized < 0) grainNormalized += System.Math.PI;
var diff = System.Math.Abs(bendAngle - grainNormalized);
diff = System.Math.Min(diff, System.Math.PI - diff);
if (diff > tolerance)
{
var box = part.BaseDrawing.Program.BoundingBox();
var location = part.Location;
var pt1 = view.PointWorldToGraph(location);
var pt2 = view.PointWorldToGraph(new Vector(
location.X + box.Width, location.Y + box.Length));
using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f);
g.DrawRectangle(warnPen, pt1.X, pt2.Y,
System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y));
return;
}
}
}
private void DrawOffsetGeometry(Graphics g)
{
var layoutParts = view.LayoutParts;
using var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100));
for (var i = 0; i < layoutParts.Count; i++)
{
var layoutPart = layoutParts[i];
if (layoutPart.IsDirty)
layoutPart.Update(view);
layoutPart.UpdateOffset(view.Plate.PartSpacing, view.OffsetTolerance, view.Matrix);
if (layoutPart.OffsetPath != null)
g.DrawPath(offsetPen, layoutPart.OffsetPath);
}
}
private void DrawRapids(Graphics g)
{
var pos = new Vector(0, 0);
for (var i = 0; i < view.Plate.Parts.Count; ++i)
{
var part = view.Plate.Parts[i];
var pgm = part.Program;
var piercePoint = GetFirstPiercePoint(pgm, part.Location);
DrawLine(g, pos, piercePoint, view.ColorScheme.RapidPen);
pos = part.Location;
DrawRapids(g, pgm, ref pos, skipFirstRapid: true);
}
}
private static Vector GetFirstPiercePoint(Program pgm, Vector partLocation)
{
for (var i = 0; i < pgm.Length; i++)
{
if (pgm[i] is Motion motion)
{
if (pgm.Mode == Mode.Incremental)
return motion.EndPoint + partLocation;
return motion.EndPoint;
}
}
return partLocation;
}
private void DrawRapids(Graphics g, Program pgm, ref Vector pos, bool skipFirstRapid = false)
{
var firstRapidSkipped = false;
for (var i = 0; i < pgm.Length; ++i)
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
{
var subpgm = (SubProgramCall)code;
var program = subpgm.Program;
if (program != null)
DrawRapids(g, program, ref pos);
}
else
{
var motion = code as Motion;
if (motion != null)
{
if (pgm.Mode == Mode.Incremental)
{
var endpt = motion.EndPoint + pos;
if (code.Type == CodeType.RapidMove)
{
if (skipFirstRapid && !firstRapidSkipped)
firstRapidSkipped = true;
else
DrawLine(g, pos, endpt, view.ColorScheme.RapidPen);
}
pos = endpt;
}
else
{
if (code.Type == CodeType.RapidMove)
{
if (skipFirstRapid && !firstRapidSkipped)
firstRapidSkipped = true;
else
DrawLine(g, pos, motion.EndPoint, view.ColorScheme.RapidPen);
}
pos = motion.EndPoint;
}
}
}
}
}
private void DrawAllPiercePoints(Graphics g)
{
using var brush = new SolidBrush(Color.Red);
using var pen = new Pen(Color.DarkRed, 1f);
for (var i = 0; i < view.Plate.Parts.Count; ++i)
{
var part = view.Plate.Parts[i];
var pgm = part.Program;
var pos = part.Location;
DrawProgramPiercePoints(g, pgm, ref pos, brush, pen);
}
}
private void DrawProgramPiercePoints(Graphics g, Program pgm, ref Vector pos, Brush brush, Pen pen)
{
for (var i = 0; i < pgm.Length; ++i)
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
{
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen);
}
else
{
var motion = code as Motion;
if (motion == null) continue;
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
if (code.Type == CodeType.RapidMove)
{
var pt = view.PointWorldToGraph(endpt);
var radius = 2f;
g.FillEllipse(brush, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
g.DrawEllipse(pen, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
}
pos = endpt;
}
}
}
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));
var arrowSpacingWorld = view.LengthGuiToWorld(60f);
var arrowSize = 5f;
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);
}
}
private void DrawProgramCutDirectionArrows(Graphics g, Program pgm, ref Vector pos,
Pen pen, Brush brush, double spacing, float arrowSize)
{
for (var i = 0; i < pgm.Length; ++i)
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
{
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
DrawProgramCutDirectionArrows(g, subpgm.Program, ref pos, pen, brush, spacing, arrowSize);
continue;
}
if (code is not Motion motion) continue;
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
if (code.Type == CodeType.LinearMove)
{
var line = (LinearMove)code;
if (!line.Suppressed)
DrawLineDirectionArrows(g, pos, endpt, brush, spacing, arrowSize);
}
else if (code.Type == CodeType.ArcMove)
{
var arc = (ArcMove)code;
if (!arc.Suppressed)
{
var center = pgm.Mode == Mode.Incremental
? arc.CenterPoint + pos
: arc.CenterPoint;
DrawArcDirectionArrows(g, pos, endpt, center, arc.Rotation, brush, spacing, arrowSize);
}
}
pos = endpt;
}
}
private void DrawLineDirectionArrows(Graphics g, Vector start, Vector end,
Brush brush, double spacing, float arrowSize)
{
var dx = end.X - start.X;
var dy = end.Y - start.Y;
var length = System.Math.Sqrt(dx * dx + dy * dy);
if (length < spacing * 0.5) return;
var dirX = dx / length;
var dirY = dy / length;
var count = System.Math.Max(1, (int)(length / spacing));
var step = length / (count + 1);
for (var i = 1; i <= count; i++)
{
var t = step * i;
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);
}
}
private void DrawArcDirectionArrows(Graphics g, Vector start, Vector end, Vector center,
RotationType rotation, Brush brush, double spacing, float arrowSize)
{
var radius = center.DistanceTo(start);
if (radius < Tolerance.Epsilon) return;
var startAngle = System.Math.Atan2(start.Y - center.Y, start.X - center.X);
var endAngle = System.Math.Atan2(end.Y - center.Y, end.X - center.X);
double sweep;
if (rotation == RotationType.CCW)
{
sweep = endAngle - startAngle;
if (sweep <= 0) sweep += 2 * System.Math.PI;
}
else
{
sweep = startAngle - endAngle;
if (sweep <= 0) sweep += 2 * System.Math.PI;
}
var arcLength = radius * System.Math.Abs(sweep);
if (arcLength < spacing * 0.5) return;
var count = System.Math.Max(1, (int)(arcLength / spacing));
var stepAngle = sweep / (count + 1);
for (var i = 1; i <= count; i++)
{
double angle;
if (rotation == RotationType.CCW)
angle = startAngle + stepAngle * i;
else
angle = startAngle - stepAngle * i;
var pt = new Vector(
center.X + radius * System.Math.Cos(angle),
center.Y + radius * System.Math.Sin(angle));
var screenPt = view.PointWorldToGraph(pt);
double tangent;
if (rotation == RotationType.CCW)
tangent = angle + System.Math.PI / 2;
else
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);
}
}
private static void DrawArrowHead(Graphics g, Brush brush, PointF tip, double angle, float size)
{
var sin = (float)System.Math.Sin(angle);
var cos = (float)System.Math.Cos(angle);
var backX = -size * cos;
var backY = -size * sin;
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),
};
g.FillPolygon(brush, points);
}
private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen)
{
var point1 = view.PointWorldToGraph(pt1);
var point2 = view.PointWorldToGraph(pt2);
g.DrawLine(pen, point1, point2);
}
private void DrawBox(Graphics g, Box box)
{
var rect = new RectangleF
{
Location = view.PointWorldToGraph(box.Location),
Width = view.LengthWorldToGui(box.Width),
Height = view.LengthWorldToGui(box.Length)
};
g.DrawRectangle(view.ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);
}
}
}
+25 -490
View File
@@ -1,5 +1,4 @@
using OpenNest.Actions;
using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Collections;
using OpenNest.Engine.Fill;
@@ -41,6 +40,7 @@ namespace OpenNest.Controls
private Point middleMouseDownPoint;
private Box activeWorkArea;
private List<Box> debugRemnants;
private PlateRenderer renderer;
public Box ActiveWorkArea
{
@@ -114,6 +114,7 @@ namespace OpenNest.Controls
DrawBounds = true;
DrawOffset = false;
FillParts = true;
renderer = new PlateRenderer(this);
SetAction(typeof(ActionSelect));
UpdateMatrix();
@@ -137,12 +138,30 @@ namespace OpenNest.Controls
public bool DrawOffset { get; set; }
public bool DrawCutDirection { get; set; }
public bool ShowBendLines { get; set; }
public double OffsetTolerance { get; set; } = 0.001;
public bool FillParts { get; set; }
internal List<LayoutPart> LayoutParts => parts;
internal IReadOnlyList<LayoutPart> PreviewParts =>
activeParts.Count > 0 ? activeParts : stationaryParts;
internal Brush PreviewBrush =>
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartBrush : ColorScheme.PreviewPartBrush;
internal Pen PreviewPen =>
activeParts.Count > 0 ? ColorScheme.ActivePreviewPartPen : ColorScheme.PreviewPartPen;
internal RectangleF GetViewBounds() =>
new RectangleF(-origin.X, -origin.Y, Width, Height);
internal PlateRenderer Renderer => renderer;
public CutOffSettings CutOffSettings
{
get => cutOffSettings;
@@ -465,11 +484,11 @@ namespace OpenNest.Controls
e.Graphics.TranslateTransform(origin.X, origin.Y);
DrawPlate(e.Graphics);
DrawParts(e.Graphics);
DrawCutOffs(e.Graphics);
DrawActiveWorkArea(e.Graphics);
DrawDebugRemnants(e.Graphics);
renderer.DrawPlate(e.Graphics);
renderer.DrawParts(e.Graphics);
renderer.DrawCutOffs(e.Graphics);
renderer.DrawActiveWorkArea(e.Graphics);
renderer.DrawDebugRemnants(e.Graphics);
base.OnPaint(e);
}
@@ -494,284 +513,6 @@ namespace OpenNest.Controls
Invalidate();
}
protected void DrawPlate(Graphics g)
{
var plateRect = new RectangleF
{
Width = LengthWorldToGui(Plate.Size.Length),
Height = LengthWorldToGui(Plate.Size.Width)
};
var edgeSpacingRect = new RectangleF
{
Width = LengthWorldToGui(Plate.Size.Length - Plate.EdgeSpacing.Left - Plate.EdgeSpacing.Right),
Height = LengthWorldToGui(Plate.Size.Width - Plate.EdgeSpacing.Top - Plate.EdgeSpacing.Bottom)
};
switch (Plate.Quadrant)
{
case 1:
plateRect.Location = PointWorldToGraph(0, 0);
edgeSpacingRect.Location = PointWorldToGraph(
Plate.EdgeSpacing.Left,
Plate.EdgeSpacing.Bottom);
break;
case 2:
plateRect.Location = PointWorldToGraph(-Plate.Size.Length, 0);
edgeSpacingRect.Location = PointWorldToGraph(
Plate.EdgeSpacing.Left - Plate.Size.Length,
Plate.EdgeSpacing.Bottom);
break;
case 3:
plateRect.Location = PointWorldToGraph(-Plate.Size.Length, -Plate.Size.Width);
edgeSpacingRect.Location = PointWorldToGraph(
Plate.EdgeSpacing.Left - Plate.Size.Length,
Plate.EdgeSpacing.Bottom - Plate.Size.Width);
break;
case 4:
plateRect.Location = PointWorldToGraph(0, -Plate.Size.Width);
edgeSpacingRect.Location = PointWorldToGraph(
Plate.EdgeSpacing.Left,
Plate.EdgeSpacing.Bottom - Plate.Size.Width);
break;
default:
return;
}
plateRect.Y -= plateRect.Height;
edgeSpacingRect.Y -= edgeSpacingRect.Height;
g.FillRectangle(ColorScheme.LayoutFillBrush, plateRect);
var viewBounds = new RectangleF(-origin.X, -origin.Y, Width, Height);
if (!edgeSpacingRect.Contains(viewBounds))
{
g.DrawRectangle(ColorScheme.EdgeSpacingPen,
edgeSpacingRect.X,
edgeSpacingRect.Y,
edgeSpacingRect.Width,
edgeSpacingRect.Height);
}
g.DrawRectangle(ColorScheme.LayoutOutlinePen,
plateRect.X,
plateRect.Y,
plateRect.Width,
plateRect.Height);
}
protected void DrawParts(Graphics g)
{
var viewBounds = new RectangleF(-origin.X, -origin.Y, Width, Height);
for (int i = 0; i < parts.Count; ++i)
{
var part = parts[i];
if (part.IsDirty)
part.Update(this);
var path = part.Path;
var pathBounds = path.GetBounds();
if (!pathBounds.IntersectsWith(viewBounds))
continue;
part.Draw(g, (i + 1).ToString());
DrawBendLines(g, part.BasePart);
DrawEtchMarks(g, part.BasePart);
DrawGrainWarning(g, part.BasePart);
}
// Draw preview parts — active (current strategy) takes precedence
// over stationary (overall best) to avoid overlapping fills.
var previewParts = activeParts.Count > 0 ? activeParts : stationaryParts;
var previewBrush = activeParts.Count > 0 ? ColorScheme.ActivePreviewPartBrush : ColorScheme.PreviewPartBrush;
var previewPen = activeParts.Count > 0 ? ColorScheme.ActivePreviewPartPen : ColorScheme.PreviewPartPen;
for (var i = 0; i < previewParts.Count; i++)
{
var part = previewParts[i];
if (part.IsDirty)
part.Update(this);
var path = part.Path;
if (!path.GetBounds().IntersectsWith(viewBounds))
continue;
g.FillPath(previewBrush, path);
g.DrawPath(previewPen, path);
}
if (DrawOffset && Plate.PartSpacing > 0)
DrawOffsetGeometry(g);
if (DrawBounds)
{
var bounds = SelectedParts.Select(p => p.BasePart).ToList().GetBoundingBox();
DrawBox(g, bounds);
}
if (DrawRapid)
DrawRapids(g);
if (DrawPiercePoints)
DrawAllPiercePoints(g);
}
private void DrawBendLines(Graphics g, Part part)
{
if (!ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
using var bendPen = new Pen(Color.Yellow, 1.5f)
{
DashStyle = System.Drawing.Drawing2D.DashStyle.Dash
};
foreach (var bend in part.BaseDrawing.Bends)
{
var start = bend.StartPoint;
var end = bend.EndPoint;
// Apply part rotation
if (part.Rotation != 0)
{
start = start.Rotate(part.Rotation);
end = end.Rotate(part.Rotation);
}
// Apply part offset
start = start + part.Location;
end = end + part.Location;
var pt1 = PointWorldToGraph(start);
var pt2 = PointWorldToGraph(end);
g.DrawLine(bendPen, pt1, pt2);
}
}
private void DrawEtchMarks(Graphics g, Part part)
{
if (!ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
using var etchPen = new Pen(Color.Green, 1.5f);
var etchLength = 1.0;
foreach (var bend in part.BaseDrawing.Bends)
{
if (bend.Direction != BendDirection.Up)
continue;
var start = bend.StartPoint;
var end = bend.EndPoint;
// Apply part rotation
if (part.Rotation != 0)
{
start = start.Rotate(part.Rotation);
end = end.Rotate(part.Rotation);
}
// Apply part offset
start = start + part.Location;
end = end + part.Location;
var length = bend.Length;
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
if (length < etchLength * 3.0)
{
var pt1 = PointWorldToGraph(start);
var pt2 = PointWorldToGraph(end);
g.DrawLine(etchPen, pt1, pt2);
}
else
{
var dx = System.Math.Cos(angle) * etchLength;
var dy = System.Math.Sin(angle) * etchLength;
var s1 = PointWorldToGraph(start);
var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
g.DrawLine(etchPen, s1, e1);
var s2 = PointWorldToGraph(end);
var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
g.DrawLine(etchPen, s2, e2);
}
}
}
private void DrawGrainWarning(Graphics g, Part part)
{
if (!ShowBendLines || Plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
var grainAngle = Plate.GrainAngle;
var tolerance = Angle.ToRadians(5);
foreach (var bend in part.BaseDrawing.Bends)
{
var bendAngle = bend.LineAngle + part.Rotation;
bendAngle = bendAngle % System.Math.PI;
if (bendAngle < 0) bendAngle += System.Math.PI;
var grainNormalized = grainAngle % System.Math.PI;
if (grainNormalized < 0) grainNormalized += System.Math.PI;
var diff = System.Math.Abs(bendAngle - grainNormalized);
diff = System.Math.Min(diff, System.Math.PI - diff);
if (diff > tolerance)
{
var box = part.BaseDrawing.Program.BoundingBox();
var location = part.Location;
var pt1 = PointWorldToGraph(location);
var pt2 = PointWorldToGraph(new Vector(
location.X + box.Width, location.Y + box.Length));
using var warnPen = new Pen(Color.FromArgb(180, 255, 140, 0), 2f);
g.DrawRectangle(warnPen, pt1.X, pt2.Y,
System.Math.Abs(pt2.X - pt1.X), System.Math.Abs(pt2.Y - pt1.Y));
return;
}
}
}
private void DrawCutOffs(Graphics g)
{
if (Plate?.CutOffs == null || Plate.CutOffs.Count == 0)
return;
using var pen = new Pen(Color.FromArgb(64, 64, 64), 1.5f);
using var selectedPen = new Pen(Color.FromArgb(0, 120, 255), 3.5f);
foreach (var cutoff in Plate.CutOffs)
{
var program = cutoff.Drawing?.Program;
if (program == null || program.Codes.Count == 0)
continue;
var activePen = cutoff == selectedCutOff ? selectedPen : pen;
for (var i = 0; i < program.Codes.Count - 1; i += 2)
{
if (program.Codes[i] is RapidMove rapid &&
program.Codes[i + 1] is LinearMove linear)
{
DrawLine(g, rapid.EndPoint, linear.EndPoint, activePen);
}
}
}
}
public CutOff GetCutOffAtPoint(Vector point, double tolerance)
{
if (Plate?.CutOffs == null)
@@ -798,212 +539,6 @@ namespace OpenNest.Controls
return null;
}
private void DrawOffsetGeometry(Graphics g)
{
using (var offsetPen = new Pen(Color.FromArgb(120, 255, 100, 100)))
{
for (var i = 0; i < parts.Count; i++)
{
var layoutPart = parts[i];
if (layoutPart.IsDirty)
layoutPart.Update(this);
layoutPart.UpdateOffset(Plate.PartSpacing, OffsetTolerance, Matrix);
if (layoutPart.OffsetPath != null)
g.DrawPath(offsetPen, layoutPart.OffsetPath);
}
}
}
private void DrawRapids(Graphics g)
{
var pos = new Vector(0, 0);
for (int i = 0; i < Plate.Parts.Count; ++i)
{
var part = Plate.Parts[i];
var pgm = part.Program;
DrawLine(g, pos, part.Location, ColorScheme.RapidPen);
pos = part.Location;
DrawRapids(g, pgm, ref pos);
}
}
private void DrawRapids(Graphics g, Program pgm, ref Vector pos)
{
for (int i = 0; i < pgm.Length; ++i)
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
{
var subpgm = (SubProgramCall)code;
var program = subpgm.Program;
if (program != null)
DrawRapids(g, program, ref pos);
}
else
{
var motion = code as Motion;
if (motion != null)
{
if (pgm.Mode == Mode.Incremental)
{
var endpt = motion.EndPoint + pos;
if (code.Type == CodeType.RapidMove)
DrawLine(g, pos, endpt, ColorScheme.RapidPen);
pos = endpt;
}
else
{
if (code.Type == CodeType.RapidMove)
DrawLine(g, pos, motion.EndPoint, ColorScheme.RapidPen);
pos = motion.EndPoint;
}
}
}
}
}
private void DrawAllPiercePoints(Graphics g)
{
using var brush = new SolidBrush(Color.Red);
using var pen = new Pen(Color.DarkRed, 1f);
for (var i = 0; i < Plate.Parts.Count; ++i)
{
var part = Plate.Parts[i];
var pgm = part.Program;
var pos = part.Location;
DrawProgramPiercePoints(g, pgm, ref pos, brush, pen);
}
}
private void DrawProgramPiercePoints(Graphics g, Program pgm, ref Vector pos, Brush brush, Pen pen)
{
for (var i = 0; i < pgm.Length; ++i)
{
var code = pgm[i];
if (code.Type == CodeType.SubProgramCall)
{
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
DrawProgramPiercePoints(g, subpgm.Program, ref pos, brush, pen);
}
else
{
var motion = code as Motion;
if (motion == null) continue;
var endpt = pgm.Mode == Mode.Incremental
? motion.EndPoint + pos
: motion.EndPoint;
if (code.Type == CodeType.RapidMove)
{
var pt = PointWorldToGraph(endpt);
var radius = 2f;
g.FillEllipse(brush, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
g.DrawEllipse(pen, pt.X - radius, pt.Y - radius, radius * 2, radius * 2);
}
pos = endpt;
}
}
}
private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen)
{
var point1 = PointWorldToGraph(pt1);
var point2 = PointWorldToGraph(pt2);
g.DrawLine(pen, point1, point2);
}
private void DrawBox(Graphics g, Box box)
{
var rect = new RectangleF
{
Location = PointWorldToGraph(box.Location),
Width = LengthWorldToGui(box.Width),
Height = LengthWorldToGui(box.Length)
};
g.DrawRectangle(ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);
}
private void DrawActiveWorkArea(Graphics g)
{
if (activeWorkArea == null)
return;
var rect = new RectangleF
{
Location = PointWorldToGraph(activeWorkArea.Location),
Width = LengthWorldToGui(activeWorkArea.Width),
Height = LengthWorldToGui(activeWorkArea.Length)
};
rect.Y -= rect.Height;
using var pen = new Pen(Color.Red, 1.5f)
{
DashStyle = DashStyle.Dash
};
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
}
// Priority 0 = green (preferred), 1 = yellow (extend), 2 = red (last resort)
private static readonly Color[] PriorityFills =
{
Color.FromArgb(60, Color.LimeGreen),
Color.FromArgb(60, Color.Gold),
Color.FromArgb(60, Color.Salmon),
};
private static readonly Color[] PriorityBorders =
{
Color.FromArgb(180, Color.Green),
Color.FromArgb(180, Color.DarkGoldenrod),
Color.FromArgb(180, Color.DarkRed),
};
private void DrawDebugRemnants(Graphics g)
{
if (debugRemnants == null || debugRemnants.Count == 0)
return;
for (var i = 0; i < debugRemnants.Count; i++)
{
var box = debugRemnants[i];
var loc = PointWorldToGraph(box.Location);
var w = LengthWorldToGui(box.Width);
var h = LengthWorldToGui(box.Length);
var rect = new RectangleF(loc.X, loc.Y - h, w, h);
var priority = DebugRemnantPriorities != null && i < DebugRemnantPriorities.Count
? System.Math.Min(DebugRemnantPriorities[i], 2)
: 0;
using var brush = new SolidBrush(PriorityFills[priority]);
g.FillRectangle(brush, rect);
using var pen = new Pen(PriorityBorders[priority], 1.5f);
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
var label = $"P{priority} {box.Width:F1}x{box.Length:F1}";
using var font = new Font("Segoe UI", 8f);
using var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
g.DrawString(label, font, Brushes.Black, rect, sf);
}
}
public LayoutPart GetPartAtControlPoint(Point pt)
{
var pt2 = PointControlToGraph(pt);
+2 -2
View File
@@ -114,7 +114,7 @@ namespace OpenNest.Forms
this.bottomPanel.Controls.Add(this.acceptButton);
this.bottomPanel.Controls.Add(this.cancelButton);
this.bottomPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
this.bottomPanel.Location = new System.Drawing.Point(0, 406);
this.bottomPanel.Location = new System.Drawing.Point(0, 466);
this.bottomPanel.Name = "bottomPanel";
this.bottomPanel.Size = new System.Drawing.Size(380, 50);
this.bottomPanel.TabIndex = 1;
@@ -125,7 +125,7 @@ namespace OpenNest.Forms
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelButton;
this.ClientSize = new System.Drawing.Size(380, 456);
this.ClientSize = new System.Drawing.Size(380, 516);
this.Controls.Add(this.tabControl);
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)));
+70 -2
View File
@@ -22,8 +22,20 @@ namespace OpenNest.Forms
private CheckBox chkTabsEnabled;
private NumericUpDown nudTabWidth;
private NumericUpDown nudPierceClearance;
public CuttingParameters Parameters { get; set; } = new CuttingParameters();
private bool hasCustomParameters;
private CuttingParameters parameters = new CuttingParameters();
public CuttingParameters Parameters
{
get => parameters;
set
{
parameters = value;
hasCustomParameters = true;
}
}
public CuttingParametersForm()
{
@@ -54,9 +66,33 @@ namespace OpenNest.Forms
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// If caller didn't provide custom parameters, try loading saved ones
if (!hasCustomParameters)
{
var json = Properties.Settings.Default.CuttingParametersJson;
if (!string.IsNullOrEmpty(json))
{
try { Parameters = CuttingParametersSerializer.Deserialize(json); }
catch { /* use defaults on corrupt data */ }
}
}
LoadFromParameters(Parameters);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
base.OnFormClosing(e);
if (DialogResult == System.Windows.Forms.DialogResult.OK)
{
var json = CuttingParametersSerializer.Serialize(BuildParameters());
Properties.Settings.Default.CuttingParametersJson = json;
Properties.Settings.Default.Save();
}
}
private static void SetupTab(TabPage tab,
out ComboBox leadInCombo, out Panel leadInPanel,
out ComboBox leadOutCombo, out Panel leadOutPanel)
@@ -164,6 +200,35 @@ namespace OpenNest.Forms
grpTabs.Controls.Add(nudTabWidth);
Controls.Add(grpTabs);
var grpPierce = new GroupBox
{
Text = "Pierce",
Location = new System.Drawing.Point(4, 410),
Size = new System.Drawing.Size(372, 55),
Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
};
grpPierce.Controls.Add(new Label
{
Text = "Pierce Clearance:",
Location = new System.Drawing.Point(12, 23),
AutoSize = true
});
nudPierceClearance = new NumericUpDown
{
Location = new System.Drawing.Point(130, 20),
Size = new System.Drawing.Size(100, 22),
DecimalPlaces = 4,
Increment = 0.0625m,
Minimum = 0,
Maximum = 9999,
Value = 0.0625m
};
grpPierce.Controls.Add(nudPierceClearance);
Controls.Add(grpPierce);
}
private void PopulateDropdowns()
@@ -305,6 +370,8 @@ namespace OpenNest.Forms
chkTabsEnabled.Checked = p.TabsEnabled;
if (p.TabConfig != null)
nudTabWidth.Value = (decimal)p.TabConfig.Size;
nudPierceClearance.Value = (decimal)p.PierceClearance;
}
private static void LoadLeadIn(ComboBox combo, Panel panel, LeadIn leadIn)
@@ -379,7 +446,8 @@ namespace OpenNest.Forms
ArcCircleLeadIn = BuildLeadIn(cboArcCircleLeadIn, pnlArcCircleLeadIn),
ArcCircleLeadOut = BuildLeadOut(cboArcCircleLeadOut, pnlArcCircleLeadOut),
TabsEnabled = chkTabsEnabled.Checked,
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value }
TabConfig = new NormalTab { Size = (double)nudTabWidth.Value },
PierceClearance = (double)nudPierceClearance.Value
};
return p;
}
@@ -0,0 +1,138 @@
using OpenNest.CNC.CuttingStrategy;
using System.Text.Json;
namespace OpenNest.Forms
{
internal static class CuttingParametersSerializer
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
public static string Serialize(CuttingParameters p)
{
var dto = new CuttingParametersDto
{
ExternalLeadIn = ToDto(p.ExternalLeadIn),
ExternalLeadOut = ToLeadOutDto(p.ExternalLeadOut),
InternalLeadIn = ToDto(p.InternalLeadIn),
InternalLeadOut = ToLeadOutDto(p.InternalLeadOut),
ArcCircleLeadIn = ToDto(p.ArcCircleLeadIn),
ArcCircleLeadOut = ToLeadOutDto(p.ArcCircleLeadOut),
TabsEnabled = p.TabsEnabled,
TabWidth = p.TabConfig?.Size ?? 0.25,
PierceClearance = p.PierceClearance
};
return JsonSerializer.Serialize(dto, JsonOptions);
}
public static CuttingParameters Deserialize(string json)
{
var dto = JsonSerializer.Deserialize<CuttingParametersDto>(json, JsonOptions);
if (dto == null)
return new CuttingParameters();
return new CuttingParameters
{
ExternalLeadIn = FromDto(dto.ExternalLeadIn),
ExternalLeadOut = FromLeadOutDto(dto.ExternalLeadOut),
InternalLeadIn = FromDto(dto.InternalLeadIn),
InternalLeadOut = FromLeadOutDto(dto.InternalLeadOut),
ArcCircleLeadIn = FromDto(dto.ArcCircleLeadIn),
ArcCircleLeadOut = FromLeadOutDto(dto.ArcCircleLeadOut),
TabsEnabled = dto.TabsEnabled,
TabConfig = new NormalTab { Size = dto.TabWidth },
PierceClearance = dto.PierceClearance
};
}
private static LeadInDto ToDto(LeadIn leadIn)
{
return leadIn switch
{
LineLeadIn line => new LeadInDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle },
ArcLeadIn arc => new LeadInDto { Type = "Arc", Radius = arc.Radius },
LineArcLeadIn la => new LeadInDto { Type = "LineArc", LineLength = la.LineLength, ArcRadius = la.ArcRadius, ApproachAngle = la.ApproachAngle },
CleanHoleLeadIn ch => new LeadInDto { Type = "CleanHole", LineLength = ch.LineLength, ArcRadius = ch.ArcRadius, Kerf = ch.Kerf },
LineLineLeadIn ll => new LeadInDto { Type = "LineLine", Length1 = ll.Length1, Angle1 = ll.ApproachAngle1, Length2 = ll.Length2, Angle2 = ll.ApproachAngle2 },
_ => new LeadInDto { Type = "None" }
};
}
private static LeadIn FromDto(LeadInDto dto)
{
if (dto == null) return new NoLeadIn();
return dto.Type switch
{
"Line" => new LineLeadIn { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
"Arc" => new ArcLeadIn { Radius = dto.Radius },
"LineArc" => new LineArcLeadIn { LineLength = dto.LineLength, ArcRadius = dto.ArcRadius, ApproachAngle = dto.ApproachAngle },
"CleanHole" => new CleanHoleLeadIn { LineLength = dto.LineLength, ArcRadius = dto.ArcRadius, Kerf = dto.Kerf },
"LineLine" => new LineLineLeadIn { Length1 = dto.Length1, ApproachAngle1 = dto.Angle1, Length2 = dto.Length2, ApproachAngle2 = dto.Angle2 },
_ => new NoLeadIn()
};
}
private static LeadOutDto ToLeadOutDto(LeadOut leadOut)
{
return leadOut switch
{
LineLeadOut line => new LeadOutDto { Type = "Line", Length = line.Length, ApproachAngle = line.ApproachAngle },
ArcLeadOut arc => new LeadOutDto { Type = "Arc", Radius = arc.Radius },
MicrotabLeadOut mt => new LeadOutDto { Type = "Microtab", GapSize = mt.GapSize },
_ => new LeadOutDto { Type = "None" }
};
}
private static LeadOut FromLeadOutDto(LeadOutDto dto)
{
if (dto == null) return new NoLeadOut();
return dto.Type switch
{
"Line" => new LineLeadOut { Length = dto.Length, ApproachAngle = dto.ApproachAngle },
"Arc" => new ArcLeadOut { Radius = dto.Radius },
"Microtab" => new MicrotabLeadOut { GapSize = dto.GapSize },
_ => new NoLeadOut()
};
}
private class CuttingParametersDto
{
public LeadInDto ExternalLeadIn { get; set; }
public LeadOutDto ExternalLeadOut { get; set; }
public LeadInDto InternalLeadIn { get; set; }
public LeadOutDto InternalLeadOut { get; set; }
public LeadInDto ArcCircleLeadIn { get; set; }
public LeadOutDto ArcCircleLeadOut { get; set; }
public bool TabsEnabled { get; set; }
public double TabWidth { get; set; }
public double PierceClearance { get; set; }
}
private class LeadInDto
{
public string Type { get; set; } = "None";
public double Length { get; set; }
public double ApproachAngle { get; set; }
public double Radius { get; set; }
public double LineLength { get; set; }
public double ArcRadius { get; set; }
public double Kerf { get; set; }
public double Length1 { get; set; }
public double Angle1 { get; set; }
public double Length2 { get; set; }
public double Angle2 { get; set; }
}
private class LeadOutDto
{
public string Type { get; set; } = "None";
public double Length { get; set; }
public double ApproachAngle { get; set; }
public double Radius { get; set; }
public double GapSize { get; set; }
}
}
}
+13 -1
View File
@@ -40,6 +40,7 @@
this.toolStripButton1 = new System.Windows.Forms.ToolStripButton();
this.btnAssignLeadIns = new System.Windows.Forms.ToolStripButton();
this.btnPlaceLeadIn = new System.Windows.Forms.ToolStripButton();
this.btnRemoveLeadIns = new System.Windows.Forms.ToolStripButton();
this.tabPage2 = new System.Windows.Forms.TabPage();
this.drawingListBox1 = new OpenNest.Controls.DrawingListBox();
this.toolStrip2 = new System.Windows.Forms.ToolStrip();
@@ -137,7 +138,8 @@
this.toolStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.toolStripButton1,
this.btnAssignLeadIns,
this.btnPlaceLeadIn});
this.btnPlaceLeadIn,
this.btnRemoveLeadIns});
this.toolStrip1.Location = new System.Drawing.Point(3, 3);
this.toolStrip1.Name = "toolStrip1";
this.toolStrip1.Size = new System.Drawing.Size(227, 31);
@@ -174,6 +176,15 @@
this.btnPlaceLeadIn.Text = "Place Lead-in";
this.btnPlaceLeadIn.Click += new System.EventHandler(this.PlaceLeadIn_Click);
//
// btnRemoveLeadIns
//
this.btnRemoveLeadIns.DisplayStyle = System.Windows.Forms.ToolStripItemDisplayStyle.Text;
this.btnRemoveLeadIns.ImageTransparentColor = System.Drawing.Color.Magenta;
this.btnRemoveLeadIns.Name = "btnRemoveLeadIns";
this.btnRemoveLeadIns.Size = new System.Drawing.Size(104, 28);
this.btnRemoveLeadIns.Text = "Remove Lead-ins";
this.btnRemoveLeadIns.Click += new System.EventHandler(this.RemoveLeadIns_Click);
//
// tabPage2
//
this.tabPage2.Controls.Add(this.drawingListBox1);
@@ -290,5 +301,6 @@
private System.Windows.Forms.ToolStripButton toolStripButton3;
private System.Windows.Forms.ToolStripButton btnAssignLeadIns;
private System.Windows.Forms.ToolStripButton btnPlaceLeadIn;
private System.Windows.Forms.ToolStripButton btnRemoveLeadIns;
}
}
+38
View File
@@ -498,6 +498,12 @@ namespace OpenNest.Forms
PlateView.Invalidate();
}
public void ToggleCutDirection()
{
PlateView.DrawCutDirection = !PlateView.DrawCutDirection;
PlateView.Invalidate();
}
public void ToggleFillParts()
{
PlateView.FillParts = !PlateView.FillParts;
@@ -738,6 +744,38 @@ namespace OpenNest.Forms
PlateView.Invalidate();
}
private void RemoveLeadIns_Click(object sender, EventArgs e)
{
if (PlateView?.Plate == null)
return;
var plate = PlateView.Plate;
var count = 0;
foreach (var part in plate.Parts)
{
if (part.HasManualLeadIns)
{
part.RemoveLeadIns();
count++;
}
}
if (count == 0)
return;
plate.CuttingParameters = null;
// Rebuild all layout part graphics
foreach (var lp in PlateView.Parts)
{
lp.IsDirty = true;
lp.Update();
}
PlateView.Invalidate();
}
private void PlaceLeadIn_Click(object sender, EventArgs e)
{
if (PlateView?.Plate == null)
+12 -2
View File
@@ -52,6 +52,7 @@
mnuViewDrawPiercePoints = new System.Windows.Forms.ToolStripMenuItem();
mnuViewDrawBounds = new System.Windows.Forms.ToolStripMenuItem();
mnuViewDrawOffset = new System.Windows.Forms.ToolStripMenuItem();
mnuViewDrawCutDirection = new System.Windows.Forms.ToolStripMenuItem();
toolStripMenuItem5 = new System.Windows.Forms.ToolStripSeparator();
mnuViewZoomTo = new System.Windows.Forms.ToolStripMenuItem();
mnuViewZoomToArea = new System.Windows.Forms.ToolStripMenuItem();
@@ -305,7 +306,7 @@
//
// mnuView
//
mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawPiercePoints, mnuViewDrawBounds, mnuViewDrawOffset, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut });
mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawPiercePoints, mnuViewDrawBounds, mnuViewDrawOffset, mnuViewDrawCutDirection, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut });
mnuView.Name = "mnuView";
mnuView.Size = new System.Drawing.Size(44, 20);
mnuView.Text = "&View";
@@ -340,7 +341,15 @@
mnuViewDrawOffset.Size = new System.Drawing.Size(222, 22);
mnuViewDrawOffset.Text = "Draw Offset";
mnuViewDrawOffset.Click += ToggleDrawOffset_Click;
//
//
// mnuViewDrawCutDirection
//
mnuViewDrawCutDirection.CheckOnClick = true;
mnuViewDrawCutDirection.Name = "mnuViewDrawCutDirection";
mnuViewDrawCutDirection.Size = new System.Drawing.Size(222, 22);
mnuViewDrawCutDirection.Text = "Draw Cut Direction";
mnuViewDrawCutDirection.Click += ToggleDrawCutDirection_Click;
//
// toolStripMenuItem5
//
toolStripMenuItem5.Name = "toolStripMenuItem5";
@@ -1144,6 +1153,7 @@
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawPiercePoints;
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawBounds;
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset;
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawCutDirection;
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
private System.Windows.Forms.ToolStripMenuItem mnuTools;
private System.Windows.Forms.ToolStripMenuItem mnuToolsMachineConfig;
+8
View File
@@ -396,6 +396,7 @@ namespace OpenNest.Forms
mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid;
mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints;
mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds;
mnuViewDrawCutDirection.Checked = activeForm.PlateView.DrawCutDirection;
statusLabel1.Text = activeForm.PlateView.Status;
}
@@ -585,6 +586,13 @@ namespace OpenNest.Forms
mnuViewDrawOffset.Checked = activeForm.PlateView.DrawOffset;
}
private void ToggleDrawCutDirection_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
activeForm.ToggleCutDirection();
mnuViewDrawCutDirection.Checked = activeForm.PlateView.DrawCutDirection;
}
private void ZoomToArea_Click(object sender, EventArgs e)
{
if (activeForm == null) return;
+56 -14
View File
@@ -135,6 +135,8 @@ namespace OpenNest
break;
case CodeType.RapidMove:
cutPath.StartFigure();
leadPath.StartFigure();
AddLine(cutPath, (RapidMove)code, mode, ref curpos);
break;
@@ -203,12 +205,6 @@ namespace OpenNest
size, size,
(float)startAngle,
(float)sweepAngle);
if (arc.Layer == LayerType.Leadin || arc.Layer == LayerType.Leadout)
{
path.AddArc(pt.X, pt.Y, size, size, (float)(-startAngle + sweepAngle), (float)-sweepAngle);
path.CloseFigure();
}
}
curpos = endpt;
@@ -226,9 +222,6 @@ namespace OpenNest
path.AddLine(pt1, pt2);
if (line.Layer == LayerType.Leadin || line.Layer == LayerType.Leadout)
path.CloseFigure();
curpos = pt;
}
@@ -239,14 +232,23 @@ namespace OpenNest
if (mode == Mode.Incremental)
pt += curpos;
path.CloseFigure();
curpos = pt;
}
private static void AddProgram(GraphicsPath path, Program pgm, Mode mode, ref Vector curpos)
{
mode = pgm.Mode;
GraphicsPath currentFigure = null;
void Flush()
{
if (currentFigure != null)
{
path.AddPath(currentFigure, false);
currentFigure.Dispose();
currentFigure = null;
}
}
for (int i = 0; i < pgm.Length; ++i)
{
@@ -255,25 +257,54 @@ namespace OpenNest
switch (code.Type)
{
case CodeType.ArcMove:
AddArc(path, (ArcMove)code, mode, ref curpos);
{
var arc = (ArcMove)code;
if (arc.Layer != LayerType.Leadin && arc.Layer != LayerType.Leadout)
{
if (currentFigure == null) currentFigure = new GraphicsPath();
AddArc(currentFigure, arc, mode, ref curpos);
}
else
{
Flush();
var endpt = arc.EndPoint;
if (mode == Mode.Incremental) endpt += curpos;
curpos = endpt;
}
}
break;
case CodeType.LinearMove:
AddLine(path, (LinearMove)code, mode, ref curpos);
{
var line = (LinearMove)code;
if (line.Layer != LayerType.Leadin && line.Layer != LayerType.Leadout)
{
if (currentFigure == null) currentFigure = new GraphicsPath();
AddLine(currentFigure, line, mode, ref curpos);
}
else
{
Flush();
var endpt = line.EndPoint;
if (mode == Mode.Incremental) endpt += curpos;
curpos = endpt;
}
}
break;
case CodeType.RapidMove:
Flush();
AddLine(path, (RapidMove)code, mode, ref curpos);
break;
case CodeType.SubProgramCall:
{
Flush();
var tmpmode = mode;
var subpgm = (SubProgramCall)code;
if (subpgm.Program != null)
{
path.StartFigure();
AddProgram(path, subpgm.Program, mode, ref curpos);
}
@@ -282,6 +313,8 @@ namespace OpenNest
}
}
}
Flush();
}
private static void AddArc(GraphicsPath path, Arc arc)
@@ -331,6 +364,15 @@ namespace OpenNest
{
foreach (var entity in shape.Entities)
{
if (entity.Layer != null)
{
if (string.Equals(entity.Layer.Name, SpecialLayers.Leadin.Name, System.StringComparison.OrdinalIgnoreCase) ||
string.Equals(entity.Layer.Name, SpecialLayers.Leadout.Name, System.StringComparison.OrdinalIgnoreCase))
{
continue;
}
}
switch (entity.Type)
{
case EntityType.Arc:
+12
View File
@@ -226,5 +226,17 @@ namespace OpenNest.Properties {
this["DisabledStrategies"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string CuttingParametersJson {
get {
return ((string)(this["CuttingParametersJson"]));
}
set {
this["CuttingParametersJson"] = value;
}
}
}
}
+3
View File
@@ -53,5 +53,8 @@
<Setting Name="DisabledStrategies" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="CuttingParametersJson" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
</Settings>
</SettingsFile>
+3
View File
@@ -56,6 +56,9 @@
<setting name="LastPierceTime" serializeAs="String">
<value>0</value>
</setting>
<setting name="CuttingParametersJson" serializeAs="String">
<value/>
</setting>
</OpenNest.Properties.Settings>
<OpenNest.Resources.Settings>
<setting name="MainFormLocation" serializeAs="String">