Compare commits

55 Commits

Author SHA1 Message Date
aj 14fa1e6906 chore: add docs/superpowers to gitignore and remove from tracking
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:34:11 -04:00
aj 41022a93cc docs: update README for Excel export and template-based naming
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:33:46 -04:00
aj 0fd117da92 merge: reconcile GitHub history before mirror setup
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:31:21 -04:00
aj 5f28a6ce2b chore: remove netDxf project from solution
netDxf was replaced by ACadSharp in the EtchBendLines submodule.
The netDxf directory was already removed from git tracking;
this removes the stale solution reference.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:26:16 -04:00
aj a4db71f074 fix: update EtchBendLines submodule to ACadSharp version
Advances submodule from netDxf (89d987f) to ACadSharp (da4d322),
picking up all fixes from the fabworks-api branch:
- Switch from netDxf to ACadSharp for DXF operations
- Fix ACAD_GROUP dictionary, upgrade ACadSharp to 3.4.9
- Fix bend line detection and ByLayer color
- Fix degree symbol encoding (reverted in submodule, handled by caller)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:23:18 -04:00
aj 4d01b2654d refactor: rewire Program.cs DI — remove API/DB, add Excel and log services
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:18:31 -04:00
aj 1d3b6b8f0f refactor: replace equipment/drawing dropdowns with filename template textbox
- Remove equipmentBox, drawingNoBox, titleBox and related controls
- Add txtFilenameTemplate text box with auto-fill via IDrawingInfoExtractor
- Validate template before export (must contain {item_no})
- Output to Templates/ folder next to source file
- Remove all API client references from MainForm

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:18:15 -04:00
aj 9bc29e98c8 refactor: rewrite DxfExportService for local file export with revision tracking
- Replace API client with ExcelExportService and LogFileService
- Export DXFs to Templates folder with template-based naming
- Content hash comparison against existing xlsx for revision tracking
- Changed DXFs get revision suffix (e.g., PT03 Rev2.dxf)
- Unchanged DXFs are skipped
- Raw BOM table copied from SolidWorks drawing to Excel
- PDF exported directly to output folder
- Update PartExporter to remove FilePrefix dependency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:16:47 -04:00
aj c6dde6e217 feat: add ExcelExportService, LogFileService, and RawBomTableReader
- ExcelExportService: read/write BOM and Cut Templates xlsx with ClosedXML
- LogFileService: per-export and app-level log file writing
- RawBomTableReader: copy visible SolidWorks BOM table data for Excel output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:14:43 -04:00
aj cf17e71b80 refactor: update models — add CutTemplate.Revision, remove ExportRecord, simplify ExportContext
- CutTemplate: add Revision property, remove EF Core navigation properties
- BomItem: remove EF Core navigation properties and ExportRecordId
- ExportContext: replace Equipment/DrawingNo/Title/FilePrefix with
  FilenameTemplate and OutputFolder
- Delete ExportRecord.cs (replaced by log file)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:13:40 -04:00
aj 742d86ab8a feat: add FilenameTemplateParser with placeholder evaluation and validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:12:58 -04:00
aj ba782b99db feat: add IDrawingInfoExtractor with equipment and default implementations
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:12:38 -04:00
aj 9a33d405e2 refactor: remove API client, database, and EF Core; add ClosedXML
- Delete ApiClient/ directory (FabWorksApiClient, DTOs, interface)
- Delete Data/ and Migrations/ directories
- Add ClosedXML 0.104.2 for Excel output
- Replace FabWorksApiUrl with DefaultSuffix in app.config
- Remove .NET Framework startup config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:11:57 -04:00
aj e0d4563cc6 chore: remove FabWorks.Core, FabWorks.Api, and FabWorks.Tests after merge
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:10:51 -04:00
aj a4f6dffe12 merge: bring fabworks-api structural improvements into master 2026-04-13 22:10:36 -04:00
aj b7d35bbe78 docs: add implementation plan for removing API and switching to Excel export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:09:35 -04:00
aj 5e5c6ab72f docs: add design spec for removing API and switching to Excel export
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:04:29 -04:00
aj 036ab2a55a docs: add context to FixDegreeSymbol workaround
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:42:54 -05:00
aj 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
aj 622cbf1170 fix: update EtchBendLines submodule with degree symbol fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:28:45 -05:00
aj 4a3f33db33 fix: update EtchBendLines submodule with bend line ByLayer color
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:51:59 -05:00
aj 77d0157370 fix: update EtchBendLines submodule with bend detection fixes
Fixes missing etch lines and incorrect bend layer assignment after
the ACadSharp migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:34:05 -05:00
aj 26e9233b30 fix: update EtchBendLines submodule with ACadSharp 3.4.9 upgrade
Fixes DXF files failing to open with "GroupTable dictionary was not
defined in NamedObject dictionary" error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:20:57 -05:00
aj e59584a5c0 fix: update EtchBendLines submodule with ACAD_GROUP dictionary fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:13:12 -05:00
aj dcc508d479 feat: update EtchBendLines submodule with ACadSharp migration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:52:48 -05:00
aj 1266378b51 fix: update EtchBendLines submodule with etch line fix
Updates submodule to include the yield break -> return lines fix
that was causing etch lines to be silently discarded.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 12:37:29 -05:00
aj 5de40ebafd feat: add delete button to exports list and detail pages
Add DELETE /api/exports/{id} endpoint with cascade delete, trash icon
buttons on both the exports list and export detail pages, and disable
browser caching for static files to prevent stale JS issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:34:59 -05:00
aj e072919a59 fix: prevent date wrapping on exports page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:12:49 -05:00
aj 7db44640ca feat: switch web UI to light theme with larger font sizes
Replace dark blueprint theme with a clean light theme for better
readability. Bump all font sizes (10px labels to 12px, 13px body
text to 14px) and improve text contrast for users with reading
glasses. Icon colors now use CSS variables instead of hardcoded hex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:03:21 -05:00
aj 0d5742124e feat: add revision tracking to CutTemplate and scope BOM items to export record
Each export record now keeps a complete BOM snapshot instead of moving
BomItems between records. CutTemplate gains a Revision field that
auto-increments when the content hash changes across exports for the
same drawing+item, and stays the same when the geometry is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:47:11 -05:00
aj 463916c75c fix: resolve drawing dropdown race condition and save PDF hash to export record
Detach EquipmentBox event before programmatically setting equipment to
prevent async UpdateDrawingDropdownAsync from clearing the drawing
selection and duplicating entries. Also update ExportRecord.PdfContentHash
in StorePdfAsync so the web frontend can serve PDF downloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:40:22 -05:00
aj c06d834e05 feat: add PDF download button to export detail page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 22:20:03 -05:00
aj d3c154b875 chore: reset FabWorks.Core migrations from scratch
Delete old incremental migrations and regenerate a single
InitialCreate that creates all tables (ExportRecords, BomItems,
CutTemplates, FormPrograms) with current schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:37:56 -05:00
aj 2721c33a39 fix: parse equipment number from part names without drawing number
Add equipmentOnlyRegex fallback so names like "5028 Prox switch bracket"
correctly extract equipment number 5028 even without a drawing number.
Handle null DrawingNo in ToString and UI dropdown population.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:37:28 -05:00
aj 5ec66f9039 feat: add web frontend for FabWorks API
Add static HTML/CSS/JS frontend with export browser, search, and
file download capabilities served via UseStaticFiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:37:16 -05:00
aj cf76ca8bb1 refactor: wire ExportDXF to use FabWorks API
Replace direct DB access with API client calls throughout MainForm,
DxfExportService, PartExporter, and Program. Add title field to UI,
async export flow, API-based dropdown loading, and file uploads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:37:05 -05:00
aj 696bf2f72c feat: add BomItem upsert and find endpoints
Add find-existing endpoint and upsert logic to POST so re-exporting
a part updates the existing BomItem rather than creating duplicates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:36:52 -05:00
aj 8de441e126 feat: expand ExportsController with search and file endpoints
Add list/search, equipment/drawing number lookups, PDF hash tracking,
cut template lookup, DXF zip download, and wire up FileStorageService
and static files in Program.cs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:36:42 -05:00
aj 8b6950ef28 feat: add Title, EquipmentNo, DrawingNo to ExportRecord
Add separate EquipmentNo and DrawingNo fields alongside the combined
DrawingNumber, plus a Title field for labeling exports. Updated across
Core model, DbContext, API DTOs, and ExportDXF models.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:36:30 -05:00
aj dba68ecc71 feat: add file storage service with content-addressed blob store
Add FileStorageService for DXF/PDF storage using content hashing,
FileStorageOptions config, FilesController for uploads, and
FileBrowserController for browsing stored files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:36:18 -05:00
aj f75b83d483 feat: add FabWorks API client for ExportDXF
Add IFabWorksApiClient interface, FabWorksApiClient implementation,
and DTO classes for communicating with the FabWorks API from the
SolidWorks add-in.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:36:06 -05:00
aj 2273a83e42 refactor: remove local DB and file export from ExportDXF
Remove ExportDxfDbContext, EF migrations, FileExportService, and
SqlServer/EF Tools packages. ExportDXF will now use the FabWorks API
for persistence and file storage instead of direct DB access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 20:35:55 -05:00
aj e10a7ed0ed feat: add EF migration for FormPrograms table
Add initial FabWorksDbContext migration that creates the FormPrograms
table with FK to BomItems. Existing tables (ExportRecords, BomItems,
CutTemplates) are excluded from the migration since they were already
created by ExportDXF's ExportDxfDbContext. Also adds EF Core Design
package to FabWorks.Api for migration tooling support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:37:03 -05:00
aj 16dc74c35d test: add FormProgramService tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:36:02 -05:00
aj 9e5e44c1ed feat: add BomItems and FormPrograms controllers with parse service
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:32:52 -05:00
aj ab76fa61c9 feat: add FabWorks.Api with ExportsController and DTOs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:32:41 -05:00
aj 28c9f715be test: add ProgramReader tests validating CincyLib port
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:27:12 -05:00
aj 2bef75f548 feat: port CincyLib PressBrake parser to FabWorks.Core (net8.0)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:22:46 -05:00
aj 78a8a2197d feat: add FabWorks.Core shared library with entity models and FormProgram
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:20:13 -05:00
aj 719dca1ca5 feat: add export history auto-fill, fix filename prefixes, persist records for all doc types
- Add database-first lookup for equipment/drawing number auto-fill when
  reopening previously exported files
