Compare commits

...

7 Commits

Author SHA1 Message Date
3c1700c480 fix: repair double-encoded degree symbol in DXF output
ACadSharp misreads UTF-8 degree symbol (C2 B0) as two ANSI_1252
characters (°) then writes that back out. Post-process the saved
DXF to replace ° with ° so bend notes display correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:28:38 -05:00
f2f50f9914 fix: set bend line entity color to ByLayer
Bend lines retained their original explicit color (white) from
SolidWorks after being moved to the BEND layer. Now set to ByLayer
so they inherit the layer's yellow color.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:51:54 -05:00
bf36a56387 fix: detect bend lines from SolidWorks DXF exports
Three issues prevented bend line detection after the ACadSharp migration:

1. IsBendLine required layer "BEND" but SolidWorks exports bend lines
   on layer "0" with CENTERX2 linetype. Added "0" as accepted layer.

2. Bend note regex used literal ° which failed when ACadSharp reads the
   degree symbol as multi-byte "°" due to ANSI/UTF-8 encoding mismatch.
   Changed to [^R\d]* to tolerate any characters between angle and R.

3. Radius filter excluded bends with null Radius (unparsed notes).
   Nullable comparison null <= 4.0 evaluates to false in C#, filtering
   out all bends. Added null check to include unparsed bends.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:33:59 -05:00
6e131d402e fix: upgrade ACadSharp 3.1.32 → 3.4.9 to fix missing OBJECTS section
The DxfWriter in 3.1.32 wrote root dictionary entries referencing child
dictionary objects (ACAD_GROUP, ACAD_LAYOUT, etc.) but never serialized
the actual objects. This caused AutoCAD to report "GroupTable dictionary
was not defined in NamedObject dictionary". Version 3.4.9 includes the
root dictionary fix from PR #957.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:20:45 -05:00
f04c75235c fix: enable CreateDefaults on DxfReader to fix missing ACAD_GROUP dictionary
SolidWorks DXF exports don't include the ACAD_GROUP entry in the Named
Object Dictionary. Without it, AutoCAD reports "GroupTable dictionary was
not defined in NamedObject dictionary" when opening the file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:13:06 -05:00
2e8f0e60c5 feat: switch from netDxf to ACadSharp for DXF operations
Replace vendored netDxf git submodule with ACadSharp NuGet package
(v3.1.32). This aligns the DXF dependency with the parent project.

API migration: DxfDocument→CadDocument, Vector3→XYZ, Vector2→XY,
MText.Position→MText.InsertPoint, Line.Linetype→Line.LineType,
DxfDocument.Load()→DxfReader, doc.Save()→DxfWriter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:52:08 -05:00
b0e48442ca fix: return etch lines and support additional bend layer names
- Fix GetEtchLines() using yield break instead of return lines, which
  caused all etch lines to be silently discarded
- Support BEND LINES and BENDLINES layer names in addition to BEND
- Move all bend lines to BEND layer before processing
- Migrate csproj from .NET Framework 4.8 to SDK-style net8.0-windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:37:22 -05:00
7 changed files with 122 additions and 137 deletions

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "netDxf"]
path = netDxf
url = https://github.com/haplokuon/netDxf.git

View File

@@ -1,6 +1,4 @@
using netDxf;
using netDxf.Entities;
using netDxf.Tables;
using ACadSharp.Entities;
using System;
using System.Collections.Generic;
@@ -69,7 +67,7 @@ namespace EtchBendLines
public double? Angle { get; set; }
public override string ToString()
public override string ToString()
=> $"{Direction} {(Angle?.ToString("0.##") ?? "?")}° R{(Radius?.ToString("0.##") ?? "?")}";
}
}

View File

