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 string NoteText { get; set; }
|
||||
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public Entity SourceEntity { get; set; }
|
||||
|
||||
public double Length => StartPoint.DistanceTo(EndPoint);
|
||||
|
||||
public double AngleRadians => Angle.HasValue
|
||||
|
||||
@@ -133,17 +133,30 @@ namespace OpenNest.Geometry
|
||||
if (!arc1.Radius.IsEqualTo(arc2.Radius))
|
||||
return false;
|
||||
|
||||
if (arc1.StartAngle > arc1.EndAngle)
|
||||
arc1.StartAngle -= Angle.TwoPI;
|
||||
var start1 = arc1.StartAngle;
|
||||
var end1 = arc1.EndAngle;
|
||||
var start2 = arc2.StartAngle;
|
||||
var end2 = arc2.EndAngle;
|
||||
|
||||
if (arc2.StartAngle > arc2.EndAngle)
|
||||
arc2.StartAngle -= Angle.TwoPI;
|
||||
if (start1 > end1)
|
||||
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;
|
||||
|
||||
var startAngle = arc1.StartAngle < arc2.StartAngle ? arc1.StartAngle : arc2.StartAngle;
|
||||
var endAngle = arc1.EndAngle > arc2.EndAngle ? arc1.EndAngle : arc2.EndAngle;
|
||||
var startAngle = start1 < start2 ? start1 : start2;
|
||||
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 (endAngle < 0) endAngle += Angle.TwoPI;
|
||||
|
||||
@@ -5,17 +5,17 @@ namespace OpenNest.Shapes
|
||||
{
|
||||
public class RectangleShape : ShapeDefinition
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
{
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, Width, 0),
|
||||
new Line(Width, 0, Width, Height),
|
||||
new Line(Width, Height, 0, Height),
|
||||
new Line(0, Height, 0, 0)
|
||||
new Line(0, 0, Length, 0),
|
||||
new Line(Length, 0, Length, Width),
|
||||
new Line(Length, Width, 0, Width),
|
||||
new Line(0, Width, 0, 0)
|
||||
};
|
||||
|
||||
return CreateDrawing(entities);
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace OpenNest.Shapes
|
||||
{
|
||||
public class RoundedRectangleShape : ShapeDefinition
|
||||
{
|
||||
public double Length { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double Radius { get; set; }
|
||||
|
||||
public override Drawing GetDrawing()
|
||||
@@ -17,36 +17,36 @@ namespace OpenNest.Shapes
|
||||
|
||||
if (r <= 0)
|
||||
{
|
||||
entities.Add(new Line(0, 0, Width, 0));
|
||||
entities.Add(new Line(Width, 0, Width, Height));
|
||||
entities.Add(new Line(Width, Height, 0, Height));
|
||||
entities.Add(new Line(0, Height, 0, 0));
|
||||
entities.Add(new Line(0, 0, Length, 0));
|
||||
entities.Add(new Line(Length, 0, Length, Width));
|
||||
entities.Add(new Line(Length, Width, 0, Width));
|
||||
entities.Add(new Line(0, Width, 0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 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
|
||||
entities.Add(new Arc(Width - r, r, r,
|
||||
// Bottom-right corner arc: center at (Length-r, r), from 270deg to 360deg
|
||||
entities.Add(new Arc(Length - r, r, r,
|
||||
Angle.ToRadians(270), Angle.ToRadians(360)));
|
||||
|
||||
// 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
|
||||
entities.Add(new Arc(Width - r, Height - r, r,
|
||||
// Top-right corner arc: center at (Length-r, Width-r), from 0deg to 90deg
|
||||
entities.Add(new Arc(Length - r, Width - r, r,
|
||||
Angle.ToRadians(0), Angle.ToRadians(90)));
|
||||
|
||||
// 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
|
||||
entities.Add(new Arc(r, Height - r, r,
|
||||
// Top-left corner arc: center at (r, Width-r), from 90deg to 180deg
|
||||
entities.Add(new Arc(r, Width - r, r,
|
||||
Angle.ToRadians(90), Angle.ToRadians(180)));
|
||||
|
||||
// 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
|
||||
entities.Add(new Arc(r, r, r,
|
||||
|
||||
@@ -19,19 +19,11 @@ public static class AutoSplitCalculator
|
||||
if (verticalSplits < 0) verticalSplits = 0;
|
||||
if (horizontalSplits < 0) horizontalSplits = 0;
|
||||
|
||||
if (verticalSplits > 0)
|
||||
{
|
||||
var spacing = partBounds.Width / (verticalSplits + 1);
|
||||
for (var i = 1; i <= verticalSplits; i++)
|
||||
lines.Add(new SplitLine(partBounds.X + spacing * i, CutOffAxis.Vertical));
|
||||
}
|
||||
for (var i = 1; i <= verticalSplits; i++)
|
||||
lines.Add(new SplitLine(partBounds.X + usableWidth * i, CutOffAxis.Vertical));
|
||||
|
||||
if (horizontalSplits > 0)
|
||||
{
|
||||
var spacing = partBounds.Length / (horizontalSplits + 1);
|
||||
for (var i = 1; i <= horizontalSplits; i++)
|
||||
lines.Add(new SplitLine(partBounds.Y + spacing * i, CutOffAxis.Horizontal));
|
||||
}
|
||||
for (var i = 1; i <= horizontalSplits; i++)
|
||||
lines.Add(new SplitLine(partBounds.Y + usableHeight * i, CutOffAxis.Horizontal));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public static class DrawingSplitter
|
||||
allEntities.AddRange(pieceEntities);
|
||||
allEntities.AddRange(cutoutEntities);
|
||||
|
||||
var piece = BuildPieceDrawing(drawing, allEntities, pieceIndex);
|
||||
var piece = BuildPieceDrawing(drawing, allEntities, pieceIndex, region);
|
||||
results.Add(piece);
|
||||
pieceIndex++;
|
||||
}
|
||||
@@ -80,7 +80,7 @@ public static class DrawingSplitter
|
||||
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 offsetX = -pieceBounds.X;
|
||||
@@ -98,9 +98,69 @@ public static class DrawingSplitter
|
||||
piece.Customer = source.Customer;
|
||||
piece.Source = source.Source;
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
DecomposeCirclesInShape(profile.Perimeter);
|
||||
|
||||
@@ -24,6 +24,10 @@ namespace OpenNest.IO.Bending
|
||||
@"\\[fHCTQWASpOoLlKk][^;]*;|\\P|[{}]|%%[dDpPcC]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex UnicodeEscapeRegex = new Regex(
|
||||
@"\\U\+([0-9A-Fa-f]{4})",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public List<Bend> DetectBends(CadDocument document)
|
||||
{
|
||||
var bendLines = FindBendLines(document);
|
||||
@@ -116,8 +120,15 @@ namespace OpenNest.IO.Bending
|
||||
if (string.IsNullOrEmpty(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
|
||||
var result = text
|
||||
result = result
|
||||
.Replace("%%d", "°").Replace("%%D", "°")
|
||||
.Replace("%%p", "±").Replace("%%P", "±")
|
||||
.Replace("%%c", "⌀").Replace("%%C", "⌀");
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("Name for the drawing")] string name,
|
||||
[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("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("G-code string (only used when shape is 'gcode')")] string gcode = null)
|
||||
{
|
||||
@@ -131,7 +131,7 @@ namespace OpenNest.Mcp.Tools
|
||||
switch (shape.ToLower())
|
||||
{
|
||||
case "rectangle":
|
||||
shapeDef = new RectangleShape { Name = name, Width = width, Height = height };
|
||||
shapeDef = new RectangleShape { Name = name, Width = width, Length = length };
|
||||
break;
|
||||
|
||||
case "circle":
|
||||
@@ -139,11 +139,11 @@ namespace OpenNest.Mcp.Tools
|
||||
break;
|
||||
|
||||
case "l_shape":
|
||||
shapeDef = new LShape { Name = name, Width = width, Height = height };
|
||||
shapeDef = new LShape { Name = name, Width = width, Height = length };
|
||||
break;
|
||||
|
||||
case "t_shape":
|
||||
shapeDef = new TShape { Name = name, Width = width, Height = height };
|
||||
shapeDef = new TShape { Name = name, Width = width, Height = length };
|
||||
break;
|
||||
|
||||
case "gcode":
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace OpenNest.Mcp.Tools
|
||||
[Description("X origin of the area")] double x,
|
||||
[Description("Y origin of the area")] double y,
|
||||
[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)
|
||||
{
|
||||
var plate = _session.GetPlate(plateIndex);
|
||||
@@ -73,14 +73,14 @@ namespace OpenNest.Mcp.Tools
|
||||
var countBefore = plate.Parts.Count;
|
||||
var engine = NestEngineRegistry.Create(plate);
|
||||
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 countAfter = plate.Parts.Count;
|
||||
var added = countAfter - countBefore;
|
||||
|
||||
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($" Total parts: {countAfter}");
|
||||
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.")]
|
||||
public string CreatePlate(
|
||||
[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("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("Material name (optional)")] string material = null)
|
||||
{
|
||||
var plate = new Plate(width, height);
|
||||
var plate = new Plate(width, length);
|
||||
plate.PartSpacing = partSpacing;
|
||||
plate.EdgeSpacing = new Spacing(edgeSpacing, edgeSpacing);
|
||||
plate.Quadrant = quadrant;
|
||||
|
||||
@@ -87,4 +87,30 @@ public class BendModelTests
|
||||
Assert.Contains("90", 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.IO.Bending;
|
||||
|
||||
@@ -27,4 +28,27 @@ public class SolidWorksBendDetectorTests
|
||||
var bends = BendDetectorRegistry.AutoDetect(doc);
|
||||
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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Bending\TestData\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -7,7 +7,7 @@ public class RectangleShapeTests
|
||||
[Fact]
|
||||
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 bbox = drawing.Program.BoundingBox();
|
||||
@@ -18,7 +18,7 @@ public class RectangleShapeTests
|
||||
[Fact]
|
||||
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();
|
||||
|
||||
Assert.Equal("Rectangle", drawing.Name);
|
||||
@@ -27,7 +27,7 @@ public class RectangleShapeTests
|
||||
[Fact]
|
||||
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();
|
||||
|
||||
Assert.Equal("Plate1", drawing.Name);
|
||||
|
||||
@@ -7,7 +7,7 @@ public class RoundedRectangleShapeTests
|
||||
[Fact]
|
||||
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 bbox = drawing.Program.BoundingBox();
|
||||
@@ -18,7 +18,7 @@ public class RoundedRectangleShapeTests
|
||||
[Fact]
|
||||
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();
|
||||
|
||||
// Area should be less than 20*10=200 because corners are rounded
|
||||
@@ -30,7 +30,7 @@ public class RoundedRectangleShapeTests
|
||||
[Fact]
|
||||
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();
|
||||
|
||||
Assert.Equal(200, drawing.Area, 0.5);
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
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]
|
||||
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 parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
@@ -42,7 +27,7 @@ public class DrawingSplitterTests
|
||||
[Fact]
|
||||
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 parameters = new SplitParameters { Type = SplitType.Straight };
|
||||
|
||||
@@ -56,7 +41,7 @@ public class DrawingSplitterTests
|
||||
[Fact]
|
||||
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>
|
||||
{
|
||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||
@@ -75,7 +60,7 @@ public class DrawingSplitterTests
|
||||
[Fact]
|
||||
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.Priority = 5;
|
||||
|
||||
@@ -93,7 +78,7 @@ public class DrawingSplitterTests
|
||||
[Fact]
|
||||
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,
|
||||
new List<SplitLine> { new SplitLine(50.0, CutOffAxis.Vertical) },
|
||||
new SplitParameters());
|
||||
@@ -146,7 +131,7 @@ public class DrawingSplitterTests
|
||||
[Fact]
|
||||
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>
|
||||
{
|
||||
new SplitLine(50.0, CutOffAxis.Vertical),
|
||||
@@ -160,4 +145,263 @@ public class DrawingSplitterTests
|
||||
Assert.Equal("GRID-3", results[2].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.Geometry;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Splitting;
|
||||
|
||||
@@ -8,16 +9,7 @@ public class SplitIntegrationTest
|
||||
[Fact]
|
||||
public void Split_SpikeGroove_NoContinuityGaps()
|
||||
{
|
||||
// Create a rectangle
|
||||
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 drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
|
||||
|
||||
var sl = new SplitLine(50.0, CutOffAxis.Vertical);
|
||||
sl.FeaturePositions.Add(12.5);
|
||||
@@ -60,15 +52,7 @@ public class SplitIntegrationTest
|
||||
[Fact]
|
||||
public void Split_SpikeGroove_Horizontal_NoContinuityGaps()
|
||||
{
|
||||
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 drawing = new RectangleShape { Name = "TEST", Length = 100, Width = 50 }.GetDrawing();
|
||||
|
||||
var sl = new SplitLine(25.0, CutOffAxis.Horizontal);
|
||||
sl.FeaturePositions.Add(25.0);
|
||||
|
||||
@@ -42,7 +42,7 @@ public class AutoSplitCalculatorTests
|
||||
|
||||
Assert.Single(lines);
|
||||
Assert.Equal(CutOffAxis.Vertical, lines[0].Axis);
|
||||
Assert.Equal(50.0, lines[0].Position, 1);
|
||||
Assert.Equal(58.0, lines[0].Position, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using OpenNest.Bending;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
@@ -17,6 +18,20 @@ namespace OpenNest.Controls
|
||||
private readonly Pen gridPen = new Pen(Color.FromArgb(70, 70, 70));
|
||||
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()
|
||||
{
|
||||
Entities = new List<Entity>();
|
||||
@@ -34,6 +49,13 @@ namespace OpenNest.Controls
|
||||
{
|
||||
base.OnMouseClick(e);
|
||||
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)
|
||||
@@ -117,6 +139,20 @@ namespace OpenNest.Controls
|
||||
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)
|
||||
{
|
||||
if (color.IsEmpty || color.A == 0)
|
||||
@@ -158,9 +194,9 @@ namespace OpenNest.Controls
|
||||
{
|
||||
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++)
|
||||
@@ -168,10 +204,44 @@ namespace OpenNest.Controls
|
||||
var bend = Bends[i];
|
||||
var pt1 = PointWorldToGraph(bend.StartPoint);
|
||||
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)
|
||||
{
|
||||
if (disposing)
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace OpenNest.Controls
|
||||
private readonly CheckedListBox colorsList;
|
||||
private readonly CheckedListBox lineTypesList;
|
||||
private readonly ListBox bendLinesList;
|
||||
private readonly LinkLabel bendAddLink;
|
||||
|
||||
private List<Entity> currentEntities;
|
||||
private List<Bend> currentBends;
|
||||
@@ -26,6 +27,7 @@ namespace OpenNest.Controls
|
||||
public event EventHandler FilterChanged;
|
||||
public event EventHandler<int> BendLineSelected;
|
||||
public event EventHandler<int> BendLineRemoved;
|
||||
public event EventHandler AddBendLineClicked;
|
||||
|
||||
public FilterPanel()
|
||||
{
|
||||
@@ -51,9 +53,8 @@ namespace OpenNest.Controls
|
||||
|
||||
var bendDeleteLink = new LinkLabel
|
||||
{
|
||||
Text = "Remove Selected",
|
||||
Dock = DockStyle.Bottom,
|
||||
Height = 20,
|
||||
Text = "Remove",
|
||||
AutoSize = true,
|
||||
Font = new Font("Segoe UI", 8f)
|
||||
};
|
||||
bendDeleteLink.LinkClicked += (s, e) =>
|
||||
@@ -62,8 +63,27 @@ namespace OpenNest.Controls
|
||||
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(bendDeleteLink);
|
||||
bendLinesPanel.ContentPanel.Controls.Add(bendLinksPanel);
|
||||
|
||||
// Line Types
|
||||
lineTypesPanel = new CollapsiblePanel
|
||||
@@ -237,6 +257,12 @@ namespace OpenNest.Controls
|
||||
|
||||
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
|
||||
|
||||
@@ -131,6 +131,8 @@ namespace OpenNest.Controls
|
||||
|
||||
public bool DrawRapid { get; set; }
|
||||
|
||||
public bool DrawPiercePoints { get; set; }
|
||||
|
||||
public bool DrawBounds { get; set; }
|
||||
|
||||
public bool DrawOffset { get; set; }
|
||||
@@ -617,6 +619,9 @@ namespace OpenNest.Controls
|
||||
|
||||
if (DrawRapid)
|
||||
DrawRapids(g);
|
||||
|
||||
if (DrawPiercePoints)
|
||||
DrawAllPiercePoints(g);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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.BendLineSelected += OnBendLineSelected;
|
||||
filterPanel.BendLineRemoved += OnBendLineRemoved;
|
||||
filterPanel.AddBendLineClicked += OnAddBendLineClicked;
|
||||
entityView1.LinePicked += OnLinePicked;
|
||||
entityView1.PickCancelled += OnPickCancelled;
|
||||
btnSplit.Click += OnSplitClicked;
|
||||
numQuantity.ValueChanged += OnQuantityChanged;
|
||||
txtCustomer.TextChanged += OnCustomerChanged;
|
||||
@@ -132,6 +135,11 @@ namespace OpenNest.Forms
|
||||
private void LoadItem(FileListItem item)
|
||||
{
|
||||
entityView1.ClearPenCache();
|
||||
if (entityView1.IsPickingBendLine)
|
||||
{
|
||||
entityView1.IsPickingBendLine = false;
|
||||
filterPanel.SetPickMode(false);
|
||||
}
|
||||
entityView1.Entities.Clear();
|
||||
entityView1.Entities.AddRange(item.Entities);
|
||||
entityView1.Bends = item.Bends ?? new List<Bend>();
|
||||
@@ -139,6 +147,7 @@ namespace OpenNest.Forms
|
||||
item.Entities.ForEach(e => e.IsVisible = true);
|
||||
if (item.Entities.Any(e => e.Layer != null))
|
||||
item.Entities.ForEach(e => e.Layer.IsVisible = true);
|
||||
ReHidePromotedEntities(item.Bends);
|
||||
|
||||
filterPanel.LoadItem(item.Entities, item.Bends);
|
||||
|
||||
@@ -170,6 +179,7 @@ namespace OpenNest.Forms
|
||||
if (item == null) return;
|
||||
|
||||
filterPanel.ApplyFilters(item.Entities);
|
||||
ReHidePromotedEntities(item.Bends);
|
||||
entityView1.Invalidate();
|
||||
}
|
||||
|
||||
@@ -184,6 +194,10 @@ namespace OpenNest.Forms
|
||||
var item = CurrentItem;
|
||||
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);
|
||||
entityView1.Bends = item.Bends;
|
||||
entityView1.SelectedBendIndex = -1;
|
||||
@@ -231,14 +245,25 @@ namespace OpenNest.Forms
|
||||
shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities));
|
||||
|
||||
var pgm = ConvertGeometry.ToProgram(drawEntities);
|
||||
var originOffset = Vector.Zero;
|
||||
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
|
||||
{
|
||||
var rapid = (RapidMove)pgm[0];
|
||||
pgm.Offset(-rapid.EndPoint);
|
||||
originOffset = rapid.EndPoint;
|
||||
pgm.Offset(-originOffset);
|
||||
pgm.Codes.RemoveAt(0);
|
||||
}
|
||||
|
||||
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);
|
||||
if (form.ShowDialog(this) != DialogResult.OK || form.ResultDrawings?.Count <= 1)
|
||||
@@ -255,31 +280,86 @@ namespace OpenNest.Forms
|
||||
var newItems = new List<string>();
|
||||
|
||||
var splitWriter = new SplitDxfWriter();
|
||||
var splitItems = new List<FileListItem>();
|
||||
|
||||
for (var i = 0; i < form.ResultDrawings.Count; i++)
|
||||
{
|
||||
var splitDrawing = form.ResultDrawings[i];
|
||||
|
||||
// Assign bends from the source item — spatial filtering is a future enhancement
|
||||
splitDrawing.Bends.AddRange(item.Bends);
|
||||
|
||||
var splitName = $"{baseName}_split{i + 1}.dxf";
|
||||
var splitName = $"{baseName}-{i + 1}.dxf";
|
||||
var splitPath = GetUniquePath(Path.Combine(writableDir, splitName));
|
||||
|
||||
splitWriter.Write(splitPath, splitDrawing);
|
||||
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);
|
||||
foreach (var path in newItems)
|
||||
AddFile(path);
|
||||
foreach (var splitItem in splitItems)
|
||||
fileList.AddItem(splitItem);
|
||||
|
||||
if (writableDir != sourceDir)
|
||||
MessageBox.Show($"Split files written to: {writableDir}", "Split Output",
|
||||
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)
|
||||
{
|
||||
if (e.Data.GetDataPresent(DataFormats.FileDrop))
|
||||
@@ -358,6 +438,16 @@ namespace OpenNest.Forms
|
||||
|
||||
#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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -473,6 +473,12 @@ namespace OpenNest.Forms
|
||||
PlateView.Invalidate();
|
||||
}
|
||||
|
||||
public void TogglePiercePoints()
|
||||
{
|
||||
PlateView.DrawPiercePoints = !PlateView.DrawPiercePoints;
|
||||
PlateView.Invalidate();
|
||||
}
|
||||
|
||||
public void ToggleDrawBounds()
|
||||
{
|
||||
PlateView.DrawBounds = !PlateView.DrawBounds;
|
||||
|
||||
Generated
+12
-2
@@ -48,6 +48,7 @@
|
||||
mnuEditSelectAll = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuView = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuViewDrawRapids = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuViewDrawPiercePoints = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuViewDrawBounds = new System.Windows.Forms.ToolStripMenuItem();
|
||||
mnuViewDrawOffset = new System.Windows.Forms.ToolStripMenuItem();
|
||||
toolStripMenuItem5 = new System.Windows.Forms.ToolStripSeparator();
|
||||
@@ -297,7 +298,7 @@
|
||||
//
|
||||
// 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.Size = new System.Drawing.Size(44, 20);
|
||||
mnuView.Text = "&View";
|
||||
@@ -308,7 +309,15 @@
|
||||
mnuViewDrawRapids.Size = new System.Drawing.Size(222, 22);
|
||||
mnuViewDrawRapids.Text = "Draw Rapids";
|
||||
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.CheckOnClick = true;
|
||||
@@ -1131,6 +1140,7 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuEditSelectAll;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuView;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawRapids;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawPiercePoints;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawBounds;
|
||||
private System.Windows.Forms.ToolStripMenuItem mnuViewDrawOffset;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
|
||||
|
||||
@@ -78,10 +78,24 @@ namespace OpenNest.Forms
|
||||
|
||||
private string GetNestName(DateTime date, int id)
|
||||
{
|
||||
var month = date.Month.ToString().PadLeft(2, '0');
|
||||
var day = date.Day.ToString().PadLeft(2, '0');
|
||||
var year = (date.Year % 100).ToString("D2");
|
||||
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)
|
||||
@@ -377,6 +391,7 @@ namespace OpenNest.Forms
|
||||
activeForm.PlateView.PartAdded += PlateView_PartAdded;
|
||||
activeForm.PlateView.PartRemoved += PlateView_PartRemoved;
|
||||
mnuViewDrawRapids.Checked = activeForm.PlateView.DrawRapid;
|
||||
mnuViewDrawPiercePoints.Checked = activeForm.PlateView.DrawPiercePoints;
|
||||
mnuViewDrawBounds.Checked = activeForm.PlateView.DrawBounds;
|
||||
statusLabel1.Text = activeForm.PlateView.Status;
|
||||
}
|
||||
@@ -539,6 +554,13 @@ namespace OpenNest.Forms
|
||||
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)
|
||||
{
|
||||
if (activeForm == null) return;
|
||||
@@ -826,7 +848,6 @@ namespace OpenNest.Forms
|
||||
}
|
||||
|
||||
var finder = RemnantFinder.FromPlate(plate);
|
||||
var tiered = finder.FindTieredRemnants(minDim);
|
||||
|
||||
if (remnantViewer == null || remnantViewer.IsDisposed)
|
||||
{
|
||||
@@ -840,7 +861,7 @@ namespace OpenNest.Forms
|
||||
Top);
|
||||
}
|
||||
|
||||
remnantViewer.LoadRemnants(tiered, activeForm.PlateView);
|
||||
remnantViewer.LoadRemnants(finder, minDim, activeForm.PlateView);
|
||||
remnantViewer.Show();
|
||||
remnantViewer.BringToFront();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ namespace OpenNest.Forms
|
||||
public class RemnantViewerForm : Form
|
||||
{
|
||||
private ListView listView;
|
||||
private CheckBox filterCheckBox;
|
||||
private PlateView plateView;
|
||||
private RemnantFinder finder;
|
||||
private double minDim;
|
||||
private List<TieredRemnant> remnants = new();
|
||||
private int selectedIndex = -1;
|
||||
|
||||
@@ -24,6 +27,15 @@ namespace OpenNest.Forms
|
||||
ShowInTaskbar = false;
|
||||
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
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
@@ -42,6 +54,7 @@ namespace OpenNest.Forms
|
||||
listView.SelectedIndexChanged += ListView_SelectedIndexChanged;
|
||||
|
||||
Controls.Add(listView);
|
||||
Controls.Add(filterCheckBox);
|
||||
}
|
||||
|
||||
protected override bool ProcessDialogKey(Keys keyData)
|
||||
@@ -54,10 +67,25 @@ namespace OpenNest.Forms
|
||||
return base.ProcessDialogKey(keyData);
|
||||
}
|
||||
|
||||
public void LoadRemnants(List<TieredRemnant> tieredRemnants, PlateView view)
|
||||
public void LoadRemnants(RemnantFinder finder, double minDim, 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;
|
||||
|
||||
listView.BeginUpdate();
|
||||
|
||||
+39
-36
@@ -29,9 +29,6 @@ namespace OpenNest.Forms
|
||||
private void InitializeComponent()
|
||||
{
|
||||
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();
|
||||
nudSpikePairCount = new System.Windows.Forms.NumericUpDown();
|
||||
lblSpikePairCount = new System.Windows.Forms.Label();
|
||||
@@ -70,6 +67,9 @@ namespace OpenNest.Forms
|
||||
radByCount = new System.Windows.Forms.RadioButton();
|
||||
radFitToPlate = 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();
|
||||
toolStrip = new System.Windows.Forms.ToolStrip();
|
||||
btnAddLine = new System.Windows.Forms.ToolStripButton();
|
||||
@@ -96,6 +96,7 @@ namespace OpenNest.Forms
|
||||
((System.ComponentModel.ISupportInitialize)nudPlateHeight).BeginInit();
|
||||
((System.ComponentModel.ISupportInitialize)nudPlateWidth).BeginInit();
|
||||
grpMethod.SuspendLayout();
|
||||
pnlButtons.SuspendLayout();
|
||||
toolStrip.SuspendLayout();
|
||||
statusStrip.SuspendLayout();
|
||||
SuspendLayout();
|
||||
@@ -116,38 +117,6 @@ namespace OpenNest.Forms
|
||||
pnlSettings.Padding = new System.Windows.Forms.Padding(6);
|
||||
pnlSettings.Size = new System.Drawing.Size(220, 611);
|
||||
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
|
||||
//
|
||||
@@ -216,7 +185,7 @@ namespace OpenNest.Forms
|
||||
nudGrooveDepth.Name = "nudGrooveDepth";
|
||||
nudGrooveDepth.Size = new System.Drawing.Size(88, 23);
|
||||
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;
|
||||
//
|
||||
// lblGrooveDepth
|
||||
@@ -568,6 +537,39 @@ namespace OpenNest.Forms
|
||||
radManual.Text = "Manual";
|
||||
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.BackColor = System.Drawing.Color.FromArgb(33, 40, 48);
|
||||
@@ -667,6 +669,7 @@ namespace OpenNest.Forms
|
||||
((System.ComponentModel.ISupportInitialize)nudPlateWidth).EndInit();
|
||||
grpMethod.ResumeLayout(false);
|
||||
grpMethod.PerformLayout();
|
||||
pnlButtons.ResumeLayout(false);
|
||||
toolStrip.ResumeLayout(false);
|
||||
toolStrip.PerformLayout();
|
||||
statusStrip.ResumeLayout(false);
|
||||
|
||||
@@ -18,6 +18,7 @@ public partial class SplitDrawingForm : Form
|
||||
private readonly List<SplitLine> _splitLines = new();
|
||||
private CutOffAxis _currentAxis = CutOffAxis.Vertical;
|
||||
private bool _placingLine;
|
||||
private Vector _placingCursor;
|
||||
|
||||
// Feature handle drag state
|
||||
private int _dragLineIndex = -1;
|
||||
@@ -74,30 +75,22 @@ public partial class SplitDrawingForm : Form
|
||||
|
||||
if (axisIndex == 1)
|
||||
{
|
||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||
var usable = plateW - 2 * spacing - overhang;
|
||||
if (usable > 0)
|
||||
{
|
||||
var splits = (int)System.Math.Ceiling(_drawingBounds.Width / usable) - 1;
|
||||
if (splits > 0)
|
||||
{
|
||||
var step = _drawingBounds.Width / (splits + 1);
|
||||
for (var i = 1; i <= splits; i++)
|
||||
_splitLines.Add(new SplitLine(_drawingBounds.X + step * i, CutOffAxis.Vertical));
|
||||
}
|
||||
for (var i = 1; i <= splits; i++)
|
||||
_splitLines.Add(new SplitLine(_drawingBounds.X + usable * i, CutOffAxis.Vertical));
|
||||
}
|
||||
}
|
||||
else if (axisIndex == 2)
|
||||
{
|
||||
var usable = System.Math.Min(plateW, plateH) - 2 * spacing - overhang;
|
||||
var usable = plateH - 2 * spacing - overhang;
|
||||
if (usable > 0)
|
||||
{
|
||||
var splits = (int)System.Math.Ceiling(_drawingBounds.Length / usable) - 1;
|
||||
if (splits > 0)
|
||||
{
|
||||
var step = _drawingBounds.Length / (splits + 1);
|
||||
for (var i = 1; i <= splits; i++)
|
||||
_splitLines.Add(new SplitLine(_drawingBounds.Y + step * i, CutOffAxis.Horizontal));
|
||||
}
|
||||
for (var i = 1; i <= splits; i++)
|
||||
_splitLines.Add(new SplitLine(_drawingBounds.Y + usable * i, CutOffAxis.Horizontal));
|
||||
}
|
||||
}
|
||||
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}";
|
||||
}
|
||||
|
||||
@@ -339,6 +338,7 @@ public partial class SplitDrawingForm : Form
|
||||
if (keyData == Keys.Space)
|
||||
{
|
||||
_currentAxis = _currentAxis == CutOffAxis.Vertical ? CutOffAxis.Horizontal : CutOffAxis.Vertical;
|
||||
pnlPreview.Invalidate();
|
||||
return true;
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// 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
|
||||
var parameters = GetCurrentParameters();
|
||||
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
|
||||
if (!radStraight.Checked)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user