- Remove prefix prepending for named parts (only use prefix for PT## BOM items)
- Create ExportRecord/BomItem/CutTemplate chains for Part and Assembly
  exports, not just Drawings
- Add auto-incrementing item numbers across drawing numbers
- Add content hashing (SHA256) for DXF and PDF versioning with
  stash/archive pattern
- Add EF Core initial migration for ExportDxfDb

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 13:09:02 -05:00
aj a17d8cac49 refactor: consolidate output folder resolution and prefix handling
Move ParseDrawingNumber + GetDrawingOutputFolder into Export() before
the document-type switch so folder resolution happens once. Extract
PrependPrefix helper in PartExporter to deduplicate the prefix guard.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 08:45:53 -05:00
aj 32e8379e9b refactor: extract CutTemplate from BomItem for all-item BOM tracking
BomItems are now created for every BOM item regardless of whether they
produce a DXF. Sheet metal cut data (thickness, k-factor, bend radius,
DXF path, content hash) moved to a new CutTemplate entity with a 1:1
optional relationship. Non-sheet-metal items are counted as "skipped"
instead of "failed" in the export summary. Added Cut Templates tab to
the UI with a DataGridView for viewing cut template records.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:32:17 -05:00
aj 0ace378eff docs: update README to reflect local export and .NET 8 migration
Remove CutFab API references and document the current architecture:
local file export, SQL Server tracking, and .NET 8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:35:41 -05:00
aj 697463f61e feat: disable SolidWorks user input during export
Sets CommandInProgress to block user interaction with SolidWorks
while the DXF export is running, preventing accidental interference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:32:12 -05:00
aj f418573908 Move project to github 2022-09-16 09:59:44 -04:00
28 changed files with 1281 additions and 653 deletions
+3
View File
@@ -245,3 +245,6 @@ ModelManifest.xml
# Test documents
TestDocs/
# Superpowers specs and plans
docs/superpowers/
+20 -6
View File
@@ -7,26 +7,40 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportDXF", "ExportDXF\Expo
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLines\EtchBendLines\EtchBendLines.csproj", "{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "netDxf", "EtchBendLines\netDxf\netDxf\netDxf.csproj", "{785380E0-CEB9-4C34-82E5-60D0E33E848E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x64.ActiveCfg = Debug|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x64.Build.0 = Debug|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x86.ActiveCfg = Debug|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|x86.Build.0 = Debug|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|Any CPU.Build.0 = Release|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x64.ActiveCfg = Release|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x64.Build.0 = Release|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x86.ActiveCfg = Release|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|x86.Build.0 = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x64.ActiveCfg = Debug|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x64.Build.0 = Debug|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x86.ActiveCfg = Debug|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|x86.Build.0 = Debug|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|Any CPU.Build.0 = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.Build.0 = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.ActiveCfg = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.Build.0 = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.ActiveCfg = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
-61
View File
@@ -1,61 +0,0 @@
using ExportDXF.Models;
using Microsoft.EntityFrameworkCore;
using System.Configuration;
namespace ExportDXF.Data
{
public class ExportDxfDbContext : DbContext
{
public DbSet<ExportRecord> ExportRecords { get; set; }
public DbSet<BomItem> BomItems { get; set; }
public ExportDxfDbContext() : base()
{
}
public ExportDxfDbContext(DbContextOptions<ExportDxfDbContext> options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
var connectionString = ConfigurationManager.ConnectionStrings["ExportDxfDb"]?.ConnectionString
?? "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;";
optionsBuilder.UseSqlServer(connectionString);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ExportRecord>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.DrawingNumber).HasMaxLength(100);
entity.Property(e => e.SourceFilePath).HasMaxLength(500);
entity.Property(e => e.OutputFolder).HasMaxLength(500);
entity.Property(e => e.ExportedBy).HasMaxLength(100);
entity.HasMany(e => e.BomItems)
.WithOne(b => b.ExportRecord)
.HasForeignKey(b => b.ExportRecordId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<BomItem>(entity =>
{
entity.HasKey(e => e.ID);
entity.Property(e => e.ItemNo).HasMaxLength(50);
entity.Property(e => e.PartNo).HasMaxLength(100);
entity.Property(e => e.Description).HasMaxLength(500);
entity.Property(e => e.PartName).HasMaxLength(200);
entity.Property(e => e.ConfigurationName).HasMaxLength(100);
entity.Property(e => e.Material).HasMaxLength(100);
entity.Property(e => e.CutTemplateName).HasMaxLength(100);
});
}
}
}
+19 -2
View File
@@ -1,10 +1,11 @@
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
namespace ExportDXF
{
public class DrawingInfo
{
private static Regex drawingFormatRegex = new Regex(@"(?<equipmentNo>[345]\d{3}(-\d+\w{1,2})?)\s?(?<dwgNo>[ABEP]\d+(-?(\d+[A-Z]?))?)", RegexOptions.IgnoreCase);
private static Regex equipmentOnlyRegex = new Regex(@"^(?<equipmentNo>[345]\d{3}(-\d+\w{1,2})?)\b", RegexOptions.IgnoreCase);
public string EquipmentNo { get; set; }
@@ -14,6 +15,8 @@ namespace ExportDXF
public override string ToString()
{
if (string.IsNullOrEmpty(DrawingNo))
return EquipmentNo ?? string.Empty;
return $"{EquipmentNo} {DrawingNo}";
}
@@ -35,7 +38,21 @@ namespace ExportDXF
var match = drawingFormatRegex.Match(input);
if (match.Success == false)
{
// Try matching just the equipment number (e.g. "5028 Prox switch bracket")
var eqMatch = equipmentOnlyRegex.Match(input);
if (eqMatch.Success)
{
return new DrawingInfo
{
EquipmentNo = eqMatch.Groups["equipmentNo"].Value,
DrawingNo = null,
Source = input
};
}
return null;
}
var dwg = new DrawingInfo();
@@ -46,4 +63,4 @@ namespace ExportDXF
return dwg;
}
}
}
}
+1 -5
View File
@@ -14,11 +14,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ClosedXML" Version="0.104.2" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
</ItemGroup>
+82 -76
View File
@@ -36,151 +36,155 @@ namespace ExportDXF.Forms
logEventsDataGrid = new System.Windows.Forms.DataGridView();
bomTab = new System.Windows.Forms.TabPage();
bomDataGrid = new System.Windows.Forms.DataGridView();
equipmentBox = new System.Windows.Forms.ComboBox();
label1 = new System.Windows.Forms.Label();
label2 = new System.Windows.Forms.Label();
drawingNoBox = new System.Windows.Forms.ComboBox();
cutTemplatesTab = new System.Windows.Forms.TabPage();
cutTemplatesDataGrid = new System.Windows.Forms.DataGridView();
templateLabel = new System.Windows.Forms.Label();
txtFilenameTemplate = new System.Windows.Forms.TextBox();
mainTabControl.SuspendLayout();
logEventsTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit();
bomTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)bomDataGrid).BeginInit();
cutTemplatesTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)cutTemplatesDataGrid).BeginInit();
SuspendLayout();
//
//
// runButton
//
//
runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
runButton.Location = new System.Drawing.Point(656, 13);
runButton.Location = new System.Drawing.Point(508, 12);
runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
runButton.Name = "runButton";
runButton.Size = new System.Drawing.Size(100, 30);
runButton.Size = new System.Drawing.Size(65, 57);
runButton.TabIndex = 11;
runButton.Text = "Start";
runButton.UseVisualStyleBackColor = true;
runButton.Click += button1_Click;
//
//
// templateLabel
//
templateLabel.AutoSize = true;
templateLabel.Location = new System.Drawing.Point(15, 15);
templateLabel.Name = "templateLabel";
templateLabel.Size = new System.Drawing.Size(116, 17);
templateLabel.TabIndex = 2;
templateLabel.Text = "Filename Template";
//
// txtFilenameTemplate
//
txtFilenameTemplate.Location = new System.Drawing.Point(137, 12);
txtFilenameTemplate.Name = "txtFilenameTemplate";
txtFilenameTemplate.Size = new System.Drawing.Size(365, 25);
txtFilenameTemplate.TabIndex = 1;
//
// label3
//
//
label3.AutoSize = true;
label3.Location = new System.Drawing.Point(26, 46);
label3.Name = "label3";
label3.Size = new System.Drawing.Size(105, 17);
label3.TabIndex = 2;
label3.Text = "View flip decider";
//
//
// viewFlipDeciderBox
//
//
viewFlipDeciderBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
viewFlipDeciderBox.FormattingEnabled = true;
viewFlipDeciderBox.Location = new System.Drawing.Point(137, 43);
viewFlipDeciderBox.Name = "viewFlipDeciderBox";
viewFlipDeciderBox.Size = new System.Drawing.Size(502, 25);
viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25);
viewFlipDeciderBox.TabIndex = 3;
//
//
// mainTabControl
//
//
mainTabControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
mainTabControl.Controls.Add(logEventsTab);
mainTabControl.Controls.Add(bomTab);
mainTabControl.Location = new System.Drawing.Point(15, 74);
mainTabControl.Controls.Add(cutTemplatesTab);
mainTabControl.Location = new System.Drawing.Point(15, 75);
mainTabControl.Name = "mainTabControl";
mainTabControl.Padding = new System.Drawing.Point(20, 5);
mainTabControl.SelectedIndex = 0;
mainTabControl.Size = new System.Drawing.Size(741, 586);
mainTabControl.Size = new System.Drawing.Size(910, 522);
mainTabControl.TabIndex = 12;
//
//
// logEventsTab
//
//
logEventsTab.Controls.Add(logEventsDataGrid);
logEventsTab.Location = new System.Drawing.Point(4, 30);
logEventsTab.Name = "logEventsTab";
logEventsTab.Padding = new System.Windows.Forms.Padding(3);
logEventsTab.Size = new System.Drawing.Size(733, 552);
logEventsTab.Size = new System.Drawing.Size(902, 488);
logEventsTab.TabIndex = 0;
logEventsTab.Text = "Log Events";
logEventsTab.UseVisualStyleBackColor = true;
//
//
// logEventsDataGrid
//
//
logEventsDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
logEventsDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
logEventsDataGrid.Name = "logEventsDataGrid";
logEventsDataGrid.Size = new System.Drawing.Size(721, 540);
logEventsDataGrid.Size = new System.Drawing.Size(890, 476);
logEventsDataGrid.TabIndex = 0;
//
//
// bomTab
//
//
bomTab.Controls.Add(bomDataGrid);
bomTab.Location = new System.Drawing.Point(4, 30);
bomTab.Location = new System.Drawing.Point(4, 28);
bomTab.Name = "bomTab";
bomTab.Padding = new System.Windows.Forms.Padding(3);
bomTab.Size = new System.Drawing.Size(733, 552);
bomTab.Size = new System.Drawing.Size(902, 490);
bomTab.TabIndex = 1;
bomTab.Text = "Bill Of Materials";
bomTab.UseVisualStyleBackColor = true;
//
//
// bomDataGrid
//
//
bomDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
bomDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
bomDataGrid.Location = new System.Drawing.Point(6, 6);
bomDataGrid.Name = "bomDataGrid";
bomDataGrid.Size = new System.Drawing.Size(721, 540);
bomDataGrid.Size = new System.Drawing.Size(890, 478);
bomDataGrid.TabIndex = 1;
//
// equipmentBox
//
equipmentBox.FormattingEnabled = true;
equipmentBox.Location = new System.Drawing.Point(137, 12);
equipmentBox.Name = "equipmentBox";
equipmentBox.Size = new System.Drawing.Size(166, 25);
equipmentBox.TabIndex = 13;
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(61, 15);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(70, 17);
label1.TabIndex = 2;
label1.Text = "Equipment";
//
// label2
//
label2.AutoSize = true;
label2.Location = new System.Drawing.Point(354, 15);
label2.Name = "label2";
label2.Size = new System.Drawing.Size(56, 17);
label2.TabIndex = 2;
label2.Text = "Drawing";
//
// drawingNoBox
//
drawingNoBox.FormattingEnabled = true;
drawingNoBox.Location = new System.Drawing.Point(416, 12);
drawingNoBox.Name = "drawingNoBox";
drawingNoBox.Size = new System.Drawing.Size(223, 25);
drawingNoBox.TabIndex = 13;
//
//
// cutTemplatesTab
//
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
cutTemplatesTab.Location = new System.Drawing.Point(4, 28);
cutTemplatesTab.Name = "cutTemplatesTab";
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
cutTemplatesTab.Size = new System.Drawing.Size(902, 490);
cutTemplatesTab.TabIndex = 2;
cutTemplatesTab.Text = "Cut Templates";
cutTemplatesTab.UseVisualStyleBackColor = true;
//
// cutTemplatesDataGrid
//
cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
cutTemplatesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 478);
cutTemplatesDataGrid.TabIndex = 2;
//
// MainForm
//
//
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
ClientSize = new System.Drawing.Size(768, 672);
Controls.Add(drawingNoBox);
Controls.Add(equipmentBox);
ClientSize = new System.Drawing.Size(937, 609);
Controls.Add(txtFilenameTemplate);
Controls.Add(mainTabControl);
Controls.Add(viewFlipDeciderBox);
Controls.Add(label2);
Controls.Add(label1);
Controls.Add(templateLabel);
Controls.Add(label3);
Controls.Add(runButton);
Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
MaximizeBox = false;
MinimumSize = new System.Drawing.Size(643, 355);
MinimumSize = new System.Drawing.Size(642, 455);
Name = "MainForm";
StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
Text = "ExportDXF";
@@ -189,6 +193,8 @@ namespace ExportDXF.Forms
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).EndInit();
bomTab.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)bomDataGrid).EndInit();
cutTemplatesTab.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)cutTemplatesDataGrid).EndInit();
ResumeLayout(false);
PerformLayout();
}
@@ -203,9 +209,9 @@ namespace ExportDXF.Forms
private System.Windows.Forms.TabPage bomTab;
private System.Windows.Forms.DataGridView logEventsDataGrid;
private System.Windows.Forms.DataGridView bomDataGrid;
private System.Windows.Forms.ComboBox equipmentBox;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ComboBox drawingNoBox;
private System.Windows.Forms.TabPage cutTemplatesTab;
private System.Windows.Forms.DataGridView cutTemplatesDataGrid;
private System.Windows.Forms.Label templateLabel;
private System.Windows.Forms.TextBox txtFilenameTemplate;
}
}
+99 -102
View File
@@ -1,12 +1,11 @@
using ExportDXF.Data;
using ExportDXF.Extensions;
using ExportDXF.Models;
using ExportDXF.Services;
using ExportDXF.ViewFlipDeciders;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -18,14 +17,13 @@ namespace ExportDXF.Forms
{
private readonly ISolidWorksService _solidWorksService;
private readonly IDxfExportService _exportService;
private readonly IFileExportService _fileExportService;
private readonly Func<ExportDxfDbContext> _dbContextFactory;
private readonly IDrawingInfoExtractor[] _extractors;
private CancellationTokenSource _cancellationTokenSource;
private readonly BindingList<LogEvent> _logEvents;
private readonly BindingList<BomItem> _bomItems;
private List<DrawingInfo> _allDrawings;
private readonly BindingList<CutTemplate> _cutTemplates;
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFileExportService fileExportService, Func<ExportDxfDbContext> dbContextFactory = null)
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IDrawingInfoExtractor[] extractors)
{
InitializeComponent();
_solidWorksService = solidWorksService ??
@@ -33,16 +31,15 @@ namespace ExportDXF.Forms
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
_exportService = exportService ??
throw new ArgumentNullException(nameof(exportService));
_fileExportService = fileExportService ??
throw new ArgumentNullException(nameof(fileExportService));
_dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext());
_extractors = extractors ??
throw new ArgumentNullException(nameof(extractors));
_logEvents = new BindingList<LogEvent>();
_bomItems = new BindingList<BomItem>();
_allDrawings = new List<DrawingInfo>();
_cutTemplates = new BindingList<CutTemplate>();
InitializeViewFlipDeciders();
InitializeLogEventsGrid();
InitializeBomGrid();
InitializeDrawingDropdowns();
InitializeCutTemplatesGrid();
}
~MainForm()
@@ -67,7 +64,6 @@ namespace ExportDXF.Forms
LogMessage("Connecting to SolidWorks, this may take a minute...");
await _solidWorksService.ConnectAsync();
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
LogMessage($"Output folder: {_fileExportService.OutputFolder}");
LogMessage("Ready");
UpdateActiveDocumentDisplay();
runButton.Enabled = true;
@@ -89,7 +85,7 @@ namespace ExportDXF.Forms
ViewFlipDecider = d
})
.ToList();
// Move "Automatic" to the top if it exists
var automatic = items.FirstOrDefault(i => i.Name == "Automatic");
if (automatic != null)
{
@@ -102,17 +98,13 @@ namespace ExportDXF.Forms
private void InitializeLogEventsGrid()
{
// Clear any existing columns first
logEventsDataGrid.Columns.Clear();
// Configure grid settings
logEventsDataGrid.AutoGenerateColumns = false;
logEventsDataGrid.AllowUserToAddRows = false;
logEventsDataGrid.AllowUserToDeleteRows = false;
logEventsDataGrid.ReadOnly = true;
logEventsDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
// Add columns
logEventsDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(LogEvent.Time),
@@ -142,26 +134,19 @@ namespace ExportDXF.Forms
AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
});
// Add row coloring based on log level
logEventsDataGrid.CellFormatting += LogEventsDataGrid_CellFormatting;
// Set the data source AFTER adding columns
logEventsDataGrid.DataSource = _logEvents;
}
private void InitializeBomGrid()
{
// Clear any existing columns first
bomDataGrid.Columns.Clear();
// Configure grid settings
bomDataGrid.AutoGenerateColumns = false;
bomDataGrid.AllowUserToAddRows = false;
bomDataGrid.AllowUserToDeleteRows = false;
bomDataGrid.ReadOnly = true;
bomDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
// Add columns
bomDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(BomItem.ItemNo),
@@ -211,85 +196,68 @@ namespace ExportDXF.Forms
Width = 120
});
// Set the data source AFTER adding columns
bomDataGrid.DataSource = _bomItems;
}
private void InitializeDrawingDropdowns()
private void InitializeCutTemplatesGrid()
{
try
cutTemplatesDataGrid.Columns.Clear();
cutTemplatesDataGrid.AutoGenerateColumns = false;
cutTemplatesDataGrid.AllowUserToAddRows = false;
cutTemplatesDataGrid.AllowUserToDeleteRows = false;
cutTemplatesDataGrid.ReadOnly = true;
cutTemplatesDataGrid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
using (var db = _dbContextFactory())
{
// Get all drawing numbers from the database
var drawingNumbers = db.ExportRecords
.Select(r => r.DrawingNumber)
.Where(d => !string.IsNullOrEmpty(d))
.Distinct()
.ToList();
DataPropertyName = nameof(CutTemplate.CutTemplateName),
HeaderText = "Template Name",
Width = 150
});
// Parse into DrawingInfo objects
_allDrawings = drawingNumbers
.Select(DrawingInfo.Parse)
.Where(d => d != null)
.Distinct()
.OrderBy(d => d.EquipmentNo)
.ThenBy(d => d.DrawingNo)
.ToList();
// Get distinct equipment numbers
var equipmentNumbers = _allDrawings
.Select(d => d.EquipmentNo)
.Distinct()
.OrderBy(e => e)
.ToList();
// Populate equipment dropdown
equipmentBox.Items.Clear();
equipmentBox.Items.Add(""); // Empty option for "all"
foreach (var eq in equipmentNumbers)
{
equipmentBox.Items.Add(eq);
}
// Populate drawing dropdown with all drawings initially
UpdateDrawingDropdown();
// Wire up event handler for equipment selection change
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
}
}
catch (Exception ex)
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
// Database might not exist yet - that's OK
System.Diagnostics.Debug.WriteLine($"Failed to load drawings from database: {ex.Message}");
}
}
DataPropertyName = nameof(CutTemplate.DxfFilePath),
HeaderText = "DXF File",
Width = 250
});
private void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e)
{
UpdateDrawingDropdown();
}
private void UpdateDrawingDropdown()
{
var selectedEquipment = equipmentBox.SelectedItem?.ToString();
var filteredDrawings = string.IsNullOrEmpty(selectedEquipment)
? _allDrawings
: _allDrawings.Where(d => d.EquipmentNo == selectedEquipment).ToList();
drawingNoBox.Items.Clear();
drawingNoBox.Items.Add(""); // Empty option
foreach (var drawing in filteredDrawings)
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
drawingNoBox.Items.Add(drawing.DrawingNo);
}
DataPropertyName = nameof(CutTemplate.Revision),
HeaderText = "Rev",
Width = 50
});
if (drawingNoBox.Items.Count > 0)
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
drawingNoBox.SelectedIndex = 0;
}
DataPropertyName = nameof(CutTemplate.Thickness),
HeaderText = "Thickness",
Width = 80
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.KFactor),
HeaderText = "K-Factor",
Width = 80
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.DefaultBendRadius),
HeaderText = "Bend Radius",
Width = 90
});
cutTemplatesDataGrid.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = nameof(CutTemplate.ContentHash),
HeaderText = "Content Hash",
Width = 150
});
cutTemplatesDataGrid.DataSource = _cutTemplates;
}
private async void button1_Click(object sender, EventArgs e)
@@ -308,6 +276,13 @@ namespace ExportDXF.Forms
{
try
{
var template = txtFilenameTemplate.Text.Trim();
if (!FilenameTemplateParser.Validate(template, out var error))
{
MessageBox.Show(error, "Invalid Template", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
_cancellationTokenSource = new CancellationTokenSource();
var token = _cancellationTokenSource.Token;
UpdateUIForExportStart();
@@ -319,29 +294,31 @@ namespace ExportDXF.Forms
return;
}
// Parse drawing number from active document title
var drawingInfo = DrawingInfo.Parse(activeDoc.Title);
var filePrefix = drawingInfo != null ? $"{drawingInfo.EquipmentNo} {drawingInfo.DrawingNo}" : activeDoc.Title;
var sourceDir = Path.GetDirectoryName(activeDoc.FilePath);
var outputFolder = Path.Combine(sourceDir, "Templates");
var viewFlipDecider = GetSelectedViewFlipDecider();
var exportContext = new ExportContext
{
ActiveDocument = activeDoc,
ViewFlipDecider = viewFlipDecider,
FilePrefix = filePrefix,
EquipmentId = null,
FilenameTemplate = template,
OutputFolder = outputFolder,
CancellationToken = token,
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
BomItemCallback = AddBomItem
};
// Clear previous BOM items
_bomItems.Clear();
_cutTemplates.Clear();
LogMessage($"Started at {DateTime.Now:t}");
LogMessage($"Exporting to: {_fileExportService.OutputFolder}");
LogMessage($"Output: {outputFolder}");
await Task.Run(() => _exportService.Export(exportContext), token);
_solidWorksService.SetCommandInProgress(true);
await Task.Run(async () => await _exportService.ExportAsync(exportContext), token);
LogMessage("Done.");
}
@@ -356,6 +333,7 @@ namespace ExportDXF.Forms
}
finally
{
_solidWorksService.SetCommandInProgress(false);
UpdateUIForExportComplete();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
@@ -377,12 +355,14 @@ namespace ExportDXF.Forms
private void UpdateUIForExportStart()
{
viewFlipDeciderBox.Enabled = false;
txtFilenameTemplate.Enabled = false;
runButton.Text = "Stop";
}
private void UpdateUIForExportComplete()
{
viewFlipDeciderBox.Enabled = true;
txtFilenameTemplate.Enabled = true;
runButton.Text = "Start";
runButton.Enabled = true;
}
@@ -391,7 +371,7 @@ namespace ExportDXF.Forms
{
if (InvokeRequired)
{
Invoke(new Action(() => OnActiveDocumentChanged(sender, e)));
Invoke(new Action(() => UpdateActiveDocumentDisplay()));
return;
}
UpdateActiveDocumentDisplay();
@@ -402,6 +382,19 @@ namespace ExportDXF.Forms
var activeDoc = _solidWorksService.GetActiveDocument();
var docTitle = activeDoc?.Title ?? "No Document Open";
this.Text = $"ExportDXF - {docTitle}";
if (activeDoc == null)
return;
// Try each extractor to auto-fill the template
foreach (var extractor in _extractors)
{
if (extractor.TryExtract(activeDoc.Title, out var info))
{
txtFilenameTemplate.Text = info.DefaultTemplate;
return;
}
}
}
private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null)
@@ -433,7 +426,6 @@ namespace ExportDXF.Forms
_logEvents.Add(logEvent);
// Auto-scroll to the last row
if (logEventsDataGrid.Rows.Count > 0)
{
logEventsDataGrid.FirstDisplayedScrollingRowIndex = logEventsDataGrid.Rows.Count - 1;
@@ -448,6 +440,11 @@ namespace ExportDXF.Forms
return;
}
_bomItems.Add(item);
if (item.CutTemplate != null)
{
_cutTemplates.Add(item.CutTemplate);
}
}
private void LogEventsDataGrid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
+1 -26
View File
@@ -1,10 +1,7 @@
using System;
namespace ExportDXF.Models
{
public class BomItem
{
public int ID { get; set; }
public string ItemNo { get; set; } = "";
public string PartNo { get; set; } = "";
public int SortOrder { get; set; }
@@ -14,29 +11,7 @@ namespace ExportDXF.Models
public string PartName { get; set; } = "";
public string ConfigurationName { get; set; } = "";
public string Material { get; set; } = "";
public string CutTemplateName { get; set; } = "";
public string DxfFilePath { get; set; } = "";
// Sheet metal properties
private double? _thickness;
public double? Thickness
{
get => _thickness;
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public double? KFactor { get; set; }
private double? _defaultBendRadius;
public double? DefaultBendRadius
{
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
// EF Core relationship to ExportRecord
public int ExportRecordId { get; set; }
public virtual ExportRecord ExportRecord { get; set; }
public CutTemplate CutTemplate { get; set; }
}
public struct Size
+28
View File
@@ -0,0 +1,28 @@
using System;
namespace ExportDXF.Models
{
public class CutTemplate
{
public string DxfFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string CutTemplateName { get; set; } = "";
public int Revision { get; set; } = 1;
private double? _thickness;
public double? Thickness
{
get => _thickness;
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public double? KFactor { get; set; }
private double? _defaultBendRadius;
public double? DefaultBendRadius
{
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
}
}
+7 -13
View File
@@ -1,4 +1,4 @@
using ExportDXF.Models;
using ExportDXF.Models;
using ExportDXF.ViewFlipDeciders;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
@@ -28,14 +28,14 @@ namespace ExportDXF.Services
public IViewFlipDecider ViewFlipDecider { get; set; }
/// <summary>
/// Prefix to prepend to exported filenames.
/// Filename template with placeholders (e.g., "4321 A01 PT{item_no:2}").
/// </summary>
public string FilePrefix { get; set; }
public string FilenameTemplate { get; set; }
/// <summary>
/// Selected Equipment ID for API operations (optional).
/// Output folder for DXF files and Excel workbook.
/// </summary>
public int? EquipmentId { get; set; }
public string OutputFolder { get; set; }
/// <summary>
/// Cancellation token for canceling the export operation.
@@ -67,8 +67,8 @@ namespace ExportDXF.Services
get
{
return Path.Combine(
Application.StartupPath,
DRAWING_TEMPLATE_FOLDER,
Application.StartupPath,
DRAWING_TEMPLATE_FOLDER,
DRAWING_TEMPLATE_FILE);
}
}
@@ -108,23 +108,17 @@ namespace ExportDXF.Services
if (!string.IsNullOrEmpty(title))
{
// Close the document without saving
SolidWorksApp.CloseDoc(title);
ProgressCallback?.Invoke("Closed template drawing", LogLevel.Info, null);
}
}
// Clear the reference regardless of success/failure
TemplateDrawing = null;
}
catch (Exception ex)
{
ProgressCallback?.Invoke($"Failed to close template drawing: {ex.Message}", LogLevel.Error, null);
// Still clear the reference to prevent further issues
TemplateDrawing = null;
// Don't throw here as this is cleanup code - log the error but continue
}
}
-17
View File
@@ -1,17 +0,0 @@
using System;
using System.Collections.Generic;
namespace ExportDXF.Models
{
public class ExportRecord
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
}
}
+11
View File
@@ -61,5 +61,16 @@ namespace ExportDXF.Services
/// The SolidWorks component reference.
/// </summary>
public Component2 Component { get; set; }
/// <summary>
/// SHA256 content hash of the exported DXF (transient, not persisted).
/// </summary>
public string ContentHash { get; set; }
/// <summary>
/// Full path to the locally-exported DXF temp file (transient, not persisted).
/// Set after successful export; used for upload to the API.
/// </summary>
public string LocalTempPath { get; set; }
}
}
+11 -17
View File
@@ -1,16 +1,12 @@
using ExportDXF.Forms;
using ExportDXF.Services;
using System;
using System.Configuration;
using System.Windows.Forms;
namespace ExportDXF
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
@@ -23,34 +19,32 @@ namespace ExportDXF
}
}
/// <summary>
/// Simple dependency injection container.
/// </summary>
public class ServiceContainer
{
private readonly string _outputFolder;
public ServiceContainer()
{
_outputFolder = ConfigurationManager.AppSettings["ExportOutputFolder"] ?? @"C:\ExportDXF\Output";
}
public MainForm ResolveMainForm()
{
var solidWorksService = new SolidWorksService();
var bomExtractor = new BomExtractor();
var partExporter = new PartExporter();
var drawingExporter = new DrawingExporter();
var fileExportService = new FileExportService(_outputFolder);
var excelExportService = new ExcelExportService();
var logFileService = new LogFileService();
var exportService = new DxfExportService(
solidWorksService,
bomExtractor,
partExporter,
drawingExporter,
fileExportService);
excelExportService,
logFileService);
return new MainForm(solidWorksService, exportService, fileExportService);
var extractors = new IDrawingInfoExtractor[]
{
new EquipmentDrawingInfoExtractor(),
new DefaultDrawingInfoExtractor()
};
return new MainForm(solidWorksService, exportService, extractors);
}
}
}
@@ -0,0 +1,28 @@
using System.Configuration;
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Fallback extractor that uses the document name as prefix
/// and appends the configured default suffix.
/// Always returns true — this is the catch-all.
/// </summary>
public class DefaultDrawingInfoExtractor : IDrawingInfoExtractor
{
public bool TryExtract(string documentName, out DrawingInfoResult info)
{
var name = Path.GetFileNameWithoutExtension(documentName);
var suffix = ConfigurationManager.AppSettings["DefaultSuffix"] ?? "PT{item_no:2}";
info = new DrawingInfoResult
{
EquipmentNumber = null,
DrawingNumber = null,
DefaultTemplate = $"{name} {suffix}"
};
return true;
}
}
}
+255 -146
View File
@@ -1,56 +1,47 @@
using ExportDXF.Data;
using ExportDXF.Extensions;
using ExportDXF.ItemExtractors;
using ExportDXF.Models;
using ExportDXF;
using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace ExportDXF.Services
{
public interface IDxfExportService
{
/// <summary>
/// Exports the document specified in the context to DXF format.
/// </summary>
/// <param name="context">The export context containing all necessary information.</param>
void Export(ExportContext context);
Task ExportAsync(ExportContext context);
}
/// <summary>
/// Service responsible for orchestrating the export of SolidWorks documents to DXF format.
/// </summary>
public class DxfExportService : IDxfExportService
{
private readonly ISolidWorksService _solidWorksService;
private readonly IBomExtractor _bomExtractor;
private readonly IPartExporter _partExporter;
private readonly IDrawingExporter _drawingExporter;
private readonly IFileExportService _fileExportService;
private readonly Func<ExportDxfDbContext> _dbContextFactory;
private readonly ExcelExportService _excelExportService;
private readonly LogFileService _logFileService;
public DxfExportService(
ISolidWorksService solidWorksService,
IBomExtractor bomExtractor,
IPartExporter partExporter,
IDrawingExporter drawingExporter,
IFileExportService fileExportService,
Func<ExportDxfDbContext> dbContextFactory = null)
ExcelExportService excelExportService,
LogFileService logFileService)
{
_solidWorksService = solidWorksService ?? throw new ArgumentNullException(nameof(solidWorksService));
_bomExtractor = bomExtractor ?? throw new ArgumentNullException(nameof(bomExtractor));
_partExporter = partExporter ?? throw new ArgumentNullException(nameof(partExporter));
_drawingExporter = drawingExporter ?? throw new ArgumentNullException(nameof(drawingExporter));
_fileExportService = fileExportService ?? throw new ArgumentNullException(nameof(fileExportService));
_dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext());
_excelExportService = excelExportService ?? throw new ArgumentNullException(nameof(excelExportService));
_logFileService = logFileService ?? throw new ArgumentNullException(nameof(logFileService));
}
/// <summary>
/// Exports the document specified in the context to DXF format.
/// </summary>
public void Export(ExportContext context)
public async Task ExportAsync(ExportContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
@@ -58,7 +49,30 @@ namespace ExportDXF.Services
ValidateContext(context);
SetupExportContext(context);
var outputFolder = context.OutputFolder;
if (!Directory.Exists(outputFolder))
Directory.CreateDirectory(outputFolder);
var prefix = FilenameTemplateParser.GetPrefix(
context.FilenameTemplate,
context.ActiveDocument.Title);
var xlsxPath = Path.Combine(outputFolder, $"{prefix}.xlsx");
var logPath = Path.Combine(outputFolder, $"{prefix}.log");
_logFileService.StartExportLog(logPath);
_logFileService.LogInfo($"Export started: {context.ActiveDocument.FilePath}");
_logFileService.LogInfo($"Template: {context.FilenameTemplate}");
_logFileService.LogInfo($"Output: {outputFolder}");
// Read existing cut templates for revision comparison
var existingTemplates = _excelExportService.ReadExistingCutTemplates(xlsxPath);
var bomItems = new List<BomItem>();
List<Dictionary<string, string>> rawBomTable = null;
var startTime = DateTime.Now;
var tempDir = CreateTempWorkDir();
try
{
@@ -67,35 +81,57 @@ namespace ExportDXF.Services
switch (context.ActiveDocument.DocumentType)
{
case DocumentType.Part:
ExportPart(context);
await ExportPartAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
break;
case DocumentType.Assembly:
ExportAssembly(context);
await ExportAssemblyAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
break;
case DocumentType.Drawing:
ExportDrawing(context);
rawBomTable = await ExportDrawingAsync(context, tempDir, outputFolder, existingTemplates, bomItems);
break;
default:
LogProgress(context, "Unknown document type.", LogLevel.Error);
break;
}
// Write Excel file
_excelExportService.Write(xlsxPath, rawBomTable, bomItems);
_logFileService.LogInfo($"Wrote {Path.GetFileName(xlsxPath)}");
LogProgress(context, $"Saved {Path.GetFileName(xlsxPath)}");
}
catch (OperationCanceledException)
{
_logFileService.LogWarning("Export cancelled by user");
throw;
}
catch (Exception ex)
{
_logFileService.LogError($"Export failed: {ex.Message}");
throw;
}
finally
{
CleanupExportContext(context);
_solidWorksService.EnableUserControl(true);
CleanupTempDir(tempDir);
var duration = DateTime.Now - startTime;
_logFileService.LogInfo($"Run time: {duration.ToReadableFormat()}");
LogProgress(context, $"Run time: {duration.ToReadableFormat()}");
}
}
#region Export Methods by Document Type
private void ExportPart(ExportContext context)
private async Task ExportPartAsync(
ExportContext context,
string tempDir,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
LogProgress(context, "Active document is a Part");
@@ -106,11 +142,25 @@ namespace ExportDXF.Services
return;
}
// Export directly to the output folder
_partExporter.ExportSinglePart(part, _fileExportService.OutputFolder, context);
var item = _partExporter.ExportSinglePart(part, tempDir, context);
if (item != null)
{
item.ItemNo = "1";
var bomItem = CreateBomItem(item);
PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem);
bomItems.Add(bomItem);
context.BomItemCallback?.Invoke(bomItem);
}
}
private void ExportAssembly(ExportContext context)
private async Task ExportAssemblyAsync(
ExportContext context,
string tempDir,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
LogProgress(context, "Active document is an Assembly");
LogProgress(context, "Fetching components...");
@@ -132,11 +182,26 @@ namespace ExportDXF.Services
LogProgress(context, $"Found {items.Count} item(s).");
// Export directly to the output folder
ExportItems(items, _fileExportService.OutputFolder, context);
// Assign item numbers
int nextNum = 1;
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.ItemNo))
{
item.ItemNo = nextNum.ToString();
nextNum++;
}
}
await ExportItemsAsync(items, tempDir, outputFolder, context, existingTemplates, bomItems);
}
private void ExportDrawing(ExportContext context)
private async Task<List<Dictionary<string, string>>> ExportDrawingAsync(
ExportContext context,
string tempDir,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
LogProgress(context, "Active document is a Drawing");
LogProgress(context, "Finding BOM tables...");
@@ -145,68 +210,35 @@ namespace ExportDXF.Services
if (drawing == null)
{
LogProgress(context, "Failed to get drawing document.", LogLevel.Error);
return;
return null;
}
// Read raw BOM table for Excel output
var rawBomTable = new List<Dictionary<string, string>>();
var bomTables = drawing.GetBomTables();
foreach (var table in bomTables)
{
var rows = RawBomTableReader.Read(table);
rawBomTable.AddRange(rows);
}
// Extract items for DXF export
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
if (items == null || items.Count == 0)
{
LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error);
return;
return rawBomTable;
}
LogProgress(context, $"Found {items.Count} component(s)");
// Determine drawing number for file naming
var drawingNumber = ParseDrawingNumber(context);
// Export drawing to PDF in output folder
_drawingExporter.ExportToPdf(drawing, outputFolder, context);
// Export drawing to PDF
var tempDir = CreateTempWorkDir();
_drawingExporter.ExportToPdf(drawing, tempDir, context);
await ExportItemsAsync(items, tempDir, outputFolder, context, existingTemplates, bomItems);
// Copy PDF to output folder
try
{
var pdfs = Directory.GetFiles(tempDir, "*.pdf");
if (pdfs.Length > 0)
{
var savedPath = _fileExportService.SavePdfFile(pdfs[0], drawingNumber);
LogProgress(context, $"Saved PDF: {Path.GetFileName(savedPath)}", LogLevel.Info);
}
}
catch (Exception ex)
{
LogProgress(context, $"PDF save error: {ex.Message}", LogLevel.Error);
}
// Create export record in database
ExportRecord exportRecord = null;
try
{
using (var db = _dbContextFactory())
{
db.Database.EnsureCreated();
exportRecord = new ExportRecord
{
DrawingNumber = drawingNumber ?? context.ActiveDocument.Title,
SourceFilePath = context.ActiveDocument.FilePath,
OutputFolder = _fileExportService.OutputFolder,
ExportedAt = DateTime.Now,
ExportedBy = System.Environment.UserName
};
db.ExportRecords.Add(exportRecord);
db.SaveChanges();
LogProgress(context, $"Created export record (ID: {exportRecord.Id})", LogLevel.Info);
}
}
catch (Exception ex)
{
LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error);
}
// Export parts to DXF (directly to output folder) and save BOM items
ExportItems(items, _fileExportService.OutputFolder, context, exportRecord?.Id);
return rawBomTable;
}
#endregion
@@ -215,17 +247,12 @@ namespace ExportDXF.Services
private void SetupExportContext(ExportContext context)
{
// Set up SolidWorks application reference
context.SolidWorksApp = _solidWorksService.GetNativeSldWorks();
if (context.SolidWorksApp == null)
{
throw new InvalidOperationException("SolidWorks service is not connected.");
}
// Set up drawing template path
context.TemplateDrawing = null;
LogProgress(context, "Export context initialized");
}
@@ -233,13 +260,11 @@ namespace ExportDXF.Services
{
try
{
// Clean up template drawing if it was created
context.CleanupTemplateDrawing();
}
catch (Exception ex)
{
LogProgress(context, $"Warning: Failed to cleanup template drawing: {ex.Message}", LogLevel.Warning);
// Don't throw - this is cleanup code
}
}
@@ -255,7 +280,6 @@ namespace ExportDXF.Services
{
TopLevelOnly = false
};
return extractor.GetItems();
}
catch (Exception ex)
@@ -265,9 +289,17 @@ namespace ExportDXF.Services
}
}
private void ExportItems(List<Item> items, string saveDirectory, ExportContext context, int? exportRecordId = null)
private async Task ExportItemsAsync(
List<Item> items,
string tempDir,
string outputFolder,
ExportContext context,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
List<BomItem> bomItems)
{
int successCount = 0;
int skippedCount = 0;
int unchangedCount = 0;
int failureCount = 0;
int sortOrder = 0;
@@ -281,79 +313,150 @@ namespace ExportDXF.Services
try
{
// PartExporter will handle template drawing creation through context
_partExporter.ExportItem(item, saveDirectory, context);
_partExporter.ExportItem(item, tempDir, context);
if (!string.IsNullOrEmpty(item.FileName))
var bomItem = CreateBomItem(item);
bomItem.SortOrder = sortOrder++;
if (!string.IsNullOrEmpty(item.LocalTempPath))
{
successCount++;
LogProgress(context, $"Exported: {item.FileName}.dxf", LogLevel.Info);
// Create BOM item
var dxfPath = Path.Combine(saveDirectory, item.FileName + ".dxf");
var bomItem = new BomItem
{
ExportRecordId = exportRecordId ?? 0,
ItemNo = item.ItemNo ?? "",
PartNo = item.FileName ?? item.PartName ?? "",
SortOrder = sortOrder++,
Qty = item.Quantity,
TotalQty = item.Quantity,
Description = item.Description ?? "",
PartName = item.PartName ?? "",
ConfigurationName = item.Configuration ?? "",
Material = item.Material ?? "",
Thickness = item.Thickness > 0 ? item.Thickness : null,
KFactor = item.KFactor > 0 ? item.KFactor : null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null,
DxfFilePath = dxfPath
};
// Add to UI
context.BomItemCallback?.Invoke(bomItem);
// Save BOM item to database if we have an export record
if (exportRecordId.HasValue)
{
try
{
using (var db = _dbContextFactory())
{
db.BomItems.Add(bomItem);
db.SaveChanges();
}
}
catch (Exception dbEx)
{
LogProgress(context, $"Database error saving BOM item: {dbEx.Message}", LogLevel.Error);
}
}
var wasPlaced = PlaceDxfFile(item, context, outputFolder, existingTemplates, bomItem);
if (wasPlaced)
successCount++;
else
unchangedCount++;
}
else
{
failureCount++;
skippedCount++;
}
bomItems.Add(bomItem);
context.BomItemCallback?.Invoke(bomItem);
}
catch (Exception ex)
{
LogProgress(context, $"Error exporting item {item.ItemNo}: {ex.Message}", LogLevel.Error);
_logFileService.LogError($"Item {item.ItemNo}: {ex.Message}");
failureCount++;
}
}
LogProgress(context, $"Export complete: {successCount} succeeded, {failureCount} failed",
failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
var summary = $"Export complete: {successCount} exported";
if (unchangedCount > 0)
summary += $", {unchangedCount} unchanged";
if (skippedCount > 0)
summary += $", {skippedCount} skipped (non-sheet-metal)";
if (failureCount > 0)
summary += $", {failureCount} failed";
if (exportRecordId.HasValue)
LogProgress(context, summary, failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
_logFileService.LogInfo(summary);
}
/// <summary>
/// Places a DXF file in the output folder with revision tracking.
/// Returns true if a new file was written, false if unchanged.
/// </summary>
private bool PlaceDxfFile(
Item item,
ExportContext context,
string outputFolder,
Dictionary<string, (string ContentHash, int Revision, string FileName)> existingTemplates,
BomItem bomItem)
{
var baseName = FilenameTemplateParser.Evaluate(context.FilenameTemplate, item);
var contentHash = item.ContentHash;
int revision = 1;
string dxfFileName;
// Check existing templates for revision comparison
if (existingTemplates.TryGetValue(item.ItemNo, out var existing))
{
LogProgress(context, $"BOM items saved to database (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info);
if (existing.ContentHash == contentHash)
{
// Unchanged — skip file write, keep existing
dxfFileName = existing.FileName;
revision = existing.Revision;
LogProgress(context, $"Unchanged: {dxfFileName}", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Unchanged: {dxfFileName}");
bomItem.CutTemplate = new CutTemplate
{
DxfFilePath = dxfFileName,
ContentHash = contentHash,
Revision = revision,
Thickness = item.Thickness > 0 ? item.Thickness : (double?)null,
KFactor = item.KFactor > 0 ? item.KFactor : (double?)null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null
};
return false;
}
else
{
// Changed — increment revision
revision = existing.Revision + 1;
dxfFileName = GetRevisionFileName(baseName, revision);
LogProgress(context, $"Updated: {dxfFileName} (Rev{revision})", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Updated: {dxfFileName} (was {existing.FileName})");
}
}
else
{
// New item
dxfFileName = $"{baseName}.dxf";
LogProgress(context, $"Exported: {dxfFileName}", LogLevel.Info, item.PartName);
_logFileService.LogInfo($"Exported: {dxfFileName}");
}
// Copy from temp to output
var destPath = Path.Combine(outputFolder, dxfFileName);
File.Copy(item.LocalTempPath, destPath, overwrite: true);
bomItem.CutTemplate = new CutTemplate
{
DxfFilePath = Path.GetFileNameWithoutExtension(dxfFileName),
ContentHash = contentHash,
Revision = revision,
Thickness = item.Thickness > 0 ? item.Thickness : (double?)null,
KFactor = item.KFactor > 0 ? item.KFactor : (double?)null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : (double?)null
};
return true;
}
private BomItem CreateBomItem(Item item)
{
return new BomItem
{
ItemNo = item.ItemNo ?? "",
PartNo = item.FileName ?? item.PartName ?? "",
SortOrder = 0,
Qty = item.Quantity,
TotalQty = item.Quantity,
Description = item.Description ?? "",
PartName = item.PartName ?? "",
ConfigurationName = item.Configuration ?? "",
Material = item.Material ?? ""
};
}
#endregion
#region Helper Methods
private string GetRevisionFileName(string baseName, int revision)
{
if (revision <= 1)
return $"{baseName}.dxf";
return $"{baseName} Rev{revision}.dxf";
}
private string CreateTempWorkDir()
{
var path = Path.Combine(Path.GetTempPath(), "ExportDXF-" + Guid.NewGuid().ToString("N"));
@@ -361,17 +464,17 @@ namespace ExportDXF.Services
return path;
}
private string ParseDrawingNumber(ExportContext context)
private void CleanupTempDir(string tempDir)
{
// Prefer prefix (e.g., "5007 A02 PT"), fallback to active document title
var candidate = context?.FilePrefix;
var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
if (info == null)
try
{
var title = context?.ActiveDocument?.Title;
info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title);
if (Directory.Exists(tempDir))
Directory.Delete(tempDir, recursive: true);
}
catch
{
// Best-effort cleanup
}
return info != null ? ($"{info.EquipmentNo} {info.DrawingNo}") : null;
}
private void ValidateContext(ExportContext context)
@@ -381,6 +484,12 @@ namespace ExportDXF.Services
if (context.ProgressCallback == null)
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
if (string.IsNullOrWhiteSpace(context.FilenameTemplate))
throw new ArgumentException("FilenameTemplate cannot be null or empty.", nameof(context));
if (string.IsNullOrWhiteSpace(context.OutputFolder))
throw new ArgumentException("OutputFolder cannot be null or empty.", nameof(context));
}
private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null)
@@ -0,0 +1,36 @@
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Extracts equipment/drawing numbers from document names matching the
/// workplace format (e.g., "4321 A01.SLDDRW" → equipment "4321", drawing "A01").
/// Uses the existing DrawingInfo.Parse() logic.
/// </summary>
public class EquipmentDrawingInfoExtractor : IDrawingInfoExtractor
{
public bool TryExtract(string documentName, out DrawingInfoResult info)
{
info = null;
var name = Path.GetFileNameWithoutExtension(documentName);
var parsed = DrawingInfo.Parse(name);
if (parsed == null || string.IsNullOrEmpty(parsed.EquipmentNo))
return false;
var template = !string.IsNullOrEmpty(parsed.DrawingNo)
? $"{parsed.EquipmentNo} {parsed.DrawingNo} PT{{item_no:2}}"
: $"{parsed.EquipmentNo} PT{{item_no:2}}";
info = new DrawingInfoResult
{
EquipmentNumber = parsed.EquipmentNo,
DrawingNumber = parsed.DrawingNo,
DefaultTemplate = template
};
return true;
}
}
}
+139
View File
@@ -0,0 +1,139 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ClosedXML.Excel;
using ExportDXF.Models;
namespace ExportDXF.Services
{
public class ExcelExportService
{
/// <summary>
/// Reads existing Cut Templates from an xlsx file to compare content hashes.
/// Returns empty dictionary if file doesn't exist or has no Cut Templates sheet.
/// Key = Item #, Value = (ContentHash, Revision, FileName)
/// </summary>
public Dictionary<string, (string ContentHash, int Revision, string FileName)> ReadExistingCutTemplates(string xlsxPath)
{
var result = new Dictionary<string, (string, int, string)>();
if (!File.Exists(xlsxPath))
return result;
using (var workbook = new XLWorkbook(xlsxPath))
{
if (!workbook.TryGetWorksheet("Cut Templates", out var ws))
return result;
var lastCol = ws.LastColumnUsed()?.ColumnNumber() ?? 0;
var lastRow = ws.LastRowUsed()?.RowNumber() ?? 1;
if (lastCol == 0 || lastRow <= 1)
return result;
var headers = new Dictionary<string, int>();
for (int col = 1; col <= lastCol; col++)
{
var header = ws.Cell(1, col).GetString();
if (!string.IsNullOrEmpty(header))
headers[header] = col;
}
if (!headers.ContainsKey("Item #") || !headers.ContainsKey("Content Hash"))
return result;
for (int row = 2; row <= lastRow; row++)
{
var itemNo = ws.Cell(row, headers["Item #"]).GetString();
var hash = ws.Cell(row, headers["Content Hash"]).GetString();
var revision = headers.ContainsKey("Revision")
? ws.Cell(row, headers["Revision"]).GetValue<int>()
: 1;
var fileName = headers.ContainsKey("File Name")
? ws.Cell(row, headers["File Name"]).GetString()
: "";
if (!string.IsNullOrEmpty(itemNo))
result[itemNo] = (hash, revision, fileName);
}
}
return result;
}
/// <summary>
/// Writes or updates the xlsx file with BOM and Cut Templates sheets.
/// rawBomTable: list of rows where each row is a dictionary of column name → value.
/// If null or empty, the BOM sheet is not written (Part/Assembly exports).
/// </summary>
public void Write(
string xlsxPath,
List<Dictionary<string, string>> rawBomTable,
List<BomItem> bomItems)
{
using (var workbook = File.Exists(xlsxPath)
? new XLWorkbook(xlsxPath)
: new XLWorkbook())
{
WriteBomSheet(workbook, rawBomTable);
WriteCutTemplatesSheet(workbook, bomItems);
workbook.SaveAs(xlsxPath);
}
}
private void WriteBomSheet(XLWorkbook workbook, List<Dictionary<string, string>> rawBomTable)
{
if (rawBomTable == null || rawBomTable.Count == 0)
return;
if (workbook.TryGetWorksheet("BOM", out _))
workbook.Worksheets.Delete("BOM");
var sheet = workbook.Worksheets.Add("BOM");
var columns = rawBomTable[0].Keys.ToList();
for (int col = 0; col < columns.Count; col++)
sheet.Cell(1, col + 1).Value = columns[col];
for (int row = 0; row < rawBomTable.Count; row++)
{
for (int col = 0; col < columns.Count; col++)
{
string value;
rawBomTable[row].TryGetValue(columns[col], out value);
sheet.Cell(row + 2, col + 1).Value = value ?? "";
}
}
sheet.Columns().AdjustToContents();
}
private void WriteCutTemplatesSheet(XLWorkbook workbook, List<BomItem> bomItems)
{
if (workbook.TryGetWorksheet("Cut Templates", out _))
workbook.Worksheets.Delete("Cut Templates");
var sheet = workbook.Worksheets.Add("Cut Templates");
var headers = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" };
for (int col = 0; col < headers.Length; col++)
sheet.Cell(1, col + 1).Value = headers[col];
int row = 2;
foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo))
{
var ct = item.CutTemplate;
sheet.Cell(row, 1).Value = item.ItemNo;
sheet.Cell(row, 2).Value = ct.DxfFilePath;
sheet.Cell(row, 3).Value = ct.Revision;
sheet.Cell(row, 4).Value = ct.Thickness ?? 0;
sheet.Cell(row, 5).Value = ct.KFactor ?? 0;
sheet.Cell(row, 6).Value = ct.DefaultBendRadius ?? 0;
sheet.Cell(row, 7).Value = ct.ContentHash ?? "";
row++;
}
sheet.Columns().AdjustToContents();
}
}
}
-72
View File
@@ -1,72 +0,0 @@
using System;
using System.IO;
namespace ExportDXF.Services
{
public interface IFileExportService
{
string OutputFolder { get; }
string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo);
string SavePdfFile(string sourcePath, string drawingNumber);
void EnsureOutputFolderExists();
}
public class FileExportService : IFileExportService
{
public string OutputFolder { get; }
public FileExportService(string outputFolder)
{
OutputFolder = outputFolder ?? throw new ArgumentNullException(nameof(outputFolder));
EnsureOutputFolderExists();
}
public void EnsureOutputFolderExists()
{
if (!Directory.Exists(OutputFolder))
{
Directory.CreateDirectory(OutputFolder);
}
}
public string SaveDxfFile(string sourcePath, string drawingNumber, string itemNo)
{
if (string.IsNullOrEmpty(sourcePath))
throw new ArgumentNullException(nameof(sourcePath));
var fileName = !string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo)
? $"{drawingNumber} PT{itemNo}.dxf"
: Path.GetFileName(sourcePath);
var destPath = Path.Combine(OutputFolder, fileName);
// If source and dest are the same, skip copy
if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
{
File.Copy(sourcePath, destPath, overwrite: true);
}
return destPath;
}
public string SavePdfFile(string sourcePath, string drawingNumber)
{
if (string.IsNullOrEmpty(sourcePath))
throw new ArgumentNullException(nameof(sourcePath));
var fileName = !string.IsNullOrEmpty(drawingNumber)
? $"{drawingNumber}.pdf"
: Path.GetFileName(sourcePath);
var destPath = Path.Combine(OutputFolder, fileName);
// If source and dest are the same, skip copy
if (!string.Equals(sourcePath, destPath, StringComparison.OrdinalIgnoreCase))
{
File.Copy(sourcePath, destPath, overwrite: true);
}
return destPath;
}
}
}
@@ -0,0 +1,96 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace ExportDXF.Services
{
public static class FilenameTemplateParser
{
private static readonly Regex PlaceholderPattern = new Regex(
@"\{(?<name>\w+)(?::(?<pad>\d+))?\}",
RegexOptions.Compiled);
/// <summary>
/// Evaluates a template string for a given item.
/// e.g. "4321 A01 PT{item_no:2}" with item_no=3 → "4321 A01 PT03"
/// </summary>
public static string Evaluate(string template, Item item)
{
return PlaceholderPattern.Replace(template, match =>
{
var name = match.Groups["name"].Value.ToLowerInvariant();
var padStr = match.Groups["pad"].Value;
int pad = string.IsNullOrEmpty(padStr) ? 0 : int.Parse(padStr);
string value;
switch (name)
{
case "item_no":
value = item.ItemNo ?? "0";
if (pad > 0)
value = value.PadLeft(pad, '0');
break;
case "part_name":
value = item.PartName ?? "";
break;
case "config":
value = item.Configuration ?? "";
break;
case "material":
value = item.Material ?? "";
break;
default:
value = match.Value;
break;
}
return value;
});
}
/// <summary>
/// Extracts the literal prefix before the first placeholder.
/// Used for naming the xlsx and log files.
/// Falls back to documentName if prefix is empty.
/// </summary>
public static string GetPrefix(string template, string documentName)
{
var match = PlaceholderPattern.Match(template);
if (!match.Success)
return template.Trim();
var prefix = template.Substring(0, match.Index).Trim();
if (string.IsNullOrEmpty(prefix))
return Path.GetFileNameWithoutExtension(documentName);
return prefix;
}
/// <summary>
/// Validates that the template contains {item_no} to prevent filename collisions.
/// </summary>
public static bool Validate(string template, out string error)
{
error = null;
if (string.IsNullOrWhiteSpace(template))
{
error = "Template cannot be empty.";
return false;
}
var hasItemNo = PlaceholderPattern.Matches(template)
.Cast<Match>()
.Any(m => m.Groups["name"].Value.Equals("item_no", StringComparison.OrdinalIgnoreCase));
if (!hasItemNo)
{
error = "Template must contain {item_no} or {item_no:N} to avoid filename collisions.";
return false;
}
return true;
}
}
}
@@ -0,0 +1,14 @@
namespace ExportDXF.Services
{
public interface IDrawingInfoExtractor
{
bool TryExtract(string documentName, out DrawingInfoResult info);
}
public class DrawingInfoResult
{
public string EquipmentNumber { get; set; }
public string DrawingNumber { get; set; }
public string DefaultTemplate { get; set; }
}
}
+60
View File
@@ -0,0 +1,60 @@
using System;
using System.IO;
namespace ExportDXF.Services
{
public class LogFileService : IDisposable
{
private StreamWriter _exportLog;
private static readonly string AppLogPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ExportDXF", "ExportDXF.log");
public void StartExportLog(string logFilePath)
{
_exportLog?.Dispose();
var dir = Path.GetDirectoryName(logFilePath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
_exportLog = new StreamWriter(logFilePath, append: true);
}
public void Log(string level, string message)
{
var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] {message}";
_exportLog?.WriteLine(line);
_exportLog?.Flush();
WriteAppLog(line);
}
public void LogInfo(string message) => Log("INFO", message);
public void LogWarning(string message) => Log("WARNING", message);
public void LogError(string message) => Log("ERROR", message);
private void WriteAppLog(string line)
{
try
{
var dir = Path.GetDirectoryName(AppLogPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.AppendAllText(AppLogPath, line + Environment.NewLine);
}
catch
{
// Best-effort app log — don't fail exports if log write fails
}
}
public void Dispose()
{
_exportLog?.Dispose();
_exportLog = null;
}
}
}
+78 -21
View File
@@ -1,4 +1,4 @@
using ExportDXF.Extensions;
using ExportDXF.Extensions;
using ExportDXF.Models;
using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks;
@@ -15,24 +15,29 @@ namespace ExportDXF.Services
{
/// <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 directory where the DXF file will be saved.</param>
/// <param name="saveDirectory">The temp directory where the DXF file will be saved.</param>
/// <param name="context">The export context.</param>
void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
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 directory where the DXF file will be saved.</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 void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
public PartExporter()
{
}
public Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
{
if (part == null)
throw new ArgumentNullException(nameof(part));
@@ -49,12 +54,52 @@ namespace ExportDXF.Services
try
{
var fileName = GetSinglePartFileName(model, context.FilePrefix);
var fileName = GetSinglePartFileName(model);
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();
ExportPartToDxf(part, originalConfigName, savePath, context);
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
{
@@ -94,7 +139,7 @@ namespace ExportDXF.Services
EnrichItemWithMetadata(item, model, part);
var fileName = GetItemFileName(item, context.FilePrefix);
var fileName = GetItemFileName(item);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
var templateDrawing = context.GetOrCreateTemplateDrawing();
@@ -102,6 +147,8 @@ namespace ExportDXF.Services
if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, savePath, context))
{
item.FileName = Path.GetFileNameWithoutExtension(savePath);
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
item.LocalTempPath = savePath;
}
else
{
@@ -260,6 +307,7 @@ namespace ExportDXF.Services
{
var etcher = new EtchBendLines.Etcher();
etcher.AddEtchLines(dxfPath);
FixDegreeSymbol(dxfPath);
}
catch (Exception)
{
@@ -267,30 +315,39 @@ namespace ExportDXF.Services
}
}
private string GetSinglePartFileName(ModelDoc2 model, string prefix)
/// <summary>
/// Workaround for ACadSharp encoding bug (no upstream fix as of v3.4.9).
/// ACadSharp's DxfReader uses $DWGCODEPAGE (ANSI_1252) to decode text, but
/// AC1018+ DXF files use UTF-8. The degree symbol ° (UTF-8: C2 B0) gets
/// misread as two ANSI_1252 characters: Â (C2) and ° (B0).
/// See: https://github.com/DomCR/ACadSharp/issues?q=encoding
/// </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)
{
var title = model.GetTitle().Replace(".SLDPRT", "");
var config = model.ConfigurationManager.ActiveConfiguration.Name;
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
var name = isDefaultConfig ? title : $"{title} [{config}]";
return prefix + name;
return isDefaultConfig ? title : $"{title} [{config}]";
}
private string GetItemFileName(Item item, string prefix)
private string GetItemFileName(Item item)
{
prefix = prefix?.Replace("\"", "''") ?? string.Empty;
if (string.IsNullOrWhiteSpace(item.ItemNo))
{
return prefix + item.PartName;
}
return item.PartName ?? "unknown";
var num = item.ItemNo.PadLeft(2, '0');
// Expected format: {DrawingNo} PT{ItemNo}
return string.IsNullOrWhiteSpace(prefix)
? $"PT{num}"
: $"{prefix} PT{num}";
return $"PT{num}";
}
private void LogExportFailure(Item item, ExportContext context)
+48
View File
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using SolidWorks.Interop.sldworks;
namespace ExportDXF.Services
{
/// <summary>
/// Reads all visible columns and rows from a SolidWorks BOM table annotation
/// as raw string data for direct copy into an Excel sheet.
/// </summary>
public static class RawBomTableReader
{
public static List<Dictionary<string, string>> Read(BomTableAnnotation bomTable)
{
var table = (TableAnnotation)bomTable;
var rows = new List<Dictionary<string, string>>();
int colCount = table.ColumnCount;
int rowCount = table.RowCount;
// Build visible column headers
var columns = new List<(int Index, string Header)>();
for (int col = 0; col < colCount; col++)
{
if (table.ColumnHidden[col])
continue;
var header = table.get_Text(0, col)?.Trim() ?? $"Column{col}";
columns.Add((col, header));
}
// Read data rows (skip header row 0, skip hidden rows)
for (int row = 1; row < rowCount; row++)
{
if (table.RowHidden[row])
continue;
var rowData = new Dictionary<string, string>();
foreach (var (colIdx, header) in columns)
{
rowData[header] = table.get_Text(row, colIdx)?.Trim() ?? "";
}
rows.Add(rowData);
}
return rows;
}
}
}
+16
View File
@@ -88,6 +88,13 @@ namespace ExportDXF.Services
/// <param name="enable">True to enable user control, false to disable.</param>
void EnableUserControl(bool enable);
/// <summary>
/// Sets whether a command is in progress. When true, user input to
/// SolidWorks is disabled and interactive dialogs are suppressed.
/// </summary>
/// <param name="inProgress">True to block user input, false to re-enable.</param>
void SetCommandInProgress(bool inProgress);
/// <summary>
/// Gets the SolidWorks application instance.
/// </summary>
@@ -188,6 +195,15 @@ namespace ExportDXF.Services
}
}
/// <inheritdoc />
public void SetCommandInProgress(bool inProgress)
{
if (_sldWorks != null)
{
_sldWorks.CommandInProgress = inProgress;
}
}
/// <summary>
/// Gets the native SolidWorks application instance.
/// Use this when you need direct access to the SolidWorks API.
+118
View File
@@ -0,0 +1,118 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace ExportDXF.Utilities
{
public static class ContentHasher
{
/// <summary>
/// Computes a SHA256 hash of DXF file content, skipping the HEADER section
/// which contains timestamps that change on every save.
/// </summary>
public static string ComputeDxfContentHash(string filePath)
{
var text = File.ReadAllText(filePath);
var contentStart = FindEndOfHeader(text);
var content = contentStart >= 0 ? text.Substring(contentStart) : text;
using (var sha = SHA256.Create())
{
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(content));
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
}
/// <summary>
/// Computes a SHA256 hash of the entire file contents (for PDFs and other binary files).
/// </summary>
public static string ComputeFileHash(string filePath)
{
using (var sha = SHA256.Create())
using (var stream = File.OpenRead(filePath))
{
var bytes = sha.ComputeHash(stream);
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
}
/// <summary>
/// Finds the position immediately after the HEADER section's ENDSEC marker.
/// DXF HEADER format:
/// 0\nSECTION\n2\nHEADER\n...variables...\n0\nENDSEC\n
/// Returns -1 if no HEADER section is found.
/// </summary>
private static int FindEndOfHeader(string text)
{
// Find the HEADER section start
var headerIndex = FindGroupCode(text, 0, "2", "HEADER");
if (headerIndex < 0)
return -1;
// Advance past the HEADER value line so pair scanning stays aligned
var headerLineEnd = text.IndexOf('\n', headerIndex);
if (headerLineEnd < 0)
return -1;
// Find the ENDSEC that closes the HEADER section
var pos = headerLineEnd + 1;
while (pos < text.Length)
{
var endsecIndex = FindGroupCode(text, pos, "0", "ENDSEC");
if (endsecIndex < 0)
return -1;
// Move past the ENDSEC line
var lineEnd = text.IndexOf('\n', endsecIndex);
return lineEnd >= 0 ? lineEnd + 1 : text.Length;
}
return -1;
}
/// <summary>
/// Finds a DXF group code pair (code line followed by value line) starting from the given position.
/// Returns the position of the value line, or -1 if not found.
/// </summary>
private static int FindGroupCode(string text, int startIndex, string groupCode, string value)
{
var pos = startIndex;
while (pos < text.Length)
{
// Skip whitespace/newlines to find the group code
while (pos < text.Length && (text[pos] == '\r' || text[pos] == '\n' || text[pos] == ' '))
pos++;
if (pos >= text.Length)
break;
// Read the group code line
var codeLineEnd = text.IndexOf('\n', pos);
if (codeLineEnd < 0)
break;
var codeLine = text.Substring(pos, codeLineEnd - pos).Trim();
// Move to the value line
var valueStart = codeLineEnd + 1;
if (valueStart >= text.Length)
break;
var valueLineEnd = text.IndexOf('\n', valueStart);
if (valueLineEnd < 0)
valueLineEnd = text.Length;
var valueLine = text.Substring(valueStart, valueLineEnd - valueStart).Trim();
if (codeLine == groupCode && string.Equals(valueLine, value, StringComparison.OrdinalIgnoreCase))
return valueStart;
// Move to the next pair
pos = valueLineEnd + 1;
}
return -1;
}
}
}
+1 -9
View File
@@ -1,15 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
</startup>
<appSettings>
<add key="MaxBendRadius" value="2.0"/>
<add key="ExportOutputFolder" value="C:\ExportDXF\Output"/>
<add key="DefaultSuffix" value="PT{item_no:2}"/>
</appSettings>
<connectionStrings>
<add name="ExportDxfDb"
connectionString="Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"
providerName="Microsoft.Data.SqlClient"/>
</connectionStrings>
</configuration>
+109 -79
View File
@@ -1,79 +1,109 @@
# ExportDXF
A Windows desktop application that automates exporting flat pattern DXF files from SolidWorks drawings, assemblies, and parts. Built for sheet metal fabrication workflows, it extracts BOM data, generates DXF flat patterns with etch/bend line markings, exports drawing PDFs, and uploads everything to a CutFab API for downstream cut programming.
## Features
- **Batch DXF export** from SolidWorks drawings, assemblies, or individual parts
- **Flat pattern generation** with automatic sheet metal detection
- **Etch line insertion** on bend-up lines for fabrication reference (via EtchBendLines library)
- **PDF export** of SolidWorks drawings
- **CutFab API integration** -- uploads DXFs, PDFs, and BOM data with sheet metal properties (thickness, K-factor, bend radius, material)
- **View flip control** with automatic, manual, and prefer-up strategies
- **Drawing selection UI** that connects to SolidWorks and displays the active document
- **BOM extraction** from drawing BOM tables or assembly component trees
## Requirements
- Windows 10/11
- .NET Framework 4.8
- SolidWorks (installed and licensed)
- CutFab API server (default: `http://localhost:7027`)
## Solution Structure
```
ExportDXF.sln
ExportDXF/ Main WinForms application
EtchBendLines/ Library for adding etch lines to DXF files (git submodule)
netDxf/ DXF file read/write library
```
### Key Namespaces
| Namespace | Purpose |
|-----------|---------|
| `ExportDXF.Services` | Core services -- SolidWorks connection, DXF export, BOM extraction, PDF export, API client |
| `ExportDXF.Forms` | WinForms UI -- drawing selection and main export form |
| `ExportDXF.Models` | Data models -- BomItem, ExportContext, SolidWorksDocument |
| `ExportDXF.ViewFlipDeciders` | Strategies for determining if a flat pattern view should be flipped |
| `ExportDXF.ItemExtractors` | Extract component items from BOM tables and assemblies |
| `ExportDXF.Utilities` | SolidWorks helpers, sheet metal property extraction, text utilities |
| `EtchBendLines` | Post-processes DXF files to add etch marks on bend-up lines |
## How It Works
1. **Startup** -- Connects to a running SolidWorks instance (or launches one). Shows a drawing selection form that pulls equipment/drawing data from the CutFab API and displays the active SolidWorks document.
2. **Export** -- Based on the active document type:
- **Drawing**: Extracts BOM items from BOM tables, exports the drawing as PDF, then iterates through each component to generate flat pattern DXFs.
- **Assembly**: Extracts components from the assembly tree and generates flat pattern DXFs for each sheet metal part.
- **Part**: Generates a single flat pattern DXF for the active part.
3. **Post-processing** -- Each exported DXF is run through the EtchBendLines library, which identifies bend-up lines and adds short etch marks at their endpoints on a dedicated `ETCH` layer. Only up bends are etched because SolidWorks automatically flips the flat pattern so the shortest flange is the first bend. The etch marks let the press brake operator verify part orientation right off the laser table without flipping -- the first bend (shortest flange) will always have an etch line.
4. **Upload** -- DXF files (zipped), PDFs, and BOM item data are uploaded to the CutFab API along with sheet metal properties. The API auto-links cut templates based on material and thickness.
## Configuration
The API base URL is configured in `App.config`:
```xml
<appSettings>
<add key="CutFab.ApiBaseUrl" value="http://localhost:7027" />
</appSettings>
```
## DXF Filename Format
Exported files follow the pattern: `{EquipmentNo} {DrawingNo} PT{ItemNo}.dxf`
Example: `5007 A02 PT01.dxf`
## Building
Open `ExportDXF.sln` in Visual Studio and build. The EtchBendLines submodule must be initialized:
```bash
git submodule update --init --recursive
```
# ExportDXF
A Windows desktop application that automates exporting flat pattern DXF files from SolidWorks drawings, assemblies, and parts. Built for sheet metal fabrication workflows, it extracts BOM data, generates DXF flat patterns with etch/bend line markings, and exports everything to a local folder with an Excel workbook for downstream tools like [OpenNest](https://github.com/ajisaacs/OpenNest).
## Features
- **Batch DXF export** from SolidWorks drawings, assemblies, or individual parts
- **Flat pattern generation** with automatic sheet metal detection
- **Etch line insertion** on bend-up lines for fabrication reference (via EtchBendLines/ACadSharp)
- **PDF export** of SolidWorks drawings
- **Excel BOM output** -- generates an xlsx workbook with a BOM sheet (direct copy of the SolidWorks BOM table) and a Cut Templates sheet (DXF filenames, thicknesses, K-factors, bend radii, content hashes)
- **Revision tracking** -- content hashing detects unchanged DXFs across re-exports; changed files get revision suffixes (e.g., `PT03 Rev2.dxf`)
- **Configurable filename template** -- format like `4321 A01 PT{item_no:2}` with placeholders for item number, part name, configuration, and material
- **Pluggable drawing info extraction** -- auto-fills the filename template from the document name; extensible via `IDrawingInfoExtractor`
- **View flip control** with automatic, manual, and prefer-up strategies
- **Active document tracking** -- connects to SolidWorks and updates the UI when the active document changes
- **Per-export and app-level logging** to plain text log files
## Requirements
- Windows 10/11
- .NET 8.0
- SolidWorks (installed and licensed)
## Solution Structure
```
ExportDXF.sln
ExportDXF/ Main WinForms application
EtchBendLines/ Library for adding etch lines to DXF files (git submodule, uses ACadSharp)
```
### Key Namespaces
| Namespace | Purpose |
|-----------|---------|
| `ExportDXF.Services` | Core services -- SolidWorks connection, DXF export, BOM extraction, PDF export, Excel output, logging, filename template parsing, drawing info extraction |
| `ExportDXF.Forms` | WinForms UI -- main export form with log, BOM, and cut templates grids |
| `ExportDXF.Models` | Data models -- BomItem, CutTemplate, ExportContext, SolidWorksDocument |
| `ExportDXF.ViewFlipDeciders` | Strategies for determining if a flat pattern view should be flipped |
| `ExportDXF.ItemExtractors` | Extract component items from BOM tables and assemblies |
| `ExportDXF.Utilities` | SolidWorks helpers, content hashing, sheet metal property extraction, text utilities |
| `ExportDXF.Extensions` | Extension methods for SolidWorks, UI, unit conversion, strings, TimeSpan |
| `EtchBendLines` | Post-processes DXF files to add etch marks on bend-up lines |
## How It Works
1. **Startup** -- Connects to a running SolidWorks instance (or launches one). Auto-fills the filename template from the active document name using pluggable extractors.
2. **Export** -- Based on the active document type:
- **Drawing**: Copies the raw BOM table to the Excel BOM sheet, exports the drawing as PDF, then generates flat pattern DXFs for each sheet metal component.
- **Assembly**: Extracts components from the assembly tree and generates flat pattern DXFs for each sheet metal part.
- **Part**: Generates a single flat pattern DXF for the active part.
3. **Post-processing** -- Each exported DXF is run through the EtchBendLines library, which identifies bend-up lines and adds short etch marks at their endpoints on a dedicated `ETCH` layer. Only up bends are etched because SolidWorks automatically flips the flat pattern so the shortest flange is the first bend. The etch marks let the press brake operator verify part orientation right off the laser table without flipping.
4. **Save** -- DXF and PDF files are saved to a `Templates/` folder next to the source file. An Excel workbook is written with the BOM and Cut Templates sheets. If re-exporting, content hashes are compared against the existing workbook -- unchanged DXFs are skipped, changed DXFs get a revision suffix.
## Output
```
C:\Projects\4321\
├── 4321 A01.SLDDRW
└── Templates\
├── 4321 A01 PT01.dxf
├── 4321 A01 PT02.dxf
├── 4321 A01 PT03.dxf
├── 4321 A01 PT03 Rev2.dxf (revised, original kept)
├── 4321 A01.pdf
├── 4321 A01.xlsx
└── 4321 A01.log
```
### Excel Workbook
**BOM sheet** -- exact copy of all visible columns and rows from the SolidWorks BOM table (Drawing exports only).
**Cut Templates sheet**:
| Item # | File Name | Revision | Thickness | K-Factor | Bend Radius | Content Hash |
|--------|-----------|----------|-----------|----------|-------------|--------------|
## Configuration
Settings are in `App.config`:
```xml
<appSettings>
<add key="MaxBendRadius" value="2.0" />
<add key="DefaultSuffix" value="PT{item_no:2}" />
</appSettings>
```
### Filename Template Placeholders
| Placeholder | Description | Example |
|-------------|-------------|---------|
| `{item_no:N}` | Item number, zero-padded to N digits | `{item_no:2}``03` |
| `{part_name}` | SolidWorks part name | `Bracket` |
| `{config}` | Configuration name | `Default` |
| `{material}` | Material name | `AISI 304` |
## Building
Open `ExportDXF.sln` in Visual Studio and build. The EtchBendLines submodule must be initialized:
```bash
git submodule update --init --recursive
```