@@ -1,5 +1,7 @@
using netDxf;
using netDxf.Entities;
using ACadSharp;
using ACadSharp.Entities;
using ACadSharp.IO;
using CSMath;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -11,12 +13,13 @@ namespace EtchBendLines
{
public BendLineExtractor(string dxfFile)
{
DxfDocument = DxfDocument.Load(dxfFile);
using var reader = new DxfReader(dxfFile);
Document = reader.Read();
}
public BendLineExtractor(DxfDocument dxfDocument)
public BendLineExtractor(CadDocument document)
{
DxfDocument = dxfDocument;
Document = document;
}
/// <summary>
@@ -33,11 +36,11 @@ namespace EtchBendLines
/// The regular expression pattern the bend note must match
/// </summary>
static readonly Regex bendNoteRegex = new Regex(
@"\b(?<direction>UP|DOWN|DN)\s+(?<angle>\d+(\.\d+)?)°?\s*R\s*(?<radius>\d+(\.\d+)?)\b",
@"\b(?<direction>UP|DOWN|DN)\s+(?<angle>\d+(\.\d+)?)[^R\d]*R\s*(?<radius>\d+(\.\d+)?)\b",
RegexOptions.Compiled | RegexOptions.IgnoreCase
);
public DxfDocument DxfDocument { get; private set; }
public CadDocument Document { get; private set; }
public List<Bend> GetBendLines()
{
@@ -47,7 +50,7 @@ namespace EtchBendLines
if (ReplaceSharpRadius)
FixSharpBends();
foreach (var line in DxfDocument.Lines)
foreach (var line in Document.Entities.OfType<Line>())
{
if (!IsBendLine(line))
continue;
@@ -63,17 +66,29 @@ namespace EtchBendLines
AssignBendDirections(bends, bendNotes);
return bends.Where(b => b.Radius <= MaxBendRadius).ToList();
return bends.Where(b => b.Radius == null || b.Radius <= MaxBendRadius).ToList();
}
private bool IsBendLine(Line line)
{
return line.Linetype.Name == "CENTERX2" && line.Layer.Name == "BEND";
if (line.LineType.Name != "CENTERX2")
return false;
switch (line.Layer.Name.ToUpperInvariant())
{
case "0":
case "BEND":
case "BEND LINES":
case "BENDLINES":
return true;
default:
return false;
}
}
private List<MText> GetBendNotes()
{
return DxfDocument.MTexts
return Document.Entities.OfType<MText>()
.Where(t => GetBendDirection(t) != BendDirection.Unknown)
.ToList();
}
@@ -148,7 +163,7 @@ namespace EtchBendLines
for (int i = bendNotesList.Count - 1; i >= 0; i--)
{
var note = bendNotesList[i];
var notePos = note.Position.ToVector2();
var notePos = note.InsertPoint.ToXY();
var perpendicularPoint = bendLine.PointPerpendicularTo(notePos);
var dist = notePos.DistanceTo(perpendicularPoint);
var maxAcceptableDist = note.Height * 2.0;
@@ -161,14 +176,14 @@ namespace EtchBendLines
return null;
var closestNote = bendNotesList.First();
var p1 = closestNote.Position.ToVector2();
var p1 = closestNote.InsertPoint.ToXY();
var p2 = bendLine.ClosestPointOnLineTo(p1);
var dist2 = p1.DistanceTo(p2);
for (int i = 1; i < bendNotesList.Count; i++)
{
var note = bendNotesList[i];
var p3 = note.Position.ToVector2();
var p3 = note.InsertPoint.ToXY();
var p4 = bendLine.ClosestPointOnLineTo(p3);
var dist = p3.DistanceTo(p4);

View File

@@ -1,69 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}</ProjectGuid>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<OutputType>Library</OutputType>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
<RootNamespace>EtchBendLines</RootNamespace>
<AssemblyName>EtchBendLines</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<NoWin32Manifest>true</NoWin32Manifest>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Configuration" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<PackageReference Include="ACadSharp" Version="3.4.9" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<Compile Include="AppConfig.cs" />
<Compile Include="Bend.cs" />
<Compile Include="BendDirection.cs" />
<Compile Include="BendLineExtractor.cs" />
<Compile Include="Etcher.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\netDxf\netDxf\netDxf.csproj">
<Project>{785380e0-ceb9-4c34-82e5-60d0e33e848e}</Project>
<Name>netDxf</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
</Project>

View File

@@ -1,6 +1,8 @@
using netDxf;
using netDxf.Entities;
using netDxf.Tables;
using ACadSharp;
using ACadSharp.Entities;
using ACadSharp.Tables;
using ACadSharp.IO;
using CSMath;
using System;
using System.Collections.Generic;
using System.IO;
@@ -12,12 +14,12 @@ namespace EtchBendLines
{
public readonly Layer BendLayer = new Layer("BEND")
{
Color = AciColor.Yellow
Color = Color.Yellow
};
static readonly Layer EtchLayer = new Layer("ETCH")
{
Color = AciColor.Green,
Color = Color.Green,
};
private const double DefaultEtchLength = 1.0;
@@ -27,35 +29,35 @@ namespace EtchBendLines
/// </summary>
public double MaxBendRadius { get; set; } = 4.0;
private DxfDocument LoadDocument(string path)
private CadDocument LoadDocument(string path)
{
try
{
return DxfDocument.Load(path)
using var reader = new DxfReader(path);
reader.Configuration.CreateDefaults = true;
return reader.Read()
?? throw new InvalidOperationException("DXF load returned null");
}
catch (Exception ex)
catch (Exception ex) when (ex is not InvalidOperationException)
{
throw new ApplicationException($"Failed to load DXF '{path}'", ex);
}
}
private IEnumerable<Bend> ExtractUpBends(DxfDocument doc)
private List<Bend> ExtractBends(CadDocument doc)
{
// your existing BendLineExtractor logic
var extractor = new BendLineExtractor(doc);
return extractor.GetBendLines()
.Where(b => b.Direction == BendDirection.Up);
return extractor.GetBendLines();
}
private HashSet<string> BuildExistingKeySet(DxfDocument doc)
private HashSet<string> BuildExistingKeySet(CadDocument doc)
=> new HashSet<string>(
doc.Lines
doc.Entities.OfType<Line>()
.Where(l => IsEtchLayer(l.Layer))
.Select(l => KeyFor(l.StartPoint, l.EndPoint))
);
private void InsertEtchLines(DxfDocument doc, IEnumerable<Bend> bends, HashSet<string> existingKeys, double etchLength)
private void InsertEtchLines(CadDocument doc, IEnumerable<Bend> bends, HashSet<string> existingKeys, double etchLength)
{
foreach (var bend in bends)
{
@@ -65,35 +67,60 @@ namespace EtchBendLines
if (existingKeys.Contains(key))
{
// ensure correct layer
var existing = doc.Lines.First(l => KeyFor(l) == key);
var existing = doc.Entities.OfType<Line>().First(l => KeyFor(l) == key);
existing.Layer = EtchLayer;
}
else
{
etch.Layer = EtchLayer;
doc.AddEntity(etch);
doc.Entities.Add(etch);
existingKeys.Add(key);
}
}
}
}
private void SaveDocument(DxfDocument doc, string path)
private void SaveDocument(CadDocument doc, string path)
{
doc.Save(path);
using (var writer = new DxfWriter(path, doc, false))
{
writer.Write();
}
FixDegreeSymbol(path);
Console.WriteLine($"→ Saved with etch lines: {path}");
}
private static void FixDegreeSymbol(string path)
{
var text = File.ReadAllText(path);
if (text.Contains("\u00C2\u00B0"))
{
text = text.Replace("\u00C2\u00B0", "\u00B0");
File.WriteAllText(path, text);
}
}
private static string KeyFor(Line l) => KeyFor(l.StartPoint, l.EndPoint);
private static string KeyFor(Vector3 a, Vector3 b) => $"{a.X:F3},{a.Y:F3}|{b.X:F3},{b.Y:F3}";
private static string KeyFor(XYZ a, XYZ b) => $"{a.X:F3},{a.Y:F3}|{b.X:F3},{b.Y:F3}";
public void AddEtchLines(string filePath, double etchLength = DefaultEtchLength)
{
Console.WriteLine(filePath);
var doc = LoadDocument(filePath);
var upBends = ExtractUpBends(doc);
var bends = ExtractBends(doc);
// Ensure all bend lines are on the BEND layer with ByLayer color
foreach (var bend in bends)
{
bend.Line.Layer = BendLayer;
bend.Line.Color = Color.ByLayer;
}
var upBends = bends.Where(b => b.Direction == BendDirection.Up);
var existing = BuildExistingKeySet(doc);
InsertEtchLines(doc, upBends, existing, etchLength);
@@ -119,12 +146,12 @@ namespace EtchBendLines
}
}
private IEnumerable<Line> GetEtchLines(Line bendLine, double etchLength)
private List<Line> GetEtchLines(Line bendLine, double etchLength)
{
var lines = new List<Line>();
var startPoint = new Vector2(bendLine.StartPoint.X, bendLine.StartPoint.Y);
var endPoint = new Vector2(bendLine.EndPoint.X, bendLine.EndPoint.Y);
var startPoint = new XY(bendLine.StartPoint.X, bendLine.StartPoint.Y);
var endPoint = new XY(bendLine.EndPoint.X, bendLine.EndPoint.Y);
var bendLength = startPoint.DistanceTo(endPoint);
if (bendLength < (etchLength * 3.0))
@@ -145,26 +172,26 @@ namespace EtchBendLines
var topY1 = Math.Max(startPoint.Y, endPoint.Y);
var topY2 = topY1 - etchLength;
var p1 = new Vector2(x, bottomY1);
var p2 = new Vector2(x, bottomY2);
var p3 = new Vector2(x, topY1);
var p4 = new Vector2(x, topY2);
var p1 = new XYZ(x, bottomY1, 0);
var p2 = new XYZ(x, bottomY2, 0);
var p3 = new XYZ(x, topY1, 0);
var p4 = new XYZ(x, topY2, 0);
lines.Add(new Line(p1, p2));
lines.Add(new Line(p3, p4));
}
else
{
var start = bendLine.StartPoint.ToVector2();
var end = bendLine.EndPoint.ToVector2();
var start = bendLine.StartPoint.ToXY();
var end = bendLine.EndPoint.ToXY();
var dx = Math.Cos(angle) * etchLength;
var dy = Math.Sin(angle) * etchLength;
var p1 = new Vector2(start.X, start.Y);
var p2 = new Vector2(start.X + dx, start.Y + dy);
var p3 = new Vector2(end.X, end.Y);
var p4 = new Vector2(end.X - dx, end.Y - dy);
var p1 = new XYZ(start.X, start.Y, 0);
var p2 = new XYZ(start.X + dx, start.Y + dy, 0);
var p3 = new XYZ(end.X, end.Y, 0);
var p4 = new XYZ(end.X - dx, end.Y - dy, 0);
lines.Add(new Line(p1, p2));
lines.Add(new Line(p3, p4));
@@ -176,7 +203,7 @@ namespace EtchBendLines
line.Layer = EtchLayer;
}
yield break;
return lines;
}
}
}

