Files
ExportDXF/ExportDXF/Services/PartExporter.cs
AJ Isaacs f9e7ace35d 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:31:57 -05:00

375 lines
14 KiB
C#

using ExportDXF.Extensions;
using ExportDXF.Models;
using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Service for exporting parts to DXF format.
/// </summary>
public interface IPartExporter
{
/// <summary>
/// Exports a single part document to DXF.
/// Returns an Item with export metadata (filename, hash, sheet metal properties), or null if export failed.
/// </summary>
/// <param name="part">The part document to export.</param>
/// <param name="saveDirectory">The temp directory where the DXF file will be saved.</param>
/// <param name="context">The export context.</param>
Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
/// <summary>
/// Exports an item (component from BOM or assembly) to DXF.
/// </summary>
/// <param name="item">The item to export.</param>
/// <param name="saveDirectory">The temp directory where the DXF file will be saved.</param>
/// <param name="context">The export context.</param>
void ExportItem(Item item, string saveDirectory, ExportContext context);
}
public class PartExporter : IPartExporter
{
public PartExporter()
{
}
public Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
{
if (part == null)
throw new ArgumentNullException(nameof(part));
if (string.IsNullOrWhiteSpace(saveDirectory))
throw new ArgumentException("Save directory cannot be null or empty.", nameof(saveDirectory));
if (context == null)
throw new ArgumentNullException(nameof(context));
var model = part as ModelDoc2;
var activeConfig = model.GetActiveConfiguration() as SolidWorks.Interop.sldworks.Configuration;
var originalConfigName = activeConfig?.Name;
try
{
var fileName = GetSinglePartFileName(model, context.FilePrefix);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
// Build result item with metadata
var item = new Item
{
PartName = model.GetTitle()?.Replace(".SLDPRT", "") ?? "",
Configuration = originalConfigName ?? "",
Quantity = 1
};
// Enrich with sheet metal properties and description
var sheetMetalProps = SolidWorksHelper.GetSheetMetalProperties(model);
if (sheetMetalProps != null)
{
item.Thickness = sheetMetalProps.Thickness;
item.KFactor = sheetMetalProps.KFactor;
item.BendRadius = sheetMetalProps.BendRadius;
}
// Get description from custom properties
var configPropMgr = model.Extension.CustomPropertyManager[originalConfigName];
item.Description = configPropMgr?.Get("Description");
if (string.IsNullOrEmpty(item.Description))
{
var docPropMgr = model.Extension.CustomPropertyManager[""];
item.Description = docPropMgr?.Get("Description");
}
item.Description = TextHelper.RemoveXmlTags(item.Description);
// Get material
item.Material = part.GetMaterialPropertyName2(originalConfigName, out _);
context.GetOrCreateTemplateDrawing();
if (ExportPartToDxf(part, originalConfigName, savePath, context))
{
item.FileName = Path.GetFileNameWithoutExtension(savePath);
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
item.LocalTempPath = savePath;
return item;
}
else
{
return null;
}
}
finally
{
if (originalConfigName != null)
{
model.ShowConfiguration(originalConfigName);
}
}
}
public void ExportItem(Item item, string saveDirectory, ExportContext context)
{
if (string.IsNullOrWhiteSpace(saveDirectory))
throw new ArgumentException("Save directory cannot be null or empty.", nameof(saveDirectory));
if (context == null)
throw new ArgumentNullException(nameof(context));
if (item?.Component == null)
{
context.ProgressCallback?.Invoke("Skipped, no component", LogLevel.Warning, $"Item {item?.ItemNo}");
return;
}
context.CancellationToken.ThrowIfCancellationRequested();
item.Component.SetLightweightToResolved();
var model = item.Component.GetModelDoc2() as ModelDoc2;
var part = model as PartDoc;
if (part == null)
{
context.ProgressCallback?.Invoke("Skipped, not a part document", LogLevel.Info, item.PartName);
return;
}
EnrichItemWithMetadata(item, model, part);
var fileName = GetItemFileName(item, context.FilePrefix);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
var templateDrawing = context.GetOrCreateTemplateDrawing();
if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, savePath, context))
{
item.FileName = Path.GetFileNameWithoutExtension(savePath);
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
item.LocalTempPath = savePath;
}
else
{
LogExportFailure(item, context);
}
}
private void EnrichItemWithMetadata(Item item, ModelDoc2 model, PartDoc part)
{
// Get sheet metal properties
var sheetMetalProps = SolidWorksHelper.GetSheetMetalProperties(model);
if (sheetMetalProps != null)
{
item.Thickness = sheetMetalProps.Thickness;
item.KFactor = sheetMetalProps.KFactor;
item.BendRadius = sheetMetalProps.BendRadius;
}
// Get description from custom properties
var config = item.Component.ReferencedConfiguration;
// Try configuration-specific properties first
var configPropertyManager = model.Extension.CustomPropertyManager[config];
item.Description = configPropertyManager?.Get("Description");
// Fall back to document-level properties if no config-specific description
if (string.IsNullOrEmpty(item.Description))
{
var docPropertyManager = model.Extension.CustomPropertyManager[""];
item.Description = docPropertyManager?.Get("Description");
}
item.Description = TextHelper.RemoveXmlTags(item.Description);
// Get material
item.Material = part.GetMaterialPropertyName2(config, out _);
}
private bool ExportPartToDxf(
PartDoc part,
string configName,
string savePath,
ExportContext context)
{
try
{
var model = part as ModelDoc2;
var partTitle = model.GetTitle();
if (!model.IsSheetMetal())
{
context.ProgressCallback?.Invoke("Skipped, not sheet metal", LogLevel.Info, partTitle);
return false;
}
var templateDrawing = context.GetOrCreateTemplateDrawing();
SolidWorksHelper.ConfigureFlatPatternSettings(model);
var sheet = templateDrawing.IGetCurrentSheet();
var modelName = Path.GetFileNameWithoutExtension(model.GetPathName());
sheet.SetName(modelName);
context.ProgressCallback?.Invoke("Creating flat pattern", LogLevel.Info, partTitle);
var view = CreateFlatPatternView(templateDrawing, model, configName);
if (view == null)
{
context.ProgressCallback?.Invoke("Failed to create flat pattern", LogLevel.Error, partTitle);
return false;
}
ConfigureFlatPatternView(view, templateDrawing, model, configName, context);
if (context.ViewFlipDecider?.ShouldFlip(view) == true)
{
context.ProgressCallback?.Invoke("Flipped view", LogLevel.Info, partTitle);
view.FlipView = true;
}
var drawingModel = templateDrawing as ModelDoc2;
drawingModel.SaveAs(savePath);
AddEtchLines(savePath);
context.ProgressCallback?.Invoke($"Saved to \"{savePath}\"", LogLevel.Info, partTitle);
DeleteView(drawingModel, view);
return true;
}
catch (Exception ex)
{
context.ProgressCallback?.Invoke($"Export failed: {ex.Message}", LogLevel.Error, null);
return false;
}
}
private SolidWorks.Interop.sldworks.View CreateFlatPatternView(
DrawingDoc drawing,
ModelDoc2 part,
string configName)
{
return drawing.CreateFlatPatternViewFromModelView3(
part.GetPathName(),
configName,
0, 0, 0,
false,
false);
}
private void ConfigureFlatPatternView(
SolidWorks.Interop.sldworks.View view,
DrawingDoc drawing,
ModelDoc2 partModel,
string configName,
ExportContext context)
{
view.ShowSheetMetalBendNotes = true;
var drawingModel = drawing as ModelDoc2;
drawingModel.ViewZoomtofit2();
var flatPatternModel = ViewHelper.GetModelFromView(view);
SolidWorksHelper.SetFlatPatternSuppressionState(
flatPatternModel,
swComponentSuppressionState_e.swComponentFullyResolved);
if (ViewHelper.HasSupressedBends(view))
{
var title = partModel.GetTitle();
context.ProgressCallback?.Invoke("A bend is suppressed, please check flat pattern", LogLevel.Error, title);
}
if (ViewHelper.HideModelSketches(view))
{
// Recreate view without sketches
DeleteView(drawingModel, view);
view = CreateFlatPatternView(drawing, partModel, configName);
view.ShowSheetMetalBendNotes = true;
}
}
private void DeleteView(ModelDoc2 drawing, SolidWorks.Interop.sldworks.View view)
{
drawing.SelectByName(0, view.Name);
drawing.DeleteSelection(false);
}
private void AddEtchLines(string dxfPath)
{
try
{
var etcher = new EtchBendLines.Etcher();
etcher.AddEtchLines(dxfPath);
FixDegreeSymbol(dxfPath);
}
catch (Exception)
{
// Silently fail if etch lines can't be added
}
}
/// <summary>
/// ACadSharp misreads the UTF-8 degree symbol (C2 B0) as two ANSI_1252
/// characters (°). Fix it in the written DXF so bend notes display correctly.
/// </summary>
private static void FixDegreeSymbol(string path)
{
var text = System.IO.File.ReadAllText(path);
if (text.Contains("\u00C2\u00B0"))
{
text = text.Replace("\u00C2\u00B0", "\u00B0");
System.IO.File.WriteAllText(path, text);
}
}
private string GetSinglePartFileName(ModelDoc2 model, string prefix)
{
var title = model.GetTitle().Replace(".SLDPRT", "");
var config = model.ConfigurationManager.ActiveConfiguration.Name;
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
return isDefaultConfig ? title : $"{title} [{config}]";
}
private string GetItemFileName(Item item, string prefix)
{
if (string.IsNullOrWhiteSpace(item.ItemNo))
return item.PartName;
prefix = prefix?.Replace("\"", "''") ?? string.Empty;
var num = item.ItemNo.PadLeft(2, '0');
// Expected format: {DrawingNo} PT{ItemNo}
return string.IsNullOrWhiteSpace(prefix)
? $"PT{num}"
: $"{prefix} PT{num}";
}
private void LogExportFailure(Item item, ExportContext context)
{
var desc = item.Description?.ToLower() ?? string.Empty;
if (desc.Contains("laser"))
{
context.ProgressCallback?.Invoke(
"Export failed but description says it is laser cut",
LogLevel.Error,
item.PartName);
}
else if (desc.Contains("plasma"))
{
context.ProgressCallback?.Invoke(
"Export failed but description says it is plasma cut",
LogLevel.Error,
item.PartName);
}
}
}
}