From 98e90cc17607bc82ba12c4de62dcc2ebef6531c4 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 25 Mar 2026 09:24:41 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20preserve=20bend=20lines=20through=20draw?= =?UTF-8?q?ing=20split=20=E2=80=94=20clip,=20offset,=20and=20carry=20metad?= =?UTF-8?q?ata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DrawingSplitter now clips bend lines to each piece's region using Liang-Barsky line clipping and offsets them to the new origin. Bend properties (direction, angle, radius, note text) are preserved through the entire split pipeline instead of being lost during re-import. CadConverterForm applies the same origin offset to bends before passing them to the splitter, and creates FileListItems directly from split results to avoid re-detection overwriting the bend metadata. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Splitting/DrawingSplitter.cs | 64 ++++++++++++++++++- OpenNest.IO/Bending/SolidWorksBendDetector.cs | 13 +++- .../Bending/SolidWorksBendDetectorTests.cs | 24 +++++++ OpenNest.Tests/OpenNest.Tests.csproj | 6 ++ OpenNest/Forms/CadConverterForm.cs | 41 ++++++++++-- 5 files changed, 138 insertions(+), 10 deletions(-) diff --git a/OpenNest.Core/Splitting/DrawingSplitter.cs b/OpenNest.Core/Splitting/DrawingSplitter.cs index 0488b53..99e54eb 100644 --- a/OpenNest.Core/Splitting/DrawingSplitter.cs +++ b/OpenNest.Core/Splitting/DrawingSplitter.cs @@ -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 entities, int pieceIndex) + private static Drawing BuildPieceDrawing(Drawing source, List 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(); + 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; } + /// + /// 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. + /// + 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); diff --git a/OpenNest.IO/Bending/SolidWorksBendDetector.cs b/OpenNest.IO/Bending/SolidWorksBendDetector.cs index c441c35..9892f6a 100644 --- a/OpenNest.IO/Bending/SolidWorksBendDetector.cs +++ b/OpenNest.IO/Bending/SolidWorksBendDetector.cs @@ -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 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", "⌀"); diff --git a/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs b/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs index db9dc74..a27b357 100644 --- a/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs +++ b/OpenNest.Tests/Bending/SolidWorksBendDetectorTests.cs @@ -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); + } + } } diff --git a/OpenNest.Tests/OpenNest.Tests.csproj b/OpenNest.Tests/OpenNest.Tests.csproj index 53f0cf0..6ffd66d 100644 --- a/OpenNest.Tests/OpenNest.Tests.csproj +++ b/OpenNest.Tests/OpenNest.Tests.csproj @@ -28,4 +28,10 @@ + + + PreserveNewest + + + diff --git a/OpenNest/Forms/CadConverterForm.cs b/OpenNest/Forms/CadConverterForm.cs index f973742..927726e 100644 --- a/OpenNest/Forms/CadConverterForm.cs +++ b/OpenNest/Forms/CadConverterForm.cs @@ -231,14 +231,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,25 +266,41 @@ namespace OpenNest.Forms var newItems = new List(); var splitWriter = new SplitDxfWriter(); + var splitItems = new List(); 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}-{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(), + 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",