View File

@@ -1,5 +1,5 @@
using netDxf;
using netDxf.Entities;
using ACadSharp.Entities;
using CSMath;
using System;
namespace EtchBendLines
@@ -8,12 +8,12 @@ namespace EtchBendLines
{
const double TwoPI = Math.PI * 2.0;
public static Vector2 ToVector2(this Vector3 pt)
public static XY ToXY(this XYZ pt)
{
return new Vector2(pt.X, pt.Y);
return new XY(pt.X, pt.Y);
}
public static bool IsEqualTo(this Vector3 pt, Vector3 pt1)
public static bool IsEqualTo(this XYZ pt, XYZ pt1)
{
return pt.X.IsEqualTo(pt1.X) && pt.Y.IsEqualTo(pt1.Y) && pt.Z.IsEqualTo(pt1.Z);
}
@@ -50,10 +50,10 @@ namespace EtchBendLines
return Math.Round(p1.Y - slope * p1.X, 4);
}
public static Vector2 PointPerpendicularTo(this Line line, Vector2 pt)
public static XY PointPerpendicularTo(this Line line, XY pt)
{
var startPoint = line.StartPoint.ToVector2();
var endPoint = line.EndPoint.ToVector2();
var startPoint = line.StartPoint.ToXY();
var endPoint = line.EndPoint.ToXY();
var d1 = pt - startPoint;
var d2 = endPoint - startPoint;
@@ -61,20 +61,20 @@ namespace EtchBendLines
var lengthSquared = d2.X * d2.X + d2.Y * d2.Y;
var param = dotProduct / lengthSquared;
return new Vector2(
return new XY(
startPoint.X + param * d2.X,
startPoint.Y + param * d2.Y);
}
public static Vector2 MidPoint(this Line line)
public static XY MidPoint(this Line line)
{
var x = (line.StartPoint.X + line.EndPoint.X) * 0.5;
var y = (line.StartPoint.Y + line.EndPoint.Y) * 0.5;
return new Vector2(x, y);
return new XY(x, y);
}
public static double DistanceTo(this Vector2 startPoint, Vector2 endPoint)
public static double DistanceTo(this XY startPoint, XY endPoint)
{
var x = endPoint.X - startPoint.X;
var y = endPoint.Y - startPoint.Y;
@@ -82,7 +82,7 @@ namespace EtchBendLines
return Math.Sqrt(x * x + y * y);
}
public static double AngleTo(this Vector2 startPoint, Vector2 endPoint)
public static double AngleTo(this XY startPoint, XY endPoint)
{
var x = endPoint.X - startPoint.X;
var y = endPoint.Y - startPoint.Y;
@@ -133,10 +133,10 @@ namespace EtchBendLines
return Math.Abs(b - a) <= tolerance;
}
public static Vector2 ClosestPointOnLineTo(this Line line, Vector2 pt)
public static XY ClosestPointOnLineTo(this Line line, XY pt)
{
var startPoint = line.StartPoint.ToVector2();
var endPoint = line.EndPoint.ToVector2();
var startPoint = line.StartPoint.ToXY();
var endPoint = line.EndPoint.ToXY();
var diff1 = pt - startPoint;
var diff2 = endPoint - startPoint;
@@ -150,7 +150,7 @@ namespace EtchBendLines
return endPoint;
else
{
return new Vector2(
return new XY(
startPoint.X + param * diff2.X,
startPoint.Y + param * diff2.Y);
}

1
netDxf

Submodule netDxf deleted from 67789bfe70