Compare commits
13 Commits
f208569e72
...
f46bcd4e4b
| Author | SHA1 | Date | |
|---|---|---|---|
| f46bcd4e4b | |||
| f29f086080 | |||
| 19001ea5be | |||
| 269746b8a4 | |||
| 35218a7435 | |||
| bd973c5f79 | |||
| d042bd1844 | |||
| ebdd489fdc | |||
| 885dec5f0e | |||
| 6106df929e | |||
| 965b9c8c1a | |||
| 98e90cc176 | |||
| d9005cccc3 |
@@ -12,6 +12,9 @@ namespace OpenNest.Bending
|
|||||||
public double? Radius { get; set; }
|
public double? Radius { get; set; }
|
||||||
public string NoteText { get; set; }
|
public string NoteText { get; set; }
|
||||||
|
|
||||||
|
[System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
public Entity SourceEntity { get; set; }
|
||||||
|
|
||||||
public double Length => StartPoint.DistanceTo(EndPoint);
|
public double Length => StartPoint.DistanceTo(EndPoint);
|
||||||
|
|
||||||
public double AngleRadians => Angle.HasValue
|
public double AngleRadians => Angle.HasValue
|
||||||
|
|||||||
@@ -133,17 +133,30 @@ namespace OpenNest.Geometry
|
|||||||
if (!arc1.Radius.IsEqualTo(arc2.Radius))
|
if (!arc1.Radius.IsEqualTo(arc2.Radius))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (arc1.StartAngle > arc1.EndAngle)
|
var start1 = arc1.StartAngle;
|
||||||
arc1.StartAngle -= Angle.TwoPI;
|
var end1 = arc1.EndAngle;
|
||||||
|
var start2 = arc2.StartAngle;
|
||||||
|
var end2 = arc2.EndAngle;
|
||||||
|
|
||||||
if (arc2.StartAngle > arc2.EndAngle)
|
if (start1 > end1)
|
||||||
arc2.StartAngle -= Angle.TwoPI;
|
start1 -= Angle.TwoPI;
|
||||||
|
|
||||||
if (arc1.EndAngle < arc2.StartAngle || arc1.StartAngle > arc2.EndAngle)
|
if (start2 > end2)
|
||||||
|
start2 -= Angle.TwoPI;
|
||||||
|
|
||||||
|
// Check that arcs are adjacent (endpoints touch), not overlapping
|
||||||
|
var touch1 = end1.IsEqualTo(start2) || (end1 + Angle.TwoPI).IsEqualTo(start2);
|
||||||
|
var touch2 = end2.IsEqualTo(start1) || (end2 + Angle.TwoPI).IsEqualTo(start1);
|
||||||
|
if (!touch1 && !touch2)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var startAngle = arc1.StartAngle < arc2.StartAngle ? arc1.StartAngle : arc2.StartAngle;
|
var startAngle = start1 < start2 ? start1 : start2;
|
||||||
var endAngle = arc1.EndAngle > arc2.EndAngle ? arc1.EndAngle : arc2.EndAngle;
|
var endAngle = end1 > end2 ? end1 : end2;
|
||||||
|
|
||||||
|
// Don't merge if the result would be a full circle (start == end)
|
||||||
|
var sweep = endAngle - startAngle;
|
||||||
|
if (sweep >= Angle.TwoPI - Tolerance.Epsilon)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (startAngle < 0) startAngle += Angle.TwoPI;
|
if (startAngle < 0) startAngle += Angle.TwoPI;
|
||||||
if (endAngle < 0) endAngle += Angle.TwoPI;
|
if (endAngle < 0) endAngle += Angle.TwoPI;
|
||||||
|
|||||||
@@ -5,17 +5,17 @@ namespace OpenNest.Shapes
|
|||||||
{
|
{
|
||||||
public class RectangleShape : ShapeDefinition
|
public class RectangleShape : ShapeDefinition
|
||||||
{
|
{
|
||||||
|
public double Length { get; set; }
|
||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
public double Height { get; set; }
|
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>
|
var entities = new List<Entity>
|
||||||
{
|
{
|
||||||
new Line(0, 0, Width, 0),
|
new Line(0, 0, Length, 0),
|
||||||
new Line(Width, 0, Width, Height),
|
new Line(Length, 0, Length, Width),
|
||||||
new Line(Width, Height, 0, Height),
|
new Line(Length, Width, 0, Width),
|
||||||
new Line(0, Height, 0, 0)
|
new Line(0, Width, 0, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
return CreateDrawing(entities);
|
return CreateDrawing(entities);
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ namespace OpenNest.Shapes
|
|||||||
{
|
{
|
||||||
public class RoundedRectangleShape : ShapeDefinition
|
public class RoundedRectangleShape : ShapeDefinition
|
||||||
{
|
{
|
||||||
|
public double Length { get; set; }
|
||||||
public double Width { get; set; }
|
public double Width { get; set; }
|
||||||
public double Height { get; set; }
|
|
||||||
public double Radius { get; set; }
|
public double Radius { get; set; }
|
||||||
|
|
||||||
public override Drawing GetDrawing()
|
public override Drawing GetDrawing()
|
||||||
@@ -17,36 +17,36 @@ namespace OpenNest.Shapes
|
|||||||
|
|
||||||
if (r <= 0)
|
if (r <= 0)
|
||||||
{
|
{
|
||||||
entities.Add(new Line(0, 0, Width, 0));
|
entities.Add(new Line(0, 0, Length, 0));
|
||||||
entities.Add(new Line(Width, 0, Width, Height));
|
entities.Add(new Line(Length, 0, Length, Width));
|
||||||
entities.Add(new Line(Width, Height, 0, Height));
|
entities.Add(new Line(Length, Width, 0, Width));
|
||||||
entities.Add(new Line(0, Height, 0, 0));
|
entities.Add(new Line(0, Width, 0, 0));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Bottom edge (left to right, above bottom-left arc to bottom-right arc)
|
// Bottom edge (left to right, above bottom-left arc to bottom-right arc)
|
||||||
entities.Add(new Line(r, 0, Width - r, 0));
|
entities.Add(new Line(r, 0, Length - r, 0));
|
||||||
|
|
||||||
// Bottom-right corner arc: center at (Width-r, r), from 270deg to 360deg
|
// Bottom-right corner arc: center at (Length-r, r), from 270deg to 360deg
|
||||||
entities.Add(new Arc(Width - r, r, r,
|
entities.Add(new Arc(Length - r, r, r,
|
||||||
Angle.ToRadians(270), Angle.ToRadians(360)));
|
Angle.ToRadians(270), Angle.ToRadians(360)));
|
||||||
|
|
||||||
// Right edge
|
// Right edge
|
||||||
entities.Add(new Line(Width, r, Width, Height - r));
|
entities.Add(new Line(Length, r, Length, Width - r));
|
||||||
|
|
||||||
// Top-right corner arc: center at (Width-r, Height-r), from 0deg to 90deg
|
// Top-right corner arc: center at (Length-r, Width-r), from 0deg to 90deg
|
||||||
entities.Add(new Arc(Width - r, Height - r, r,
|
entities.Add(new Arc(Length - r, Width - r, r,
|
||||||
Angle.ToRadians(0), Angle.ToRadians(90)));
|
Angle.ToRadians(0), Angle.ToRadians(90)));
|
||||||
|
|
||||||
// Top edge (right to left)
|
// Top edge (right to left)
|
||||||
entities.Add(new Line(Width - r, Height, r, Height));
|
entities.Add(new Line(Length - r, Width, r, Width));
|
||||||
|
|
||||||
// Top-left corner arc: center at (r, Height-r), from 90deg to 180deg
|
// Top-left corner arc: center at (r, Width-r), from 90deg to 180deg
|
||||||
entities.Add(new Arc(r, Height - r, r,
|
entities.Add(new Arc(r, Width - r, r,
|
||||||
Angle.ToRadians(90), Angle.ToRadians(180)));
|
Angle.ToRadians(90), Angle.ToRadians(180)));
|
||||||
|
|
||||||
// Left edge
|
// Left edge
|
||||||
entities.Add(new Line(0, Height - r, 0, r));
|
entities.Add(new Line(0, Width - r, 0, r));
|
||||||
|
|
||||||
// Bottom-left corner arc: center at (r, r), from 180deg to 270deg
|
// Bottom-left corner arc: center at (r, r), from 180deg to 270deg
|
||||||
entities.Add(new Arc(r, r, r,
|
entities.Add(new Arc(r, r, r,
|
||||||
|
|||||||
@@ -19,19 +19,11 @@ public static class AutoSplitCalculator
|
|||||||
if (verticalSplits < 0) verticalSplits = 0;
|
if (verticalSplits < 0) verticalSplits = 0;
|
||||||
if (horizontalSplits < 0) horizontalSplits = 0;
|
if (horizontalSplits < 0) horizontalSplits = 0;
|
||||||
|
|
||||||
if (verticalSplits > 0)
|
for (var i = 1; i <= verticalSplits; i++)
|
||||||
{
|
lines.Add(new SplitLine(partBounds.X + usableWidth * i, CutOffAxis.Vertical));
|
||||||
var spacing = partBounds.Width / (verticalSplits + 1);
|
|
||||||
for (var i = 1; i <= verticalSplits; i++)
|
|
||||||
lines.Add(new SplitLine(partBounds.X + spacing * i, CutOffAxis.Vertical));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (horizontalSplits > 0)
|
for (var i = 1; i <= horizontalSplits; i++)
|
||||||
{
|
lines.Add(new SplitLine(partBounds.Y + usableHeight * i, CutOffAxis.Horizontal));
|
||||||
var spacing = partBounds.Length / (horizontalSplits + 1);
|
|
||||||
for (var i = 1; i <= horizontalSplits; i++)
|
|
||||||
lines.Add(new SplitLine(partBounds.Y + spacing * i, CutOffAxis.Horizontal));
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public static class DrawingSplitter
|
|||||||
allEntities.AddRange(pieceEntities);
|
allEntities.AddRange(pieceEntities);
|
||||||
allEntities.AddRange(cutoutEntities);
|
allEntities.AddRange(cutoutEntities);
|
||||||
|
|
||||||
var piece = BuildPieceDrawing(drawing, allEntities, pieceIndex);
|
var piece = BuildPieceDrawing(drawing, allEntities, pieceIndex, region);
|
||||||
results.Add(piece);
|
results.Add(piece);
|
||||||
pieceIndex++;
|
pieceIndex++;
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ public static class DrawingSplitter
|
|||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Drawing BuildPieceDrawing(Drawing source, List<Entity> entities, int pieceIndex)
|
private static Drawing BuildPieceDrawing(Drawing source, List<Entity> entities, int pieceIndex, Box region)
|
||||||
{
|
{
|
||||||
var pieceBounds = entities.Select(e => e.BoundingBox).ToList().GetBoundingBox();
|
var pieceBounds = entities.Select(e => e.BoundingBox).ToList().GetBoundingBox();
|
||||||
var offsetX = -pieceBounds.X;
|
var offsetX = -pieceBounds.X;
|
||||||
@@ -98,9 +98,69 @@ public static class DrawingSplitter
|
|||||||
piece.Customer = source.Customer;
|
piece.Customer = source.Customer;
|
||||||
piece.Source = source.Source;
|
piece.Source = source.Source;
|
||||||
piece.Quantity.Required = source.Quantity.Required;
|
piece.Quantity.Required = source.Quantity.Required;
|
||||||
|
|
||||||
|
if (source.Bends != null && source.Bends.Count > 0)
|
||||||
|
{
|
||||||
|
piece.Bends = new List<Bending.Bend>();
|
||||||
|
foreach (var bend in source.Bends)
|
||||||
|
{
|
||||||
|
var clipped = ClipLineToBox(bend.StartPoint, bend.EndPoint, region);
|
||||||
|
if (clipped == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
piece.Bends.Add(new Bending.Bend
|
||||||
|
{
|
||||||
|
StartPoint = new Vector(clipped.Value.Start.X + offsetX, clipped.Value.Start.Y + offsetY),
|
||||||
|
EndPoint = new Vector(clipped.Value.End.X + offsetX, clipped.Value.End.Y + offsetY),
|
||||||
|
Direction = bend.Direction,
|
||||||
|
Angle = bend.Angle,
|
||||||
|
Radius = bend.Radius,
|
||||||
|
NoteText = bend.NoteText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return piece;
|
return piece;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clips a line segment to an axis-aligned box using Liang-Barsky algorithm.
|
||||||
|
/// Returns the clipped start/end or null if the line is entirely outside.
|
||||||
|
/// </summary>
|
||||||
|
private static (Vector Start, Vector End)? ClipLineToBox(Vector start, Vector end, Box box)
|
||||||
|
{
|
||||||
|
var dx = end.X - start.X;
|
||||||
|
var dy = end.Y - start.Y;
|
||||||
|
double t0 = 0, t1 = 1;
|
||||||
|
|
||||||
|
double[] p = { -dx, dx, -dy, dy };
|
||||||
|
double[] q = { start.X - box.Left, box.Right - start.X, start.Y - box.Bottom, box.Top - start.Y };
|
||||||
|
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
if (System.Math.Abs(p[i]) < Math.Tolerance.Epsilon)
|
||||||
|
{
|
||||||
|
if (q[i] < -Math.Tolerance.Epsilon)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var t = q[i] / p[i];
|
||||||
|
if (p[i] < 0)
|
||||||
|
t0 = System.Math.Max(t0, t);
|
||||||
|
else
|
||||||
|
t1 = System.Math.Min(t1, t);
|
||||||
|
|
||||||
|
if (t0 > t1)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var clippedStart = new Vector(start.X + t0 * dx, start.Y + t0 * dy);
|
||||||
|
var clippedEnd = new Vector(start.X + t1 * dx, start.Y + t1 * dy);
|
||||||
|
return (clippedStart, clippedEnd);
|
||||||
|
}
|
||||||
|
|
||||||
private static void DecomposeCircles(ShapeProfile profile)
|
private static void DecomposeCircles(ShapeProfile profile)
|
||||||
{
|
{
|
||||||
DecomposeCirclesInShape(profile.Perimeter);
|
DecomposeCirclesInShape(profile.Perimeter);
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ namespace OpenNest.IO.Bending
|
|||||||
@"\\[fHCTQWASpOoLlKk][^;]*;|\\P|[{}]|%%[dDpPcC]",
|
@"\\[fHCTQWASpOoLlKk][^;]*;|\\P|[{}]|%%[dDpPcC]",
|
||||||
RegexOptions.Compiled);
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Regex UnicodeEscapeRegex = new Regex(
|
||||||
|
@"\\U\+([0-9A-Fa-f]{4})",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
public List<Bend> DetectBends(CadDocument document)
|
public List<Bend> DetectBends(CadDocument document)
|
||||||
{
|
{
|
||||||
var bendLines = FindBendLines(document);
|
var bendLines = FindBendLines(document);
|
||||||
@@ -116,8 +120,15 @@ namespace OpenNest.IO.Bending
|
|||||||
if (string.IsNullOrEmpty(text))
|
if (string.IsNullOrEmpty(text))
|
||||||
return text;
|
return text;
|
||||||
|
|
||||||
|
// Convert \U+XXXX DXF unicode escapes to actual characters
|
||||||
|
var result = UnicodeEscapeRegex.Replace(text, m =>
|
||||||
|
{
|
||||||
|
var codePoint = int.Parse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
|
||||||
|
return char.ConvertFromUtf32(codePoint);
|
||||||
|
});
|
||||||
|
|
||||||
// Replace known DXF special characters
|
// Replace known DXF special characters
|
||||||
var result = text
|
result = result
|
||||||
.Replace("%%d", "°").Replace("%%D", "°")
|
.Replace("%%d", "°").Replace("%%D", "°")
|
||||||
.Replace("%%p", "±").Replace("%%P", "±")
|
.Replace("%%p", "±").Replace("%%P", "±")
|
||||||
.Replace("%%c", "⌀").Replace("%%C", "⌀");
|
.Replace("%%c", "⌀").Replace("%%C", "⌀");
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
[Description("Name for the drawing")] string name,
|
[Description("Name for the drawing")] string name,
|
||||||
[Description("Shape type: rectangle, circle, l_shape, t_shape, gcode")] string shape,
|
[Description("Shape type: rectangle, circle, l_shape, t_shape, gcode")] string shape,
|
||||||
[Description("Width of the shape (not used for circle or gcode)")] double width = 10,
|
[Description("Width of the shape (not used for circle or gcode)")] double width = 10,
|
||||||
[Description("Height of the shape (not used for circle or gcode)")] double height = 10,
|
[Description("Length of the shape (not used for circle or gcode)")] double length = 10,
|
||||||
[Description("Radius for circle shape")] double radius = 5,
|
[Description("Radius for circle shape")] double radius = 5,
|
||||||
[Description("G-code string (only used when shape is 'gcode')")] string gcode = null)
|
[Description("G-code string (only used when shape is 'gcode')")] string gcode = null)
|
||||||
{
|
{
|
||||||
@@ -131,7 +131,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
switch (shape.ToLower())
|
switch (shape.ToLower())
|
||||||
{
|
{
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
shapeDef = new RectangleShape { Name = name, Width = width, Height = height };
|
shapeDef = new RectangleShape { Name = name, Width = width, Length = length };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "circle":
|
case "circle":
|
||||||
@@ -139,11 +139,11 @@ namespace OpenNest.Mcp.Tools
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "l_shape":
|
case "l_shape":
|
||||||
shapeDef = new LShape { Name = name, Width = width, Height = height };
|
shapeDef = new LShape { Name = name, Width = width, Height = length };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "t_shape":
|
case "t_shape":
|
||||||
shapeDef = new TShape { Name = name, Width = width, Height = height };
|
shapeDef = new TShape { Name = name, Width = width, Height = length };
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "gcode":
|
case "gcode":
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace OpenNest.Mcp.Tools
|
|||||||
[Description("X origin of the area")] double x,
|
[Description("X origin of the area")] double x,
|
||||||
[Description("Y origin of the area")] double y,
|
[Description("Y origin of the area")] double y,
|
||||||
[Description("Width of the area")] double width,
|
[Description("Width of the area")] double width,
|
||||||
[Description("Height of the area")] double height,
|
[Description("Length of the area")] double length,
|
||||||
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0)
|
[Description("Maximum quantity to place (0 = unlimited)")] int quantity = 0)
|
||||||
{
|
{
|
||||||
var plate = _session.GetPlate(plateIndex);
|
var plate = _session.GetPlate(plateIndex);
|
||||||
@@ -73,14 +73,14 @@ namespace OpenNest.Mcp.Tools
|
|||||||
var countBefore = plate.Parts.Count;
|
var countBefore = plate.Parts.Count;
|
||||||
var engine = NestEngineRegistry.Create(plate);
|
var engine = NestEngineRegistry.Create(plate);
|
||||||
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
var item = new NestItem { Drawing = drawing, Quantity = quantity };
|
||||||
var area = new Box(x, y, width, height);
|
var area = new Box(x, y, width, length);
|
||||||
var success = engine.Fill(item, area);
|
var success = engine.Fill(item, area);
|
||||||
|
|
||||||
var countAfter = plate.Parts.Count;
|
var countAfter = plate.Parts.Count;
|
||||||
var added = countAfter - countBefore;
|
var added = countAfter - countBefore;
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine($"Fill area ({x:F1},{y:F1} {width:F1}x{height:F1}) on plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}");
|
sb.AppendLine($"Fill area ({x:F1},{y:F1} {width:F1}x{length:F1}) on plate {plateIndex} with '{drawingName}': {(success ? "success" : "failed")}");
|
||||||
sb.AppendLine($" Parts added: {added}");
|
sb.AppendLine($" Parts added: {added}");
|
||||||
sb.AppendLine($" Total parts: {countAfter}");
|
sb.AppendLine($" Total parts: {countAfter}");
|
||||||
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
|
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ namespace OpenNest.Mcp.Tools
|
|||||||
[Description("Create a new plate with the given dimensions and spacing. Returns plate index and work area.")]
|
[Description("Create a new plate with the given dimensions and spacing. Returns plate index and work area.")]
|
||||||
public string CreatePlate(
|
public string CreatePlate(
|
||||||
[Description("Plate width")] double width,
|
[Description("Plate width")] double width,
|
||||||
[Description("Plate height")] double height,
|
[Description("Plate length")] double length,
|
||||||
[Description("Spacing between parts (default 0)")] double partSpacing = 0,
|
[Description("Spacing between parts (default 0)")] double partSpacing = 0,
|
||||||
[Description("Edge spacing on all sides (default 0)")] double edgeSpacing = 0,
|
[Description("Edge spacing on all sides (default 0)")] double edgeSpacing = 0,
|
||||||
[Description("Quadrant 1-4 (default 1). 1=TopRight, 2=TopLeft, 3=BottomLeft, 4=BottomRight")] int quadrant = 1,
|
[Description("Quadrant 1-4 (default 1). 1=TopRight, 2=TopLeft, 3=BottomLeft, 4=BottomRight")] int quadrant = 1,
|
||||||
[Description("Material name (optional)")] string material = null)
|
[Description("Material name (optional)")] string material = null)
|
||||||
{
|
{
|
||||||
var plate = new Plate(width, height);
|
var plate = new Plate(width, length);
|
||||||
plate.PartSpacing = partSpacing;
|
plate.PartSpacing = partSpacing;
|
||||||
plate.EdgeSpacing = new Spacing(edgeSpacing, edgeSpacing);
|
plate.EdgeSpacing = new Spacing(edgeSpacing, edgeSpacing);
|
||||||
plate.Quadrant = quadrant;
|
plate.Quadrant = quadrant;
|
||||||
|
|||||||
@@ -87,4 +87,30 @@ public class BendModelTests
|
|||||||
Assert.Contains("90", str);
|
Assert.Contains("90", str);
|
||||||
Assert.Contains("0.06", str);
|
Assert.Contains("0.06", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SourceEntity_DefaultsToNull()
|
||||||
|
{
|
||||||
|
var bend = new Bend
|
||||||
|
{
|
||||||
|
StartPoint = new Vector(0, 0),
|
||||||
|
EndPoint = new Vector(10, 0),
|
||||||
|
Direction = BendDirection.Down,
|
||||||
|
Angle = 90
|
||||||
|
};
|
||||||
|
Assert.Null(bend.SourceEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SourceEntity_StoresReference()
|
||||||
|
{
|
||||||
|
var line = new Line(new Vector(0, 0), new Vector(10, 0));
|
||||||
|
var bend = new Bend
|
||||||
|
{
|
||||||
|
StartPoint = line.StartPoint,
|
||||||
|
EndPoint = line.EndPoint,
|
||||||
|
SourceEntity = line
|
||||||
|
};
|
||||||
|
Assert.Same(line, bend.SourceEntity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using ACadSharp.IO;
|
||||||
using OpenNest.Bending;
|
using OpenNest.Bending;
|
||||||
using OpenNest.IO.Bending;
|
using OpenNest.IO.Bending;
|
||||||
|
|
||||||
@@ -27,4 +28,27 @@ public class SolidWorksBendDetectorTests
|
|||||||
var bends = BendDetectorRegistry.AutoDetect(doc);
|
var bends = BendDetectorRegistry.AutoDetect(doc);
|
||||||
Assert.Empty(bends);
|
Assert.Empty(bends);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DetectBends_RealDxf_ParsesNotesCorrectly()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(AppContext.BaseDirectory, "Bending", "TestData", "4526 A14 PT45.dxf");
|
||||||
|
Assert.True(File.Exists(path), $"Test DXF not found: {path}");
|
||||||
|
|
||||||
|
using var reader = new DxfReader(path);
|
||||||
|
var doc = reader.Read();
|
||||||
|
|
||||||
|
var detector = new SolidWorksBendDetector();
|
||||||
|
var bends = detector.DetectBends(doc);
|
||||||
|
|
||||||
|
Assert.Equal(2, bends.Count);
|
||||||
|
|
||||||
|
foreach (var bend in bends)
|
||||||
|
{
|
||||||
|
Assert.NotNull(bend.NoteText);
|
||||||
|
Assert.Equal(BendDirection.Up, bend.Direction);
|
||||||
|
Assert.Equal(90.0, bend.Angle);
|
||||||
|
Assert.Equal(0.313, bend.Radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,10 @@
|
|||||||
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
|
<ProjectReference Include="..\OpenNest.Posts.Cincinnati\OpenNest.Posts.Cincinnati.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Bending\TestData\**\*">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class RectangleShapeTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetDrawing_ReturnsDrawingWithCorrectBoundingBox()
|
public void GetDrawing_ReturnsDrawingWithCorrectBoundingBox()
|
||||||
{
|
{
|
||||||
var shape = new RectangleShape { Width = 10, Height = 5 };
|
var shape = new RectangleShape { Length = 10, Width = 5 };
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
var bbox = drawing.Program.BoundingBox();
|
var bbox = drawing.Program.BoundingBox();
|
||||||
@@ -18,7 +18,7 @@ public class RectangleShapeTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetDrawing_DefaultName_IsRectangle()
|
public void GetDrawing_DefaultName_IsRectangle()
|
||||||
{
|
{
|
||||||
var shape = new RectangleShape { Width = 10, Height = 5 };
|
var shape = new RectangleShape { Length = 10, Width = 5 };
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
Assert.Equal("Rectangle", drawing.Name);
|
Assert.Equal("Rectangle", drawing.Name);
|
||||||
@@ -27,7 +27,7 @@ public class RectangleShapeTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetDrawing_CustomName_IsUsed()
|
public void GetDrawing_CustomName_IsUsed()
|
||||||
{
|
{
|
||||||
var shape = new RectangleShape { Name = "Plate1", Width = 10, Height = 5 };
|
var shape = new RectangleShape { Name = "Plate1", Length = 10, Width = 5 };
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
Assert.Equal("Plate1", drawing.Name);
|
Assert.Equal("Plate1", drawing.Name);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class RoundedRectangleShapeTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetDrawing_BoundingBoxMatchesDimensions()
|
public void GetDrawing_BoundingBoxMatchesDimensions()
|
||||||
{
|
{
|
||||||
var shape = new RoundedRectangleShape { Width = 20, Height = 10, Radius = 2 };
|
var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 2 };
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
var bbox = drawing.Program.BoundingBox();
|
var bbox = drawing.Program.BoundingBox();
|
||||||
@@ -18,7 +18,7 @@ public class RoundedRectangleShapeTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetDrawing_AreaIsLessThanFullRectangle()
|
public void GetDrawing_AreaIsLessThanFullRectangle()
|
||||||
{
|
{
|
||||||
var shape = new RoundedRectangleShape { Width = 20, Height = 10, Radius = 2 };
|
var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 2 };
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
// Area should be less than 20*10=200 because corners are rounded
|
// Area should be less than 20*10=200 because corners are rounded
|
||||||
@@ -30,7 +30,7 @@ public class RoundedRectangleShapeTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void GetDrawing_ZeroRadius_MatchesRectangleArea()
|
public void GetDrawing_ZeroRadius_MatchesRectangleArea()
|
||||||
{
|
{
|
||||||
var shape = new RoundedRectangleShape { Width = 20, Height = 10, Radius = 0 };
|
var shape = new RoundedRectangleShape { Length = 20, Width = 10, Radius = 0 };
|
||||||
var drawing = shape.GetDrawing();
|
var drawing = shape.GetDrawing();
|
||||||
|
|
||||||
Assert.Equal(200, drawing.Area, 0.5);
|
Assert.Equal(200, drawing.Area, 0.5);
|
||||||
|
|||||||
@@ -1,30 +1,15 @@
|
|||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Shapes;
|
||||||
|
|
||||||
namespace OpenNest.Tests.Splitting;
|
namespace OpenNest.Tests.Splitting;
|
||||||
|
|
||||||
public class DrawingSplitterTests
|
public class DrawingSplitterTests
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Helper: creates a Drawing from a rectangular perimeter.
|
|
||||||
/// </summary>
|
|
||||||
private static Drawing MakeRectangleDrawing(string name, double width, double height)
|
|
||||||
{
|
|
||||||
var entities = new List<Entity>
|
|
||||||
{
|
|
||||||
new Line(new Vector(0, 0), new Vector(width, 0)),
|
|
||||||
new Line(new Vector(width, 0), new Vector(width, height)),
|
|
||||||
new Line(new Vector(width, height), new Vector(0, height)),
|
|
||||||
new Line(new Vector(0, height), new Vector(0, 0))
|
|
||||||
};
|
|
||||||
var pgm = ConvertGeometry.ToProgram(entities);
|
|
||||||
return new Drawing(name, pgm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Split_Rectangle_Vertical_ProducesTwoPieces()
|
public void Split_Rectangle_Vertical_ProducesTwoPieces()
|
||||||
{
|
{
|
||||||
var drawing = MakeRectangleDrawing("RECT", 100, 50);
|
var drawing = new RectangleShape { Name = "RECT", Length = 100, Width = 50 }.GetDrawing();
|
||||||
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
|
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
|
||||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
@@ -42,7 +27,7 @@ public class DrawingSplitterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Split_Rectangle_Horizontal_ProducesTwoPieces()
|
public void Split_Rectangle_Horizontal_ProducesTwoPieces()
|
||||||
{
|
{
|
||||||
var drawing = MakeRectangleDrawing("RECT", 100, 60);
|
var drawing = new RectangleShape { Name = "RECT", Length = 100, Width = 60 }.GetDrawing();
|
||||||
var splitLines = new List<SplitLine> { new SplitLine(30.0, CutOffAxis.Horizontal) };
|
var splitLines = new List<SplitLine> { new SplitLine(30.0, CutOffAxis.Horizontal) };
|
||||||
var parameters = new SplitParameters { Type = SplitType.Straight };
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
@@ -56,7 +41,7 @@ public class DrawingSplitterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Split_ThreePieces_NamesSequentially()
|
public void Split_ThreePieces_NamesSequentially()
|
||||||
{
|
{
|
||||||
var drawing = MakeRectangleDrawing("PART", 150, 50);
|
var drawing = new RectangleShape { Name = "PART", Length = 150, Width = 50 }.GetDrawing();
|
||||||
var splitLines = new List<SplitLine>
|
var splitLines = new List<SplitLine>
|
||||||
{
|
{
|
||||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||||
@@ -75,7 +60,7 @@ public class DrawingSplitterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Split_CopiesDrawingProperties()
|
public void Split_CopiesDrawingProperties()
|
||||||
{
|
{
|
||||||
var drawing = MakeRectangleDrawing("PART", 100, 50);
|
var drawing = new RectangleShape { Name = "PART", Length = 100, Width = 50 }.GetDrawing();
|
||||||
drawing.Color = System.Drawing.Color.Red;
|
drawing.Color = System.Drawing.Color.Red;
|
||||||
drawing.Priority = 5;
|
drawing.Priority = 5;
|
||||||
|
|
||||||
@@ -93,7 +78,7 @@ public class DrawingSplitterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Split_PiecesNormalizedToOrigin()
|
public void Split_PiecesNormalizedToOrigin()
|
||||||
{
|
{
|
||||||
var drawing = MakeRectangleDrawing("PART", 100, 50);
|
var drawing = new RectangleShape { Name = "PART", Length = 100, Width = 50 }.GetDrawing();
|
||||||
var results = DrawingSplitter.Split(drawing,
|
var results = DrawingSplitter.Split(drawing,
|
||||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||||
new SplitParameters());
|
new SplitParameters());
|
||||||
@@ -146,7 +131,7 @@ public class DrawingSplitterTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Split_GridSplit_ProducesFourPieces()
|
public void Split_GridSplit_ProducesFourPieces()
|
||||||
{
|
{
|
||||||
var drawing = MakeRectangleDrawing("GRID", 100, 100);
|
var drawing = new RectangleShape { Name = "GRID", Length = 100, Width = 100 }.GetDrawing();
|
||||||
var splitLines = new List<SplitLine>
|
var splitLines = new List<SplitLine>
|
||||||
{
|
{
|
||||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||||
@@ -160,4 +145,263 @@ public class DrawingSplitterTests
|
|||||||
Assert.Equal("GRID-3", results[2].Name);
|
Assert.Equal("GRID-3", results[2].Name);
|
||||||
Assert.Equal("GRID-4", results[3].Name);
|
Assert.Equal("GRID-4", results[3].Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_Square_Vertical_PieceWidthsSumToOriginal()
|
||||||
|
{
|
||||||
|
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
|
||||||
|
var splitLines = new List<SplitLine> { new SplitLine(40.0, CutOffAxis.Vertical) };
|
||||||
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
|
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||||
|
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
|
||||||
|
var bb1 = results[0].Program.BoundingBox();
|
||||||
|
var bb2 = results[1].Program.BoundingBox();
|
||||||
|
|
||||||
|
// Piece lengths should sum to original length
|
||||||
|
Assert.Equal(100.0, bb1.Width + bb2.Width, 1);
|
||||||
|
|
||||||
|
// Both pieces should have the same width as the original
|
||||||
|
Assert.Equal(100.0, bb1.Length, 1);
|
||||||
|
Assert.Equal(100.0, bb2.Length, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_Square_Horizontal_PieceHeightsSumToOriginal()
|
||||||
|
{
|
||||||
|
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
|
||||||
|
var splitLines = new List<SplitLine> { new SplitLine(60.0, CutOffAxis.Horizontal) };
|
||||||
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
|
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||||
|
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
|
||||||
|
var bb1 = results[0].Program.BoundingBox();
|
||||||
|
var bb2 = results[1].Program.BoundingBox();
|
||||||
|
|
||||||
|
// Piece widths should sum to original width
|
||||||
|
Assert.Equal(100.0, bb1.Length + bb2.Length, 1);
|
||||||
|
|
||||||
|
// Both pieces should have the same length as the original
|
||||||
|
Assert.Equal(100.0, bb1.Width, 1);
|
||||||
|
Assert.Equal(100.0, bb2.Width, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_Square_Vertical_AreaPreserved()
|
||||||
|
{
|
||||||
|
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
|
||||||
|
var originalArea = drawing.Area;
|
||||||
|
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
|
||||||
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
|
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||||
|
|
||||||
|
var totalArea = results.Sum(d => d.Area);
|
||||||
|
Assert.Equal(originalArea, totalArea, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_Square_Vertical_PiecesAreClosedPerimeters()
|
||||||
|
{
|
||||||
|
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
|
||||||
|
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) };
|
||||||
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
|
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||||
|
|
||||||
|
foreach (var piece in results)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(piece.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
|
|
||||||
|
Assert.True(entities.Count >= 4, $"{piece.Name} should have at least 4 entities for a rectangle");
|
||||||
|
|
||||||
|
// First entity start should connect to last entity end (closed shape)
|
||||||
|
var firstStart = GetStartPoint(entities[0]);
|
||||||
|
var lastEnd = GetEndPoint(entities[^1]);
|
||||||
|
var closingGap = firstStart.DistanceTo(lastEnd);
|
||||||
|
Assert.True(closingGap < 0.01,
|
||||||
|
$"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start");
|
||||||
|
|
||||||
|
// Consecutive entities should connect
|
||||||
|
for (var i = 0; i < entities.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var end = GetEndPoint(entities[i]);
|
||||||
|
var start = GetStartPoint(entities[i + 1]);
|
||||||
|
var gap = end.DistanceTo(start);
|
||||||
|
Assert.True(gap < 0.01,
|
||||||
|
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_Square_Horizontal_PiecesAreClosedPerimeters()
|
||||||
|
{
|
||||||
|
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
|
||||||
|
var splitLines = new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Horizontal) };
|
||||||
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
|
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||||
|
|
||||||
|
foreach (var piece in results)
|
||||||
|
{
|
||||||
|
var entities = ConvertProgram.ToGeometry(piece.Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
|
|
||||||
|
Assert.True(entities.Count >= 4, $"{piece.Name} should have at least 4 entities for a rectangle");
|
||||||
|
|
||||||
|
var firstStart = GetStartPoint(entities[0]);
|
||||||
|
var lastEnd = GetEndPoint(entities[^1]);
|
||||||
|
var closingGap = firstStart.DistanceTo(lastEnd);
|
||||||
|
Assert.True(closingGap < 0.01,
|
||||||
|
$"{piece.Name} is not closed: gap of {closingGap:F6} between last end and first start");
|
||||||
|
|
||||||
|
for (var i = 0; i < entities.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var end = GetEndPoint(entities[i]);
|
||||||
|
var start = GetStartPoint(entities[i + 1]);
|
||||||
|
var gap = end.DistanceTo(start);
|
||||||
|
Assert.True(gap < 0.01,
|
||||||
|
$"Gap of {gap:F6} between entities {i} and {i + 1} in {piece.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_Square_AsymmetricSplit_PieceDimensionsMatchSplitPosition()
|
||||||
|
{
|
||||||
|
var drawing = new RectangleShape { Name = "SQ", Length = 100, Width = 100 }.GetDrawing();
|
||||||
|
var splitLines = new List<SplitLine> { new SplitLine(30.0, CutOffAxis.Vertical) };
|
||||||
|
var parameters = new SplitParameters { Type = SplitType.Straight };
|
||||||
|
|
||||||
|
var results = DrawingSplitter.Split(drawing, splitLines, parameters);
|
||||||
|
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
|
||||||
|
var bb1 = results[0].Program.BoundingBox();
|
||||||
|
var bb2 = results[1].Program.BoundingBox();
|
||||||
|
|
||||||
|
// Left piece should be 30 long, right piece should be 70 long
|
||||||
|
Assert.Equal(30.0, bb1.Width, 1);
|
||||||
|
Assert.Equal(70.0, bb2.Width, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_CircleHole_NotOnSplitLine_PreservedInCorrectPiece()
|
||||||
|
{
|
||||||
|
// Rectangle 100x50 with a circle hole at (20, 25) radius 3
|
||||||
|
// Split vertically at x=50 — hole is entirely in the left piece
|
||||||
|
var perimeterEntities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(new Vector(0, 0), new Vector(100, 0)),
|
||||||
|
new Line(new Vector(100, 0), new Vector(100, 50)),
|
||||||
|
new Line(new Vector(100, 50), new Vector(0, 50)),
|
||||||
|
new Line(new Vector(0, 50), new Vector(0, 0))
|
||||||
|
};
|
||||||
|
var hole = new Circle(new Vector(20, 25), 3);
|
||||||
|
var allEntities = new List<Entity>();
|
||||||
|
allEntities.AddRange(perimeterEntities);
|
||||||
|
allEntities.Add(hole);
|
||||||
|
|
||||||
|
var pgm = ConvertGeometry.ToProgram(allEntities);
|
||||||
|
var drawing = new Drawing("CIRC", pgm);
|
||||||
|
|
||||||
|
var results = DrawingSplitter.Split(drawing,
|
||||||
|
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||||
|
new SplitParameters());
|
||||||
|
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
|
||||||
|
// Left piece should have the hole — verify by checking it has arc entities
|
||||||
|
var leftEntities = ConvertProgram.ToGeometry(results[0].Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
|
var leftArcs = leftEntities.OfType<Arc>().ToList();
|
||||||
|
|
||||||
|
// Decomposed circle = 2 arcs. Both should be present.
|
||||||
|
Assert.True(leftArcs.Count >= 2,
|
||||||
|
$"Left piece should have at least 2 arcs (full circle), but has {leftArcs.Count}");
|
||||||
|
|
||||||
|
// Right piece should have no arcs (hole is on the left)
|
||||||
|
var rightEntities = ConvertProgram.ToGeometry(results[1].Program)
|
||||||
|
.Where(e => e.Layer != SpecialLayers.Rapid).ToList();
|
||||||
|
var rightArcs = rightEntities.OfType<Arc>().ToList();
|
||||||
|
Assert.Equal(0, rightArcs.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Split_DxfRoundTrip_CircleHolePreserved()
|
||||||
|
{
|
||||||
|
// Two semicircular arcs (decomposed circle) must survive DXF write→reimport.
|
||||||
|
// Regression: GeometryOptimizer.TryJoinArcs merged them into a single arc
|
||||||
|
// because it incorrectly handled the wrap-around case (π→2π written as π→0°).
|
||||||
|
var perimeterEntities = new List<Entity>
|
||||||
|
{
|
||||||
|
new Line(new Vector(0, 0), new Vector(100, 0)),
|
||||||
|
new Line(new Vector(100, 0), new Vector(100, 50)),
|
||||||
|
new Line(new Vector(100, 50), new Vector(0, 50)),
|
||||||
|
new Line(new Vector(0, 50), new Vector(0, 0))
|
||||||
|
};
|
||||||
|
var hole = new Circle(new Vector(20, 25), 3);
|
||||||
|
var allEntities = new List<Entity>();
|
||||||
|
allEntities.AddRange(perimeterEntities);
|
||||||
|
allEntities.Add(hole);
|
||||||
|
|
||||||
|
var pgm = ConvertGeometry.ToProgram(allEntities);
|
||||||
|
var drawing = new Drawing("CIRC", pgm);
|
||||||
|
drawing.Bends = new List<OpenNest.Bending.Bend>();
|
||||||
|
|
||||||
|
// Split — the circle gets decomposed into two arcs
|
||||||
|
var results = DrawingSplitter.Split(drawing,
|
||||||
|
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||||
|
new SplitParameters());
|
||||||
|
|
||||||
|
Assert.Equal(2, results.Count);
|
||||||
|
|
||||||
|
// Write left piece to DXF and re-import
|
||||||
|
var tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "split_roundtrip_test.dxf");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var writer = new OpenNest.IO.SplitDxfWriter();
|
||||||
|
writer.Write(tempPath, results[0]);
|
||||||
|
|
||||||
|
var reimporter = new OpenNest.IO.DxfImporter();
|
||||||
|
var reimportResult = reimporter.Import(tempPath);
|
||||||
|
|
||||||
|
var afterArcs = reimportResult.Entities.OfType<Arc>().Count();
|
||||||
|
var afterCircles = reimportResult.Entities.OfType<Circle>().Count();
|
||||||
|
|
||||||
|
Assert.True(afterArcs + afterCircles * 2 >= 2,
|
||||||
|
$"After DXF round-trip: {afterArcs} arcs, {afterCircles} circles (expected 2+ for full hole)");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(tempPath))
|
||||||
|
System.IO.File.Delete(tempPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector GetStartPoint(Entity entity)
|
||||||
|
{
|
||||||
|
return entity switch
|
||||||
|
{
|
||||||
|
Line l => l.StartPoint,
|
||||||
|
Arc a => a.StartPoint(),
|
||||||
|
_ => new Vector(0, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector GetEndPoint(Entity entity)
|
||||||
|
{
|
||||||
|
return entity switch
|
||||||
|
{
|
||||||
|
Line l => l.EndPoint,
|
||||||
|
Arc a => a.EndPoint(),
|
||||||
|
_ => new Vector(0, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using OpenNest.Converters;
|
using OpenNest.Converters;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
|
using OpenNest.Shapes;
|
||||||
|
|
||||||
namespace OpenNest.Tests.Splitting;
|
namespace OpenNest.Tests.Splitting;
|
||||||
|
|
||||||
@@ -8,16 +9,7 @@ public class SplitIntegrationTest
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Split_SpikeGroove_NoContinuityGaps()
|
public void Split_SpikeGroove_NoContinuityGaps()
|
||||||
{
|
{
|
||||||
// Create a rectangle
|
var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
|
||||||
var entities = new List<Entity>
|
|
||||||
{
|
|
||||||
new Line(new Vector(0, 0), new Vector(100, 0)),
|
|
||||||
new Line(new Vector(100, 0), new Vector(100, 50)),
|
|
||||||
new Line(new Vector(100, 50), new Vector(0, 50)),
|
|
||||||
new Line(new Vector(0, 50), new Vector(0, 0))
|
|
||||||
};
|
|
||||||
var pgm = ConvertGeometry.ToProgram(entities);
|
|
||||||
var drawing = new Drawing("TEST", pgm);
|
|
||||||
|
|
||||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||||
sl.FeaturePositions.Add(12.5);
|
sl.FeaturePositions.Add(12.5);
|
||||||
@@ -60,15 +52,7 @@ public class SplitIntegrationTest
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Split_SpikeGroove_Horizontal_NoContinuityGaps()
|
public void Split_SpikeGroove_Horizontal_NoContinuityGaps()
|
||||||
{
|
{
|
||||||
var entities = new List<Entity>
|
var drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
|
||||||
{
|
|
||||||
new Line(new Vector(0, 0), new Vector(100, 0)),
|
|
||||||
new Line(new Vector(100, 0), new Vector(100, 50)),
|
|
||||||
new Line(new Vector(100, 50), new Vector(0, 50)),
|
|
||||||
new Line(new Vector(0, 50), new Vector(0, 0))
|
|
||||||
};
|
|
||||||
var pgm = ConvertGeometry.ToProgram(entities);
|
|
||||||
var drawing = new Drawing("TEST", pgm);
|
|
||||||
|
|
||||||
var sl = new SplitLine(25.0, CutOffAxis.Horizontal);
|
var sl = new SplitLine(25.0, CutOffAxis.Horizontal);
|
||||||
sl.FeaturePositions.Add(25.0);
|
sl.FeaturePositions.Add(25.0);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public class AutoSplitCalculatorTests
|
|||||||
|
|
||||||
Assert.Single(lines);
|
Assert.Single(lines);
|
||||||
Assert.Equal(CutOffAxis.Vertical, lines[0].Axis);
|
Assert.Equal(CutOffAxis.Vertical, lines[0].Axis);
|
||||||
Assert.Equal(50.0, lines[0].Position, 1);
|
Assert.Equal(58.0, lines[0].Position, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using OpenNest.Bending;
|
using OpenNest.Bending;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
@@ -17,6 +18,20 @@ namespace OpenNest.Controls
|
|||||||
private readonly Pen gridPen = new Pen(Color.FromArgb(70, 70, 70));
|
private readonly Pen gridPen = new Pen(Color.FromArgb(70, 70, 70));
|
||||||
private readonly Dictionary<int, Pen> penCache = new Dictionary<int, Pen>();
|
private readonly Dictionary<int, Pen> penCache = new Dictionary<int, Pen>();
|
||||||
|
|
||||||
|
public event EventHandler<Line> LinePicked;
|
||||||
|
public event EventHandler PickCancelled;
|
||||||
|
|
||||||
|
private bool isPickingBendLine;
|
||||||
|
public bool IsPickingBendLine
|
||||||
|
{
|
||||||
|
get => isPickingBendLine;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
isPickingBendLine = value;
|
||||||
|
Cursor = value ? Cursors.Hand : Cursors.Cross;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public EntityView()
|
public EntityView()
|
||||||
{
|
{
|
||||||
Entities = new List<Entity>();
|
Entities = new List<Entity>();
|
||||||
@@ -34,6 +49,13 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
base.OnMouseClick(e);
|
base.OnMouseClick(e);
|
||||||
if (!Focused) Focus();
|
if (!Focused) Focus();
|
||||||
|
|
||||||
|
if (IsPickingBendLine && e.Button == MouseButtons.Left)
|
||||||
|
{
|
||||||
|
var line = HitTestLine(e.Location);
|
||||||
|
if (line != null)
|
||||||
|
LinePicked?.Invoke(this, line);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPaint(PaintEventArgs e)
|
protected override void OnPaint(PaintEventArgs e)
|
||||||
@@ -117,6 +139,20 @@ namespace OpenNest.Controls
|
|||||||
ZoomToFit();
|
ZoomToFit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnKeyDown(e);
|
||||||
|
|
||||||
|
if (e.KeyCode == Keys.F)
|
||||||
|
ZoomToFit();
|
||||||
|
|
||||||
|
if (IsPickingBendLine && e.KeyCode == Keys.Escape)
|
||||||
|
{
|
||||||
|
IsPickingBendLine = false;
|
||||||
|
PickCancelled?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Pen GetEntityPen(Color color)
|
private Pen GetEntityPen(Color color)
|
||||||
{
|
{
|
||||||
if (color.IsEmpty || color.A == 0)
|
if (color.IsEmpty || color.A == 0)
|
||||||
@@ -158,9 +194,9 @@ namespace OpenNest.Controls
|
|||||||
{
|
{
|
||||||
DashPattern = new float[] { 8, 6 }
|
DashPattern = new float[] { 8, 6 }
|
||||||
};
|
};
|
||||||
using var selectedPen = new Pen(Color.Cyan, 2.5f)
|
using var glowPen = new Pen(Color.OrangeRed, 2.0f)
|
||||||
{
|
{
|
||||||
DashPattern = new float[] { 8, 6 }
|
DashPattern = new float[] { 6, 4 }
|
||||||
};
|
};
|
||||||
|
|
||||||
for (var i = 0; i < Bends.Count; i++)
|
for (var i = 0; i < Bends.Count; i++)
|
||||||
@@ -168,10 +204,44 @@ namespace OpenNest.Controls
|
|||||||
var bend = Bends[i];
|
var bend = Bends[i];
|
||||||
var pt1 = PointWorldToGraph(bend.StartPoint);
|
var pt1 = PointWorldToGraph(bend.StartPoint);
|
||||||
var pt2 = PointWorldToGraph(bend.EndPoint);
|
var pt2 = PointWorldToGraph(bend.EndPoint);
|
||||||
g.DrawLine(i == SelectedBendIndex ? selectedPen : bendPen, pt1, pt2);
|
|
||||||
|
if (i == SelectedBendIndex)
|
||||||
|
{
|
||||||
|
g.DrawLine(glowPen, pt1, pt2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g.DrawLine(bendPen, pt1, pt2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Line HitTestLine(Point controlPoint)
|
||||||
|
{
|
||||||
|
var worldPoint = PointControlToWorld(controlPoint);
|
||||||
|
var tolerance = LengthGuiToWorld(6);
|
||||||
|
Line bestLine = null;
|
||||||
|
var bestDistance = double.MaxValue;
|
||||||
|
|
||||||
|
foreach (var entity in Entities)
|
||||||
|
{
|
||||||
|
if (entity.Type != EntityType.Line || !entity.IsVisible)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var line = (Line)entity;
|
||||||
|
var closest = line.ClosestPointTo(worldPoint);
|
||||||
|
var distance = worldPoint.DistanceTo(closest);
|
||||||
|
|
||||||
|
if (distance < tolerance && distance < bestDistance)
|
||||||
|
{
|
||||||
|
bestLine = line;
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestLine;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace OpenNest.Controls
|
|||||||
private readonly CheckedListBox colorsList;
|
private readonly CheckedListBox colorsList;
|
||||||
private readonly CheckedListBox lineTypesList;
|
private readonly CheckedListBox lineTypesList;
|
||||||
private readonly ListBox bendLinesList;
|
private readonly ListBox bendLinesList;
|
||||||
|
private readonly LinkLabel bendAddLink;
|
||||||
|
|
||||||
private List<Entity> currentEntities;
|
private List<Entity> currentEntities;
|
||||||
private List<Bend> currentBends;
|
private List<Bend> currentBends;
|
||||||
@@ -26,6 +27,7 @@ namespace OpenNest.Controls
|
|||||||
public event EventHandler FilterChanged;
|
public event EventHandler FilterChanged;
|
||||||
public event EventHandler<int> BendLineSelected;
|
public event EventHandler<int> BendLineSelected;
|
||||||
public event EventHandler<int> BendLineRemoved;
|
public event EventHandler<int> BendLineRemoved;
|
||||||
|
public event EventHandler AddBendLineClicked;
|
||||||
|
|
||||||
public FilterPanel()
|
public FilterPanel()
|
||||||
{
|
{
|
||||||
@@ -51,9 +53,8 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
var bendDeleteLink = new LinkLabel
|
var bendDeleteLink = new LinkLabel
|
||||||
{
|
{
|
||||||
Text = "Remove Selected",
|
Text = "Remove",
|
||||||
Dock = DockStyle.Bottom,
|
AutoSize = true,
|
||||||
Height = 20,
|
|
||||||
Font = new Font("Segoe UI", 8f)
|
Font = new Font("Segoe UI", 8f)
|
||||||
};
|
};
|
||||||
bendDeleteLink.LinkClicked += (s, e) =>
|
bendDeleteLink.LinkClicked += (s, e) =>
|
||||||
@@ -62,8 +63,27 @@ namespace OpenNest.Controls
|
|||||||
BendLineRemoved?.Invoke(this, bendLinesList.SelectedIndex);
|
BendLineRemoved?.Invoke(this, bendLinesList.SelectedIndex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bendAddLink = new LinkLabel
|
||||||
|
{
|
||||||
|
Text = "Add Bend Line",
|
||||||
|
AutoSize = true,
|
||||||
|
Font = new Font("Segoe UI", 8f)
|
||||||
|
};
|
||||||
|
bendAddLink.LinkClicked += (s, e) =>
|
||||||
|
AddBendLineClicked?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
var bendLinksPanel = new FlowLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Bottom,
|
||||||
|
Height = 20,
|
||||||
|
FlowDirection = FlowDirection.LeftToRight,
|
||||||
|
WrapContents = false
|
||||||
|
};
|
||||||
|
bendLinksPanel.Controls.Add(bendAddLink);
|
||||||
|
bendLinksPanel.Controls.Add(bendDeleteLink);
|
||||||
|
|
||||||
bendLinesPanel.ContentPanel.Controls.Add(bendLinesList);
|
bendLinesPanel.ContentPanel.Controls.Add(bendLinesList);
|
||||||
bendLinesPanel.ContentPanel.Controls.Add(bendDeleteLink);
|
bendLinesPanel.ContentPanel.Controls.Add(bendLinksPanel);
|
||||||
|
|
||||||
// Line Types
|
// Line Types
|
||||||
lineTypesPanel = new CollapsiblePanel
|
lineTypesPanel = new CollapsiblePanel
|
||||||
@@ -237,6 +257,12 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
e.DrawFocusRectangle();
|
e.DrawFocusRectangle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SetPickMode(bool active)
|
||||||
|
{
|
||||||
|
bendAddLink.Text = active ? "Cancel (Esc)" : "Add Bend Line";
|
||||||
|
bendAddLink.LinkColor = active ? Color.OrangeRed : Color.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ColorItem
|
public class ColorItem
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
public bool DrawRapid { get; set; }
|
public bool DrawRapid { get; set; }
|
||||||
|
|
||||||
|
public bool DrawPiercePoints { get; set; }
|
||||||
|
|
||||||
public bool DrawBounds { get; set; }
|
public bool DrawBounds { get; set; }
|
||||||
|
|
||||||
public bool DrawOffset { get; set; }
|
public bool DrawOffset { get; set; }
|
||||||
@@ -617,6 +619,9 @@ namespace OpenNest.Controls
|
|||||||
|
|
||||||
if (DrawRapid)
|
if (DrawRapid)
|
||||||
DrawRapids(g);
|
DrawRapids(g);
|
||||||
|
|
||||||
|
if (DrawPiercePoints)
|
||||||
|
DrawAllPiercePoints(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawBendLines(Graphics g, Part part)
|
private void DrawBendLines(Graphics g, Part part)
|
||||||
@@ -813,6 +818,54 @@ namespace OpenNest.Controls
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
private void DrawLine(Graphics g, Vector pt1, Vector pt2, Pen pen)
|
||||||
{
|
{
|
||||||
var point1 = PointWorldToGraph(pt1);
|
var point1 = PointWorldToGraph(pt1);
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using OpenNest.Bending;
|
||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace OpenNest.Forms
|
||||||
|
{
|
||||||
|
public class BendLineDialog : Form
|
||||||
|
{
|
||||||
|
private readonly ComboBox cboDirection;
|
||||||
|
private readonly NumericUpDown numAngle;
|
||||||
|
private readonly NumericUpDown numRadius;
|
||||||
|
private readonly CheckBox chkRadius;
|
||||||
|
|
||||||
|
public BendLineDialog()
|
||||||
|
{
|
||||||
|
Text = "Bend Line Properties";
|
||||||
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
|
MaximizeBox = false;
|
||||||
|
MinimizeBox = false;
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
Size = new Size(260, 200);
|
||||||
|
|
||||||
|
var font = new Font("Segoe UI", 9f);
|
||||||
|
|
||||||
|
// Direction
|
||||||
|
var lblDir = new Label { Text = "Direction:", Location = new Point(12, 15), AutoSize = true, Font = font };
|
||||||
|
cboDirection = new ComboBox
|
||||||
|
{
|
||||||
|
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||||
|
Location = new Point(100, 12),
|
||||||
|
Width = 130,
|
||||||
|
Font = font
|
||||||
|
};
|
||||||
|
cboDirection.Items.AddRange(new object[] { "Down", "Up" });
|
||||||
|
cboDirection.SelectedIndex = 0;
|
||||||
|
|
||||||
|
// Angle
|
||||||
|
var lblAngle = new Label { Text = "Angle:", Location = new Point(12, 47), AutoSize = true, Font = font };
|
||||||
|
numAngle = new NumericUpDown
|
||||||
|
{
|
||||||
|
Location = new Point(100, 44),
|
||||||
|
Width = 130,
|
||||||
|
Font = font,
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = 180,
|
||||||
|
DecimalPlaces = 1,
|
||||||
|
Value = 90
|
||||||
|
};
|
||||||
|
|
||||||
|
// Radius (with checkbox to enable)
|
||||||
|
chkRadius = new CheckBox { Text = "Radius:", Location = new Point(12, 79), AutoSize = true, Font = font };
|
||||||
|
numRadius = new NumericUpDown
|
||||||
|
{
|
||||||
|
Location = new Point(100, 76),
|
||||||
|
Width = 130,
|
||||||
|
Font = font,
|
||||||
|
Minimum = 0,
|
||||||
|
Maximum = 25,
|
||||||
|
DecimalPlaces = 3,
|
||||||
|
Increment = 0.0625m,
|
||||||
|
Enabled = false
|
||||||
|
};
|
||||||
|
chkRadius.CheckedChanged += (s, e) => numRadius.Enabled = chkRadius.Checked;
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
var btnOk = new Button
|
||||||
|
{
|
||||||
|
Text = "OK",
|
||||||
|
DialogResult = DialogResult.OK,
|
||||||
|
Location = new Point(62, 120),
|
||||||
|
Size = new Size(80, 28),
|
||||||
|
Font = font
|
||||||
|
};
|
||||||
|
var btnCancel = new Button
|
||||||
|
{
|
||||||
|
Text = "Cancel",
|
||||||
|
DialogResult = DialogResult.Cancel,
|
||||||
|
Location = new Point(150, 120),
|
||||||
|
Size = new Size(80, 28),
|
||||||
|
Font = font
|
||||||
|
};
|
||||||
|
|
||||||
|
AcceptButton = btnOk;
|
||||||
|
CancelButton = btnCancel;
|
||||||
|
|
||||||
|
Controls.AddRange(new Control[] {
|
||||||
|
lblDir, cboDirection,
|
||||||
|
lblAngle, numAngle,
|
||||||
|
chkRadius, numRadius,
|
||||||
|
btnOk, btnCancel
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public BendDirection Direction => cboDirection.SelectedIndex == 0
|
||||||
|
? BendDirection.Down
|
||||||
|
: BendDirection.Up;
|
||||||
|
|
||||||
|
public double BendAngle => (double)numAngle.Value;
|
||||||
|
|
||||||
|
public double? BendRadius => chkRadius.Checked ? (double)numRadius.Value : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,9 @@ namespace OpenNest.Forms
|
|||||||
filterPanel.FilterChanged += OnFilterChanged;
|
filterPanel.FilterChanged += OnFilterChanged;
|
||||||
filterPanel.BendLineSelected += OnBendLineSelected;
|
filterPanel.BendLineSelected += OnBendLineSelected;
|
||||||
filterPanel.BendLineRemoved += OnBendLineRemoved;
|
filterPanel.BendLineRemoved += OnBendLineRemoved;
|
||||||
|
filterPanel.AddBendLineClicked += OnAddBendLineClicked;
|
||||||
|
entityView1.LinePicked += OnLinePicked;
|
||||||
|
entityView1.PickCancelled += OnPickCancelled;
|
||||||
btnSplit.Click += OnSplitClicked;
|
btnSplit.Click += OnSplitClicked;
|
||||||
numQuantity.ValueChanged += OnQuantityChanged;
|
numQuantity.ValueChanged += OnQuantityChanged;
|
||||||
txtCustomer.TextChanged += OnCustomerChanged;
|
txtCustomer.TextChanged += OnCustomerChanged;
|
||||||
@@ -132,6 +135,11 @@ namespace OpenNest.Forms
|
|||||||
private void LoadItem(FileListItem item)
|
private void LoadItem(FileListItem item)
|
||||||
{
|
{
|
||||||
entityView1.ClearPenCache();
|
entityView1.ClearPenCache();
|
||||||
|
if (entityView1.IsPickingBendLine)
|
||||||
|
{
|
||||||
|
entityView1.IsPickingBendLine = false;
|
||||||
|
filterPanel.SetPickMode(false);
|
||||||
|
}
|
||||||
entityView1.Entities.Clear();
|
entityView1.Entities.Clear();
|
||||||
entityView1.Entities.AddRange(item.Entities);
|
entityView1.Entities.AddRange(item.Entities);
|
||||||
entityView1.Bends = item.Bends ?? new List<Bend>();
|
entityView1.Bends = item.Bends ?? new List<Bend>();
|
||||||
@@ -139,6 +147,7 @@ namespace OpenNest.Forms
|
|||||||
item.Entities.ForEach(e => e.IsVisible = true);
|
item.Entities.ForEach(e => e.IsVisible = true);
|
||||||
if (item.Entities.Any(e => e.Layer != null))
|
if (item.Entities.Any(e => e.Layer != null))
|
||||||
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
||||||
|
ReHidePromotedEntities(item.Bends);
|
||||||
|
|
||||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||||
|
|
||||||
@@ -170,6 +179,7 @@ namespace OpenNest.Forms
|
|||||||
if (item == null) return;
|
if (item == null) return;
|
||||||
|
|
||||||
filterPanel.ApplyFilters(item.Entities);
|
filterPanel.ApplyFilters(item.Entities);
|
||||||
|
ReHidePromotedEntities(item.Bends);
|
||||||
entityView1.Invalidate();
|
entityView1.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +194,10 @@ namespace OpenNest.Forms
|
|||||||
var item = CurrentItem;
|
var item = CurrentItem;
|
||||||
if (item == null || index < 0 || index >= item.Bends.Count) return;
|
if (item == null || index < 0 || index >= item.Bends.Count) return;
|
||||||
|
|
||||||
|
var bend = item.Bends[index];
|
||||||
|
if (bend.SourceEntity != null)
|
||||||
|
bend.SourceEntity.IsVisible = true;
|
||||||
|
|
||||||
item.Bends.RemoveAt(index);
|
item.Bends.RemoveAt(index);
|
||||||
entityView1.Bends = item.Bends;
|
entityView1.Bends = item.Bends;
|
||||||
entityView1.SelectedBendIndex = -1;
|
entityView1.SelectedBendIndex = -1;
|
||||||
@@ -231,14 +245,25 @@ namespace OpenNest.Forms
|
|||||||
shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities));
|
shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities));
|
||||||
|
|
||||||
var pgm = ConvertGeometry.ToProgram(drawEntities);
|
var pgm = ConvertGeometry.ToProgram(drawEntities);
|
||||||
|
var originOffset = Vector.Zero;
|
||||||
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
||||||
{
|
{
|
||||||
var rapid = (RapidMove)pgm[0];
|
var rapid = (RapidMove)pgm[0];
|
||||||
pgm.Offset(-rapid.EndPoint);
|
originOffset = rapid.EndPoint;
|
||||||
|
pgm.Offset(-originOffset);
|
||||||
pgm.Codes.RemoveAt(0);
|
pgm.Codes.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
var drawing = new Drawing(item.Name, pgm);
|
var drawing = new Drawing(item.Name, pgm);
|
||||||
|
drawing.Bends = item.Bends.Select(b => new Bend
|
||||||
|
{
|
||||||
|
StartPoint = new Vector(b.StartPoint.X - originOffset.X, b.StartPoint.Y - originOffset.Y),
|
||||||
|
EndPoint = new Vector(b.EndPoint.X - originOffset.X, b.EndPoint.Y - originOffset.Y),
|
||||||
|
Direction = b.Direction,
|
||||||
|
Angle = b.Angle,
|
||||||
|
Radius = b.Radius,
|
||||||
|
NoteText = b.NoteText,
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
using var form = new SplitDrawingForm(drawing);
|
using var form = new SplitDrawingForm(drawing);
|
||||||
if (form.ShowDialog(this) != DialogResult.OK || form.ResultDrawings?.Count <= 1)
|
if (form.ShowDialog(this) != DialogResult.OK || form.ResultDrawings?.Count <= 1)
|
||||||
@@ -255,31 +280,86 @@ namespace OpenNest.Forms
|
|||||||
var newItems = new List<string>();
|
var newItems = new List<string>();
|
||||||
|
|
||||||
var splitWriter = new SplitDxfWriter();
|
var splitWriter = new SplitDxfWriter();
|
||||||
|
var splitItems = new List<FileListItem>();
|
||||||
|
|
||||||
for (var i = 0; i < form.ResultDrawings.Count; i++)
|
for (var i = 0; i < form.ResultDrawings.Count; i++)
|
||||||
{
|
{
|
||||||
var splitDrawing = form.ResultDrawings[i];
|
var splitDrawing = form.ResultDrawings[i];
|
||||||
|
|
||||||
// Assign bends from the source item — spatial filtering is a future enhancement
|
var splitName = $"{baseName}-{i + 1}.dxf";
|
||||||
splitDrawing.Bends.AddRange(item.Bends);
|
|
||||||
|
|
||||||
var splitName = $"{baseName}_split{i + 1}.dxf";
|
|
||||||
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
|
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
|
||||||
|
|
||||||
splitWriter.Write(splitPath, splitDrawing);
|
splitWriter.Write(splitPath, splitDrawing);
|
||||||
newItems.Add(splitPath);
|
newItems.Add(splitPath);
|
||||||
|
|
||||||
|
// Re-import geometry but keep bends from the split drawing
|
||||||
|
var importer = new DxfImporter();
|
||||||
|
importer.SplinePrecision = Settings.Default.ImportSplinePrecision;
|
||||||
|
var result = importer.Import(splitPath);
|
||||||
|
|
||||||
|
var splitItem = new FileListItem
|
||||||
|
{
|
||||||
|
Name = Path.GetFileNameWithoutExtension(splitPath),
|
||||||
|
Entities = result.Entities,
|
||||||
|
Path = splitPath,
|
||||||
|
Quantity = item.Quantity,
|
||||||
|
Customer = item.Customer,
|
||||||
|
Bends = splitDrawing.Bends ?? new List<Bend>(),
|
||||||
|
Bounds = result.Entities.GetBoundingBox(),
|
||||||
|
EntityCount = result.Entities.Count
|
||||||
|
};
|
||||||
|
splitItems.Add(splitItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove original and add split files
|
// Remove original and add split items directly (preserving bend info)
|
||||||
fileList.RemoveAt(index);
|
fileList.RemoveAt(index);
|
||||||
foreach (var path in newItems)
|
foreach (var splitItem in splitItems)
|
||||||
AddFile(path);
|
fileList.AddItem(splitItem);
|
||||||
|
|
||||||
if (writableDir != sourceDir)
|
if (writableDir != sourceDir)
|
||||||
MessageBox.Show($"Split files written to: {writableDir}", "Split Output",
|
MessageBox.Show($"Split files written to: {writableDir}", "Split Output",
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnAddBendLineClicked(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var active = !entityView1.IsPickingBendLine;
|
||||||
|
entityView1.IsPickingBendLine = active;
|
||||||
|
filterPanel.SetPickMode(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLinePicked(object sender, Line line)
|
||||||
|
{
|
||||||
|
using var dialog = new BendLineDialog();
|
||||||
|
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var item = CurrentItem;
|
||||||
|
if (item == null) return;
|
||||||
|
|
||||||
|
var bend = new Bend
|
||||||
|
{
|
||||||
|
StartPoint = line.StartPoint,
|
||||||
|
EndPoint = line.EndPoint,
|
||||||
|
Direction = dialog.Direction,
|
||||||
|
Angle = dialog.BendAngle,
|
||||||
|
Radius = dialog.BendRadius,
|
||||||
|
SourceEntity = line
|
||||||
|
};
|
||||||
|
|
||||||
|
line.IsVisible = false;
|
||||||
|
item.Bends.Add(bend);
|
||||||
|
entityView1.Bends = item.Bends;
|
||||||
|
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||||
|
entityView1.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPickCancelled(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
entityView1.IsPickingBendLine = false;
|
||||||
|
filterPanel.SetPickMode(false);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnDragEnter(object sender, DragEventArgs e)
|
private void OnDragEnter(object sender, DragEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||||
@@ -358,6 +438,16 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
#region Helpers
|
#region Helpers
|
||||||
|
|
||||||
|
private static void ReHidePromotedEntities(List<Bend> bends)
|
||||||
|
{
|
||||||
|
if (bends == null) return;
|
||||||
|
foreach (var bend in bends)
|
||||||
|
{
|
||||||
|
if (bend.SourceEntity != null)
|
||||||
|
bend.SourceEntity.IsVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SetRotation(Shape shape, RotationType rotation)
|
private static void SetRotation(Shape shape, RotationType rotation)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -473,6 +473,12 @@ namespace OpenNest.Forms
|
|||||||
PlateView.Invalidate();
|
PlateView.Invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TogglePiercePoints()
|
||||||
|
{
|
||||||
|
PlateView.DrawPiercePoints = !PlateView.DrawPiercePoints;
|
||||||
|
PlateView.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
public void ToggleDrawBounds()
|
public void ToggleDrawBounds()
|
||||||
{
|
{
|
||||||
PlateView.DrawBounds = !PlateView.DrawBounds;
|
PlateView.DrawBounds = !PlateView.DrawBounds;
|
||||||
|
|||||||
Generated
+12
-2
@@ -48,6 +48,7 @@
|
|||||||
mnuEditSelectAll = new System.Windows.Forms.ToolStripMenuItem();
|
mnuEditSelectAll = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuView = new System.Windows.Forms.ToolStripMenuItem();
|
mnuView = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuViewDrawRapids = new System.Windows.Forms.ToolStripMenuItem();
|
mnuViewDrawRapids = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
mnuViewDrawPiercePoints = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuViewDrawBounds = new System.Windows.Forms.ToolStripMenuItem();
|
mnuViewDrawBounds = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
mnuViewDrawOffset = new System.Windows.Forms.ToolStripMenuItem();
|
mnuViewDrawOffset = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
toolStripMenuItem5 = new System.Windows.Forms.ToolStripSeparator();
|
toolStripMenuItem5 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
@@ -297,7 +298,7 @@
|
|||||||
//
|
//
|
||||||
// mnuView
|
// mnuView
|
||||||
//
|
//
|
||||||
mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawBounds, mnuViewDrawOffset, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut });
|
mnuView.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { mnuViewDrawRapids, mnuViewDrawPiercePoints, mnuViewDrawBounds, mnuViewDrawOffset, toolStripMenuItem5, mnuViewZoomTo, mnuViewZoomIn, mnuViewZoomOut });
|
||||||
mnuView.Name = "mnuView";
|
mnuView.Name = "mnuView";
|
||||||
mnuView.Size = new System.Drawing.Size(44, 20);
|
mnuView.Size = new System.Drawing.Size(44, 20);
|
||||||
mnuView.Text = "&View";
|
mnuView.Text = "&View";
|
||||||
@@ -308,7 +309,15 @@
|
|||||||
mnuViewDrawRapids.Size = new System.Drawing.Size(222, 22);
|
mnuViewDrawRapids.Size = new System.Drawing.Size(222, 22);
|
||||||
mnuViewDrawRapids.Text = "Draw Rapids";
|
mnuViewDrawRapids.Text = "Draw Rapids";
|
||||||
mnuViewDrawRapids.Click += ToggleDrawRapids_Click;
|
mnuViewDrawRapids.Click += ToggleDrawRapids_Click;
|
||||||
//
|
//
|
||||||
|
// mnuViewDrawPiercePoints
|
||||||
|
//
|
||||||
|
mnuViewDrawPiercePoints.CheckOnClick = true;
|
||||||
|
mnuViewDrawPiercePoints.Name = "mnuViewDrawPiercePoints";
|
||||||
|
mnuViewDrawPiercePoints.Size = new System.Drawing.Size(222, 22);
|
||||||
|
mnuViewDrawPiercePoints.Text = "Draw Pierce Points";
|
||||||
|
mnuViewDrawPiercePoints.Click += ToggleDrawPiercePoints_Click;
|
||||||
|
//
|
||||||
// mnuViewDrawBounds
|
// mnuViewDrawBounds
|
||||||
//
|
//
|
||||||
mnuViewDrawBounds.CheckOnClick = true;
|
mnuViewDrawBounds.CheckOnClick = true;
|
||||||
@@ -1131,6 +1140,7 @@
|
|||||||
private System.Windows.Forms.ToolStripMenuItem mnuEditSelectAll;
|
private System.Windows.Forms.ToolStripMenuItem mnuEditSelectAll;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuView;
|
private System.Windows.Forms.ToolStripMenuItem mnuView;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawRapids;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawRapids;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawPiercePoints;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawBounds;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawBounds;
|
||||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset;
|
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
|
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
|
||||||
|
|||||||
@@ -78,10 +78,24 @@ namespace OpenNest.Forms
|
|||||||
|
|
||||||
private string GetNestName(DateTime date, int id)
|
private string GetNestName(DateTime date, int id)
|
||||||
{
|
{
|
||||||
var month = date.Month.ToString().PadLeft(2, '0');
|
var year = (date.Year % 100).ToString("D2");
|
||||||
var day = date.Day.ToString().PadLeft(2, '0');
|
var seq = ToBase36(id).PadLeft(3, '0');
|
||||||
|
|
||||||
return string.Format("N{0}{1}-{2}", month, day, id.ToString().PadLeft(3, '0'));
|
return $"N{year}-{seq}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToBase36(int value)
|
||||||
|
{
|
||||||
|
const string chars = "2345679ACDEFGHJKLMNPQRSTUVWXYZ";
|
||||||
|
if (value == 0) return "0";
|
||||||
|
|
||||||
|
var result = "";
|
||||||
|
while (value > 0)
|
||||||
|
{
|
||||||
|
result = chars[value % 36] + result;
|
||||||
|
value /= 36;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized)
|
private void LoadNest(Nest nest, FormWindowState windowState = FormWindowState.Maximized)
|
||||||
@@ -377,6 +391,7 @@ namespace OpenNest.Forms
|
|||||||
activeForm.PlateView.PartAdded += PlateView_PartAdded;
|
activeForm.PlateView.PartAdded += PlateView_PartAdded;
|
||||||
activeForm.PlateView.PartRemoved += PlateView_PartRemoved;
|
activeForm.PlateView.PartRemoved += PlateView_PartRemoved;
|
||||||
mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid;
|
mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid;
|
||||||
|
mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints;
|
||||||
mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds;
|
mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds;
|
||||||
statusLabel1.Text = activeForm.PlateView.Status;
|
statusLabel1.Text = activeForm.PlateView.Status;
|
||||||
}
|
}
|
||||||
@@ -539,6 +554,13 @@ namespace OpenNest.Forms
|
|||||||
mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid;
|
mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ToggleDrawPiercePoints_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (activeForm == null) return;
|
||||||
|
activeForm.TogglePiercePoints();
|
||||||
|
mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints;
|
||||||
|
}
|
||||||
|
|
||||||
private void ToggleDrawBounds_Click(object sender, EventArgs e)
|
private void ToggleDrawBounds_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (activeForm == null) return;
|
if (activeForm == null) return;
|
||||||
@@ -826,7 +848,6 @@ namespace OpenNest.Forms
|
|||||||
}
|
}
|
||||||
|
|
||||||
var finder = RemnantFinder.FromPlate(plate);
|
var finder = RemnantFinder.FromPlate(plate);
|
||||||
var tiered = finder.FindTieredRemnants(minDim);
|
|
||||||
|
|
||||||
if (remnantViewer == null || remnantViewer.IsDisposed)
|
if (remnantViewer == null || remnantViewer.IsDisposed)
|
||||||
{
|
{
|
||||||
@@ -840,7 +861,7 @@ namespace OpenNest.Forms
|
|||||||
Top);
|
Top);
|
||||||
}
|
}
|
||||||
|
|
||||||
remnantViewer.LoadRemnants(tiered, activeForm.PlateView);
|
remnantViewer.LoadRemnants(finder, minDim, activeForm.PlateView);
|
||||||
remnantViewer.Show();
|
remnantViewer.Show();
|
||||||
remnantViewer.BringToFront();
|
remnantViewer.BringToFront();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ namespace OpenNest.Forms
|
|||||||
public class RemnantViewerForm : Form
|
public class RemnantViewerForm : Form
|
||||||
{
|
{
|
||||||
private ListView listView;
|
private ListView listView;
|
||||||
|
private CheckBox filterCheckBox;
|
||||||
private PlateView plateView;
|
private PlateView plateView;
|
||||||
|
private RemnantFinder finder;
|
||||||
|
private double minDim;
|
||||||
private List<TieredRemnant> remnants = new();
|
private List<TieredRemnant> remnants = new();
|
||||||
private int selectedIndex = -1;
|
private int selectedIndex = -1;
|
||||||
|
|
||||||
@@ -24,6 +27,15 @@ namespace OpenNest.Forms
|
|||||||
ShowInTaskbar = false;
|
ShowInTaskbar = false;
|
||||||
TopMost = true;
|
TopMost = true;
|
||||||
|
|
||||||
|
filterCheckBox = new CheckBox
|
||||||
|
{
|
||||||
|
Text = "Filter by part size",
|
||||||
|
Checked = true,
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Padding = new Padding(4, 2, 0, 2),
|
||||||
|
};
|
||||||
|
filterCheckBox.CheckedChanged += FilterCheckBox_CheckedChanged;
|
||||||
|
|
||||||
listView = new ListView
|
listView = new ListView
|
||||||
{
|
{
|
||||||
Dock = DockStyle.Fill,
|
Dock = DockStyle.Fill,
|
||||||
@@ -42,6 +54,7 @@ namespace OpenNest.Forms
|
|||||||
listView.SelectedIndexChanged += ListView_SelectedIndexChanged;
|
listView.SelectedIndexChanged += ListView_SelectedIndexChanged;
|
||||||
|
|
||||||
Controls.Add(listView);
|
Controls.Add(listView);
|
||||||
|
Controls.Add(filterCheckBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool ProcessDialogKey(Keys keyData)
|
protected override bool ProcessDialogKey(Keys keyData)
|
||||||
@@ -54,10 +67,25 @@ namespace OpenNest.Forms
|
|||||||
return base.ProcessDialogKey(keyData);
|
return base.ProcessDialogKey(keyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadRemnants(List<TieredRemnant> tieredRemnants, PlateView view)
|
public void LoadRemnants(RemnantFinder finder, double minDim, PlateView view)
|
||||||
{
|
{
|
||||||
plateView = view;
|
plateView = view;
|
||||||
remnants = tieredRemnants;
|
this.finder = finder;
|
||||||
|
this.minDim = minDim;
|
||||||
|
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FilterCheckBox_CheckedChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (finder != null)
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private new void Refresh()
|
||||||
|
{
|
||||||
|
var dim = filterCheckBox.Checked ? minDim : 0;
|
||||||
|
remnants = finder.FindTieredRemnants(dim);
|
||||||
selectedIndex = -1;
|
selectedIndex = -1;
|
||||||
|
|
||||||
listView.BeginUpdate();
|
listView.BeginUpdate();
|
||||||
|
|||||||
+39
-36
@@ -29,9 +29,6 @@ namespace OpenNest.Forms
|
|||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
pnlSettings = new System.Windows.Forms.Panel();
|
pnlSettings = new System.Windows.Forms.Panel();
|
||||||
pnlButtons = new System.Windows.Forms.Panel();
|
|
||||||
btnCancel = new System.Windows.Forms.Button();
|
|
||||||
btnOK = new System.Windows.Forms.Button();
|
|
||||||
grpSpikeParams = new System.Windows.Forms.GroupBox();
|
grpSpikeParams = new System.Windows.Forms.GroupBox();
|
||||||
nudSpikePairCount = new System.Windows.Forms.NumericUpDown();
|
nudSpikePairCount = new System.Windows.Forms.NumericUpDown();
|
||||||
lblSpikePairCount = new System.Windows.Forms.Label();
|
lblSpikePairCount = new System.Windows.Forms.Label();
|
||||||
@@ -70,6 +67,9 @@ namespace OpenNest.Forms
|
|||||||
radByCount = new System.Windows.Forms.RadioButton();
|
radByCount = new System.Windows.Forms.RadioButton();
|
||||||
radFitToPlate = new System.Windows.Forms.RadioButton();
|
radFitToPlate = new System.Windows.Forms.RadioButton();
|
||||||
radManual = new System.Windows.Forms.RadioButton();
|
radManual = new System.Windows.Forms.RadioButton();
|
||||||
|
pnlButtons = new System.Windows.Forms.Panel();
|
||||||
|
btnOK = new System.Windows.Forms.Button();
|
||||||
|
btnCancel = new System.Windows.Forms.Button();
|
||||||
pnlPreview = new SplitPreview();
|
pnlPreview = new SplitPreview();
|
||||||
toolStrip = new System.Windows.Forms.ToolStrip();
|
toolStrip = new System.Windows.Forms.ToolStrip();
|
||||||
btnAddLine = new System.Windows.Forms.ToolStripButton();
|
btnAddLine = new System.Windows.Forms.ToolStripButton();
|
||||||
@@ -96,6 +96,7 @@ namespace OpenNest.Forms
|
|||||||
((System.ComponentModel.ISupportInitialize)nudPlateHeight).BeginInit();
|
((System.ComponentModel.ISupportInitialize)nudPlateHeight).BeginInit();
|
||||||
((System.ComponentModel.ISupportInitialize)nudPlateWidth).BeginInit();
|
((System.ComponentModel.ISupportInitialize)nudPlateWidth).BeginInit();
|
||||||
grpMethod.SuspendLayout();
|
grpMethod.SuspendLayout();
|
||||||
|
pnlButtons.SuspendLayout();
|
||||||
toolStrip.SuspendLayout();
|
toolStrip.SuspendLayout();
|
||||||
statusStrip.SuspendLayout();
|
statusStrip.SuspendLayout();
|
||||||
SuspendLayout();
|
SuspendLayout();
|
||||||
@@ -116,38 +117,6 @@ namespace OpenNest.Forms
|
|||||||
pnlSettings.Padding = new System.Windows.Forms.Padding(6);
|
pnlSettings.Padding = new System.Windows.Forms.Padding(6);
|
||||||
pnlSettings.Size = new System.Drawing.Size(220, 611);
|
pnlSettings.Size = new System.Drawing.Size(220, 611);
|
||||||
pnlSettings.TabIndex = 2;
|
pnlSettings.TabIndex = 2;
|
||||||
//
|
|
||||||
// pnlButtons
|
|
||||||
//
|
|
||||||
pnlButtons.Controls.Add(btnOK);
|
|
||||||
pnlButtons.Controls.Add(btnCancel);
|
|
||||||
pnlButtons.Dock = System.Windows.Forms.DockStyle.Bottom;
|
|
||||||
pnlButtons.Name = "pnlButtons";
|
|
||||||
pnlButtons.Size = new System.Drawing.Size(208, 40);
|
|
||||||
pnlButtons.TabIndex = 8;
|
|
||||||
//
|
|
||||||
// btnCancel
|
|
||||||
//
|
|
||||||
btnCancel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
|
||||||
btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
|
||||||
btnCancel.Location = new System.Drawing.Point(110, 6);
|
|
||||||
btnCancel.Name = "btnCancel";
|
|
||||||
btnCancel.Size = new System.Drawing.Size(80, 28);
|
|
||||||
btnCancel.TabIndex = 7;
|
|
||||||
btnCancel.Text = "Cancel";
|
|
||||||
btnCancel.UseVisualStyleBackColor = true;
|
|
||||||
btnCancel.Click += OnCancel;
|
|
||||||
//
|
|
||||||
// btnOK
|
|
||||||
//
|
|
||||||
btnOK.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
|
||||||
btnOK.Location = new System.Drawing.Point(20, 6);
|
|
||||||
btnOK.Name = "btnOK";
|
|
||||||
btnOK.Size = new System.Drawing.Size(80, 28);
|
|
||||||
btnOK.TabIndex = 6;
|
|
||||||
btnOK.Text = "OK";
|
|
||||||
btnOK.UseVisualStyleBackColor = true;
|
|
||||||
btnOK.Click += OnOK;
|
|
||||||
//
|
//
|
||||||
// grpSpikeParams
|
// grpSpikeParams
|
||||||
//
|
//
|
||||||
@@ -216,7 +185,7 @@ namespace OpenNest.Forms
|
|||||||
nudGrooveDepth.Name = "nudGrooveDepth";
|
nudGrooveDepth.Name = "nudGrooveDepth";
|
||||||
nudGrooveDepth.Size = new System.Drawing.Size(88, 23);
|
nudGrooveDepth.Size = new System.Drawing.Size(88, 23);
|
||||||
nudGrooveDepth.TabIndex = 1;
|
nudGrooveDepth.TabIndex = 1;
|
||||||
nudGrooveDepth.Value = new decimal(new int[] { 625, 0, 0, 196608 });
|
nudGrooveDepth.Value = new decimal(new int[] { 125, 0, 0, 196608 });
|
||||||
nudGrooveDepth.ValueChanged += OnSpikeParamChanged;
|
nudGrooveDepth.ValueChanged += OnSpikeParamChanged;
|
||||||
//
|
//
|
||||||
// lblGrooveDepth
|
// lblGrooveDepth
|
||||||
@@ -568,6 +537,39 @@ namespace OpenNest.Forms
|
|||||||
radManual.Text = "Manual";
|
radManual.Text = "Manual";
|
||||||
radManual.CheckedChanged += OnMethodChanged;
|
radManual.CheckedChanged += OnMethodChanged;
|
||||||
//
|
//
|
||||||
|
// pnlButtons
|
||||||
|
//
|
||||||
|
pnlButtons.Controls.Add(btnOK);
|
||||||
|
pnlButtons.Controls.Add(btnCancel);
|
||||||
|
pnlButtons.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||||
|
pnlButtons.Location = new System.Drawing.Point(6, 637);
|
||||||
|
pnlButtons.Name = "pnlButtons";
|
||||||
|
pnlButtons.Size = new System.Drawing.Size(191, 40);
|
||||||
|
pnlButtons.TabIndex = 8;
|
||||||
|
//
|
||||||
|
// btnOK
|
||||||
|
//
|
||||||
|
btnOK.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||||
|
btnOK.Location = new System.Drawing.Point(11, 6);
|
||||||
|
btnOK.Name = "btnOK";
|
||||||
|
btnOK.Size = new System.Drawing.Size(80, 28);
|
||||||
|
btnOK.TabIndex = 6;
|
||||||
|
btnOK.Text = "OK";
|
||||||
|
btnOK.UseVisualStyleBackColor = true;
|
||||||
|
btnOK.Click += OnOK;
|
||||||
|
//
|
||||||
|
// btnCancel
|
||||||
|
//
|
||||||
|
btnCancel.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
|
||||||
|
btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||||
|
btnCancel.Location = new System.Drawing.Point(101, 6);
|
||||||
|
btnCancel.Name = "btnCancel";
|
||||||
|
btnCancel.Size = new System.Drawing.Size(80, 28);
|
||||||
|
btnCancel.TabIndex = 7;
|
||||||
|
btnCancel.Text = "Cancel";
|
||||||
|
btnCancel.UseVisualStyleBackColor = true;
|
||||||
|
btnCancel.Click += OnCancel;
|
||||||
|
//
|
||||||
// pnlPreview
|
// pnlPreview
|
||||||
//
|
//
|
||||||
pnlPreview.BackColor = System.Drawing.Color.FromArgb(33, 40, 48);
|
pnlPreview.BackColor = System.Drawing.Color.FromArgb(33, 40, 48);
|
||||||
@@ -667,6 +669,7 @@ namespace OpenNest.Forms
|
|||||||
((System.ComponentModel.ISupportInitialize)nudPlateWidth).EndInit();
|
((System.ComponentModel.ISupportInitialize)nudPlateWidth).EndInit();
|
||||||
grpMethod.ResumeLayout(false);
|
grpMethod.ResumeLayout(false);
|
||||||
grpMethod.PerformLayout();
|
grpMethod.PerformLayout();
|
||||||
|
pnlButtons.ResumeLayout(false);
|
||||||
toolStrip.ResumeLayout(false);
|
toolStrip.ResumeLayout(false);
|
||||||
toolStrip.PerformLayout();
|
toolStrip.PerformLayout();
|
||||||
statusStrip.ResumeLayout(false);
|
statusStrip.ResumeLayout(false);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public partial class SplitDrawingForm : Form
|
|||||||
private readonly List<SplitLine> _splitLines = new();
|
private readonly List<SplitLine> _splitLines = new();
|
||||||
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
|
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
|
||||||
private bool _placingLine;
|
private bool _placingLine;
|
||||||
|
private Vector _placingCursor;
|
||||||
|
|
||||||
// Feature handle drag state
|
// Feature handle drag state
|
||||||
private int _dragLineIndex = -1;
|
private int _dragLineIndex = -1;
|
||||||
@@ -74,30 +75,22 @@ public partial class SplitDrawingForm : Form
|
|||||||
|
|
||||||
if (axisIndex == 1)
|
if (axisIndex == 1)
|
||||||
{
|
{
|
||||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
var usable = plateW - 2 * spacing - overhang;
|
||||||
if (usable > 0)
|
if (usable > 0)
|
||||||
{
|
{
|
||||||
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
|
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
|
||||||
if (splits > 0)
|
for (var i = 1; i <= splits; i++)
|
||||||
{
|
_splitLines.Add(new SplitLine(_drawingBounds.X + usable * i, CutOffAxis.Vertical));
|
||||||
var step = _drawingBounds.Width / (splits + 1);
|
|
||||||
for (var i = 1; i <= splits; i++)
|
|
||||||
_splitLines.Add(new SplitLine(_drawingBounds.X + step * i, CutOffAxis.Vertical));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (axisIndex == 2)
|
else if (axisIndex == 2)
|
||||||
{
|
{
|
||||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
var usable = plateH - 2 * spacing - overhang;
|
||||||
if (usable > 0)
|
if (usable > 0)
|
||||||
{
|
{
|
||||||
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
|
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
|
||||||
if (splits > 0)
|
for (var i = 1; i <= splits; i++)
|
||||||
{
|
_splitLines.Add(new SplitLine(_drawingBounds.Y + usable * i, CutOffAxis.Horizontal));
|
||||||
var step = _drawingBounds.Length / (splits + 1);
|
|
||||||
for (var i = 1; i <= splits; i++)
|
|
||||||
_splitLines.Add(new SplitLine(_drawingBounds.Y + step * i, CutOffAxis.Horizontal));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -293,6 +286,12 @@ public partial class SplitDrawingForm : Form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_placingLine)
|
||||||
|
{
|
||||||
|
_placingCursor = SnapToMidpoint(worldPt);
|
||||||
|
pnlPreview.Invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
lblCursor.Text = $"Cursor: {worldPt.X:F2}, {worldPt.Y:F2}";
|
lblCursor.Text = $"Cursor: {worldPt.X:F2}, {worldPt.Y:F2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +338,7 @@ public partial class SplitDrawingForm : Form
|
|||||||
if (keyData == Keys.Space)
|
if (keyData == Keys.Space)
|
||||||
{
|
{
|
||||||
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
|
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
|
||||||
|
pnlPreview.Invalidate();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (keyData == Keys.Escape)
|
if (keyData == Keys.Escape)
|
||||||
@@ -381,6 +381,30 @@ public partial class SplitDrawingForm : Form
|
|||||||
System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y));
|
System.Math.Abs(br.X - tl.X), System.Math.Abs(br.Y - tl.Y));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Piece number and dimension labels at center of each region
|
||||||
|
if (regions.Count > 1)
|
||||||
|
{
|
||||||
|
using var labelFont = new Font("Segoe UI", 14f, FontStyle.Bold, GraphicsUnit.Pixel);
|
||||||
|
using var dimFont = new Font("Segoe UI", 11f, FontStyle.Regular, GraphicsUnit.Pixel);
|
||||||
|
using var labelBrush = new SolidBrush(Color.FromArgb(200, 255, 255, 255));
|
||||||
|
using var shadowBrush = new SolidBrush(Color.FromArgb(160, 0, 0, 0));
|
||||||
|
var sf = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
|
||||||
|
|
||||||
|
for (var i = 0; i < regions.Count; i++)
|
||||||
|
{
|
||||||
|
var r = regions[i];
|
||||||
|
var center = pnlPreview.PointWorldToGraph(r.Center.X, r.Center.Y);
|
||||||
|
var label = (i + 1).ToString();
|
||||||
|
var dim = $"{r.Width:F2} x {r.Length:F2}";
|
||||||
|
|
||||||
|
// Shadow offset for readability
|
||||||
|
g.DrawString(label, labelFont, shadowBrush, center.X + 1, center.Y - 7, sf);
|
||||||
|
g.DrawString(label, labelFont, labelBrush, center.X, center.Y - 8, sf);
|
||||||
|
g.DrawString(dim, dimFont, shadowBrush, center.X + 1, center.Y + 9, sf);
|
||||||
|
g.DrawString(dim, dimFont, labelBrush, center.X, center.Y + 8, sf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Split lines — trimmed at feature positions with feature contours
|
// Split lines — trimmed at feature positions with feature contours
|
||||||
var parameters = GetCurrentParameters();
|
var parameters = GetCurrentParameters();
|
||||||
var feature = GetSplitFeature(parameters.Type);
|
var feature = GetSplitFeature(parameters.Type);
|
||||||
@@ -449,6 +473,33 @@ public partial class SplitDrawingForm : Form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Placement preview line
|
||||||
|
if (_placingLine && _placingCursor != null)
|
||||||
|
{
|
||||||
|
var isVert = _currentAxis == CutOffAxis.Vertical;
|
||||||
|
var snapped = _placingCursor;
|
||||||
|
var pos = isVert ? snapped.X : snapped.Y;
|
||||||
|
var margin = 10.0;
|
||||||
|
|
||||||
|
PointF pp1, pp2;
|
||||||
|
if (isVert)
|
||||||
|
{
|
||||||
|
pp1 = pnlPreview.PointWorldToGraph(pos, _drawingBounds.Bottom - margin);
|
||||||
|
pp2 = pnlPreview.PointWorldToGraph(pos, _drawingBounds.Top + margin);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pp1 = pnlPreview.PointWorldToGraph(_drawingBounds.Left - margin, pos);
|
||||||
|
pp2 = pnlPreview.PointWorldToGraph(_drawingBounds.Right + margin, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var previewPen = new Pen(Color.FromArgb(180, 255, 213, 79), 1.5f);
|
||||||
|
previewPen.DashStyle = DashStyle.DashDot;
|
||||||
|
g.DrawLine(previewPen, pp1, pp2);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Feature position handles
|
// Feature position handles
|
||||||
if (!radStraight.Checked)
|
if (!radStraight.Checked)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user