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:
@@ -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", "⌀");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user