fix: preserve bend lines through drawing split — clip, offset, and carry metadata

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) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 09:24:41 -04:00
parent d9005cccc3
commit 98e90cc176
5 changed files with 138 additions and 10 deletions

View File

@@ -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);

View File

@@ -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", "⌀");

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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<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}-{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",