diff --git a/.gitignore b/.gitignore index bccd75e..00902a3 100644 --- a/.gitignore +++ b/.gitignore @@ -245,3 +245,6 @@ ModelManifest.xml # Test documents TestDocs/ + +# Superpowers specs and plans +docs/superpowers/ diff --git a/docs/superpowers/plans/2026-04-13-remove-api-excel-export.md b/docs/superpowers/plans/2026-04-13-remove-api-excel-export.md deleted file mode 100644 index a6c6046..0000000 --- a/docs/superpowers/plans/2026-04-13-remove-api-excel-export.md +++ /dev/null @@ -1,1150 +0,0 @@ -# Remove API, Export to Excel — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Remove FabWorks API and database dependencies from ExportDXF; export DXF files + a BOM Excel workbook to a local `Templates` folder, with content-hash-based revision tracking. - -**Architecture:** Merge the `feature/fabworks-api` branch for its structural improvements (service layer, async flow, content hashing, EtchBendLines fixes), then replace the API/DB output layer with local file export + ClosedXML Excel writing. A format template system with pluggable `IDrawingInfoExtractor` handles filename generation. - -**Tech Stack:** .NET 8.0, WinForms, ClosedXML 0.104.2, SolidWorks COM Interop, EtchBendLines submodule - ---- - -## File Structure - -### New Files -- `ExportDXF/Services/FilenameTemplateParser.cs` — parses `{item_no:N}`, `{part_name}`, `{config}`, `{material}` placeholders -- `ExportDXF/Services/IDrawingInfoExtractor.cs` — interface for extracting drawing info from document names -- `ExportDXF/Services/DefaultDrawingInfoExtractor.cs` — fallback: document name + configured suffix -- `ExportDXF/Services/EquipmentDrawingInfoExtractor.cs` — parses `{EquipmentNo} {DrawingNo}` pattern -- `ExportDXF/Models/DrawingInfo.cs` — result of drawing info extraction -- `ExportDXF/Services/ExcelExportService.cs` — reads/writes BOM.xlsx with ClosedXML -- `ExportDXF/Services/LogFileService.cs` — per-export and app-level log file writing -- `ExportDXF/Services/IRawBomTableReader.cs` — reads raw BOM table columns/rows from SolidWorks drawings -- `ExportDXF/Services/RawBomTableReader.cs` — implementation - -### Modified Files -- `ExportDXF/ExportDXF.csproj` — remove EF Core packages, add ClosedXML -- `ExportDXF/Program.cs` — rewire DI (remove API client, DB context; add new services) -- `ExportDXF/Forms/MainForm.cs` — replace dropdowns with format template textbox -- `ExportDXF/Forms/MainForm.Designer.cs` — UI layout changes -- `ExportDXF/App.config` — remove connection string, add DefaultSuffix -- `ExportDXF/Models/CutTemplate.cs` — add Revision property -- `ExportDXF/Models/ExportContext.cs` — replace Equipment/DrawingNo/Title with template + output folder -- `ExportDXF/Services/DxfExportService.cs` — replace API calls with local file writes + Excel updates - -### Deleted Files -- `ExportDXF/Services/FabWorksApiClient.cs` -- `ExportDXF/Services/IFabWorksApiClient.cs` -- `ExportDXF/Services/FabWorksApiDtos.cs` -- `ExportDXF/Data/ExportDxfDbContext.cs` (if present after merge) -- `ExportDXF/Data/Migrations/*` (if present after merge) -- `FabWorks.Core/` (entire project directory, if present after merge) -- `FabWorks.Api/` (entire project directory, if present after merge) - ---- - -### Task 1: Merge fabworks-api branch and clean up foreign projects - -**Files:** -- Modify: `ExportDXF.sln` -- Delete: `FabWorks.Core/` and `FabWorks.Api/` directories (if present after merge) - -- [ ] **Step 1: Merge the branch** - -```bash -git merge origin/feature/fabworks-api -m "merge: bring fabworks-api structural improvements into master" -``` - -Resolve any conflicts favoring the fabworks-api version for files in `ExportDXF/Services/` and `ExportDXF/Models/`. For `ExportDXF.csproj` and `Program.cs`, take fabworks-api version (we'll modify them in later tasks). - -- [ ] **Step 2: Remove FabWorks projects from solution and disk** - -Check what projects/directories exist after merge: - -```bash -ls -d FabWorks*/ 2>/dev/null -grep -i "fabworks" ExportDXF.sln -``` - -Remove any FabWorks project directories and their solution references: - -```bash -dotnet sln ExportDXF.sln remove FabWorks.Core/FabWorks.Core.csproj 2>/dev/null -dotnet sln ExportDXF.sln remove FabWorks.Api/FabWorks.Api.csproj 2>/dev/null -rm -rf FabWorks.Core/ FabWorks.Api/ -``` - -- [ ] **Step 3: Commit** - -```bash -git add -A -git commit -m "chore: remove FabWorks.Core and FabWorks.Api projects after merge" -``` - ---- - -### Task 2: Remove API client, database, and EF Core - -**Files:** -- Delete: `ExportDXF/Services/FabWorksApiClient.cs`, `ExportDXF/Services/IFabWorksApiClient.cs`, `ExportDXF/Services/FabWorksApiDtos.cs` -- Delete: `ExportDXF/Data/` directory (DbContext + migrations) -- Modify: `ExportDXF/ExportDXF.csproj` — remove EF Core packages, add ClosedXML -- Modify: `ExportDXF/App.config` — remove connection string and FabWorksApiUrl - -- [ ] **Step 1: Delete API client files** - -```bash -rm -f ExportDXF/Services/FabWorksApiClient.cs -rm -f ExportDXF/Services/IFabWorksApiClient.cs -rm -f ExportDXF/Services/FabWorksApiDtos.cs -``` - -- [ ] **Step 2: Delete database files** - -```bash -rm -rf ExportDXF/Data/ -``` - -- [ ] **Step 3: Update ExportDXF.csproj** - -Remove EF Core packages and add ClosedXML: - -```bash -cd ExportDXF -dotnet remove package Microsoft.EntityFrameworkCore.SqlServer 2>/dev/null -dotnet remove package Microsoft.EntityFrameworkCore.Tools 2>/dev/null -dotnet remove package Microsoft.EntityFrameworkCore.Design 2>/dev/null -dotnet add package ClosedXML --version 0.104.2 -cd .. -``` - -- [ ] **Step 4: Update App.config** - -Remove `FabWorksApiUrl` setting and the connection string. Add `DefaultSuffix` setting. - -The `appSettings` section should contain: - -```xml - - - - -``` - -Remove any `` section entirely. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "refactor: remove API client, database, and EF Core dependencies" -``` - ---- - -### Task 3: Create DrawingInfo model and IDrawingInfoExtractor - -**Files:** -- Create: `ExportDXF/Models/DrawingInfo.cs` -- Create: `ExportDXF/Services/IDrawingInfoExtractor.cs` -- Create: `ExportDXF/Services/DefaultDrawingInfoExtractor.cs` -- Create: `ExportDXF/Services/EquipmentDrawingInfoExtractor.cs` - -- [ ] **Step 1: Create DrawingInfo model** - -Create `ExportDXF/Models/DrawingInfo.cs`: - -```csharp -namespace ExportDXF.Models -{ - public class DrawingInfo - { - public string EquipmentNumber { get; set; } - public string DrawingNumber { get; set; } - public string DefaultTemplate { get; set; } - } -} -``` - -- [ ] **Step 2: Create IDrawingInfoExtractor interface** - -Create `ExportDXF/Services/IDrawingInfoExtractor.cs`: - -```csharp -using ExportDXF.Models; - -namespace ExportDXF.Services -{ - public interface IDrawingInfoExtractor - { - bool TryExtract(string documentName, out DrawingInfo info); - } -} -``` - -- [ ] **Step 3: Create EquipmentDrawingInfoExtractor** - -Create `ExportDXF/Services/EquipmentDrawingInfoExtractor.cs`: - -```csharp -using System.Text.RegularExpressions; -using ExportDXF.Models; - -namespace ExportDXF.Services -{ - public class EquipmentDrawingInfoExtractor : IDrawingInfoExtractor - { - private static readonly Regex Pattern = new Regex( - @"^(?\d+)\s+(?[A-Za-z]\d+)", - RegexOptions.Compiled); - - public bool TryExtract(string documentName, out DrawingInfo info) - { - info = null; - - // Strip file extension - var name = Path.GetFileNameWithoutExtension(documentName); - - var match = Pattern.Match(name); - if (!match.Success) - return false; - - var equip = match.Groups["equip"].Value; - var drawing = match.Groups["drawing"].Value; - - info = new DrawingInfo - { - EquipmentNumber = equip, - DrawingNumber = drawing, - DefaultTemplate = $"{equip} {drawing} PT{{item_no:2}}" - }; - - return true; - } - } -} -``` - -- [ ] **Step 4: Create DefaultDrawingInfoExtractor** - -Create `ExportDXF/Services/DefaultDrawingInfoExtractor.cs`: - -```csharp -using System.Configuration; -using ExportDXF.Models; - -namespace ExportDXF.Services -{ - public class DefaultDrawingInfoExtractor : IDrawingInfoExtractor - { - public bool TryExtract(string documentName, out DrawingInfo info) - { - var name = Path.GetFileNameWithoutExtension(documentName); - var suffix = ConfigurationManager.AppSettings["DefaultSuffix"] ?? "PT{item_no:2}"; - - info = new DrawingInfo - { - EquipmentNumber = null, - DrawingNumber = null, - DefaultTemplate = $"{name} {suffix}" - }; - - return true; - } - } -} -``` - -- [ ] **Step 5: Commit** - -```bash -git add ExportDXF/Models/DrawingInfo.cs ExportDXF/Services/IDrawingInfoExtractor.cs ExportDXF/Services/DefaultDrawingInfoExtractor.cs ExportDXF/Services/EquipmentDrawingInfoExtractor.cs -git commit -m "feat: add IDrawingInfoExtractor with equipment and default implementations" -``` - ---- - -### Task 4: Create FilenameTemplateParser - -**Files:** -- Create: `ExportDXF/Services/FilenameTemplateParser.cs` - -- [ ] **Step 1: Create FilenameTemplateParser** - -Create `ExportDXF/Services/FilenameTemplateParser.cs`: - -```csharp -using System.Text.RegularExpressions; -using ExportDXF.Models; - -namespace ExportDXF.Services -{ - public static class FilenameTemplateParser - { - private static readonly Regex PlaceholderPattern = new Regex( - @"\{(?\w+)(?::(?\d+))?\}", - RegexOptions.Compiled); - - /// - /// Evaluates a template string for a given item. - /// e.g. "4321 A01 PT{item_no:2}" with item_no=3 → "4321 A01 PT03" - /// - 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.ToString(); - 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; // leave unknown placeholders as-is - break; - } - - return value; - }); - } - - /// - /// Extracts the literal prefix before the first placeholder. - /// Used for naming the xlsx and log files. - /// Falls back to documentName if prefix is empty. - /// - 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; - } - - /// - /// Validates that the template contains {item_no} to prevent filename collisions. - /// - 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.IsMatch(template) && - PlaceholderPattern.Matches(template) - .Cast() - .Any(m => m.Groups["name"].Value.ToLowerInvariant() == "item_no"); - - if (!hasItemNo) - { - error = "Template must contain {item_no} or {item_no:N} to avoid filename collisions."; - return false; - } - - return true; - } - } -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add ExportDXF/Services/FilenameTemplateParser.cs -git commit -m "feat: add FilenameTemplateParser with placeholder evaluation and validation" -``` - ---- - -### Task 5: Update CutTemplate model with Revision - -**Files:** -- Modify: `ExportDXF/Models/CutTemplate.cs` - -- [ ] **Step 1: Add Revision property to CutTemplate** - -Add `Revision` property (int, default 1) to `CutTemplate.cs`. Remove any EF Core attributes or database-specific annotations (foreign keys to ExportRecord, etc.): - -```csharp -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; - public double Thickness { get; set; } - public double KFactor { get; set; } - public double DefaultBendRadius { get; set; } - public int BomItemId { get; set; } - } -} -``` - -- [ ] **Step 2: Clean up BomItem.cs** - -Remove any EF Core navigation properties or database annotations. Keep the data properties: - -```csharp -namespace ExportDXF.Models -{ - public class BomItem - { - public int ItemNo { get; set; } - public string PartNo { get; set; } - public int SortOrder { get; set; } - public int Qty { get; set; } - public int TotalQty { get; set; } - public string Description { get; set; } - public string PartName { get; set; } - public string ConfigurationName { get; set; } - public string Material { get; set; } - public CutTemplate CutTemplate { get; set; } - } -} -``` - -- [ ] **Step 3: Remove ExportRecord.cs** - -The export record model was for DB persistence. Delete it — the log file replaces this purpose: - -```bash -rm -f ExportDXF/Models/ExportRecord.cs -``` - -- [ ] **Step 4: Update ExportContext.cs** - -Replace API-oriented fields with template and output folder: - -```csharp -using SldWorks; - -namespace ExportDXF.Models -{ - public class ExportContext - { - public SolidWorksDocument ActiveDocument { get; set; } - public IViewFlipDecider ViewFlipDecider { get; set; } - public string FilenameTemplate { get; set; } - public string OutputFolder { get; set; } - public CancellationToken CancellationToken { get; set; } - public Action ProgressCallback { get; set; } - public Action BomItemCallback { get; set; } - public SldWorks.SldWorks SolidWorksApp { get; set; } - - private DrawingDoc _templateDrawing; - - public DrawingDoc GetOrCreateTemplateDrawing() - { - if (_templateDrawing != null) - return _templateDrawing; - - var templatePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Templates", "Blank.drwdot"); - var swApp = SolidWorksApp; - int errors = 0, warnings = 0; - var doc = swApp.NewDocument(templatePath, 0, 0, 0); - _templateDrawing = (DrawingDoc)doc; - return _templateDrawing; - } - - public void CleanupTemplateDrawing() - { - if (_templateDrawing == null) return; - var swApp = SolidWorksApp; - swApp.CloseDoc(((ModelDoc2)_templateDrawing).GetTitle()); - _templateDrawing = null; - } - - public void LogProgress(string message, LogLevel level = LogLevel.Info, string file = null) - { - ProgressCallback?.Invoke(message, level, file); - } - } -} -``` - -Note: Preserve the exact `GetOrCreateTemplateDrawing` and `CleanupTemplateDrawing` implementations from the fabworks-api branch — the above is approximate. The key change is replacing `Equipment`, `DrawingNo`, `Title`, `FilePrefix`, `EquipmentId` with `FilenameTemplate` and `OutputFolder`. - -- [ ] **Step 5: Commit** - -```bash -git add -A -git commit -m "refactor: update models — add CutTemplate.Revision, remove ExportRecord, simplify ExportContext" -``` - ---- - -### Task 6: Create ExcelExportService - -**Files:** -- Create: `ExportDXF/Services/ExcelExportService.cs` - -- [ ] **Step 1: Create ExcelExportService** - -Create `ExportDXF/Services/ExcelExportService.cs`: - -```csharp -using System.Collections.Generic; -using System.IO; -using System.Linq; -using ClosedXML.Excel; -using ExportDXF.Models; - -namespace ExportDXF.Services -{ - public class ExcelExportService - { - /// - /// Reads existing Cut Templates from an xlsx file to compare content hashes. - /// Returns empty dictionary if file doesn't exist. - /// Key = Item #, Value = (ContentHash, Revision, FileName) - /// - public Dictionary ReadExistingCutTemplates(string xlsxPath) - { - var result = new Dictionary(); - - if (!File.Exists(xlsxPath)) - return result; - - using (var workbook = new XLWorkbook(xlsxPath)) - { - if (!workbook.TryGetWorksheet("Cut Templates", out var ws)) - return result; - - var headerRow = ws.Row(1); - var headers = new Dictionary(); - for (int col = 1; col <= ws.LastColumnUsed()?.ColumnNumber() ?? 0; col++) - { - headers[headerRow.Cell(col).GetString()] = col; - } - - if (!headers.ContainsKey("Item #") || !headers.ContainsKey("Content Hash")) - return result; - - for (int row = 2; row <= ws.LastRowUsed()?.RowNumber() ?? 1; row++) - { - var itemNo = ws.Cell(row, headers["Item #"]).GetValue(); - var hash = ws.Cell(row, headers["Content Hash"]).GetString(); - var revision = headers.ContainsKey("Revision") - ? ws.Cell(row, headers["Revision"]).GetValue() - : 1; - var fileName = headers.ContainsKey("File Name") - ? ws.Cell(row, headers["File Name"]).GetString() - : ""; - - result[itemNo] = (hash, revision, fileName); - } - } - - return result; - } - - /// - /// 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, the BOM sheet is not written (Part/Assembly exports). - /// - public void Write( - string xlsxPath, - List> rawBomTable, - List bomItems) - { - using (var workbook = File.Exists(xlsxPath) - ? new XLWorkbook(xlsxPath) - : new XLWorkbook()) - { - // BOM sheet — full rewrite from drawing BOM table - if (rawBomTable != null && rawBomTable.Count > 0) - { - if (workbook.TryGetWorksheet("BOM", out var existingBom)) - workbook.Worksheets.Delete("BOM"); - - var bomSheet = workbook.Worksheets.Add("BOM"); - var columns = rawBomTable[0].Keys.ToList(); - - // Headers - for (int col = 0; col < columns.Count; col++) - bomSheet.Cell(1, col + 1).Value = columns[col]; - - // Data rows - for (int row = 0; row < rawBomTable.Count; row++) - { - for (int col = 0; col < columns.Count; col++) - { - var value = rawBomTable[row].GetValueOrDefault(columns[col], ""); - bomSheet.Cell(row + 2, col + 1).Value = value; - } - } - - bomSheet.Columns().AdjustToContents(); - } - - // Cut Templates sheet — full rewrite with current data - if (workbook.TryGetWorksheet("Cut Templates", out var existingCt)) - workbook.Worksheets.Delete("Cut Templates"); - - var ctSheet = workbook.Worksheets.Add("Cut Templates"); - - // Headers - var ctHeaders = new[] { "Item #", "File Name", "Revision", "Thickness", "K-Factor", "Bend Radius", "Content Hash" }; - for (int col = 0; col < ctHeaders.Length; col++) - ctSheet.Cell(1, col + 1).Value = ctHeaders[col]; - - // Data rows — only BOM items that have a CutTemplate - int ctRow = 2; - foreach (var item in bomItems.Where(b => b.CutTemplate != null).OrderBy(b => b.ItemNo)) - { - var ct = item.CutTemplate; - ctSheet.Cell(ctRow, 1).Value = item.ItemNo; - ctSheet.Cell(ctRow, 2).Value = ct.DxfFilePath; - ctSheet.Cell(ctRow, 3).Value = ct.Revision; - ctSheet.Cell(ctRow, 4).Value = ct.Thickness; - ctSheet.Cell(ctRow, 5).Value = ct.KFactor; - ctSheet.Cell(ctRow, 6).Value = ct.DefaultBendRadius; - ctSheet.Cell(ctRow, 7).Value = ct.ContentHash; - ctRow++; - } - - ctSheet.Columns().AdjustToContents(); - - workbook.SaveAs(xlsxPath); - } - } - } -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add ExportDXF/Services/ExcelExportService.cs -git commit -m "feat: add ExcelExportService for BOM and Cut Templates xlsx output" -``` - ---- - -### Task 7: Create LogFileService - -**Files:** -- Create: `ExportDXF/Services/LogFileService.cs` - -- [ ] **Step 1: Create LogFileService** - -Create `ExportDXF/Services/LogFileService.cs`: - -```csharp -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) - { - 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) - { - var dir = Path.GetDirectoryName(AppLogPath); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - File.AppendAllText(AppLogPath, line + Environment.NewLine); - } - - public void Dispose() - { - _exportLog?.Dispose(); - _exportLog = null; - } - } -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add ExportDXF/Services/LogFileService.cs -git commit -m "feat: add LogFileService for per-export and app-level logging" -``` - ---- - -### Task 8: Create RawBomTableReader - -**Files:** -- Create: `ExportDXF/Services/RawBomTableReader.cs` - -- [ ] **Step 1: Create RawBomTableReader** - -This reads the SolidWorks BOM table as raw column/row data (all visible columns, all visible rows) for direct copy into the Excel BOM sheet. - -Create `ExportDXF/Services/RawBomTableReader.cs`: - -```csharp -using System.Collections.Generic; -using SolidWorks.Interop.sldworks; -using SolidWorks.Interop.swconst; - -namespace ExportDXF.Services -{ - public static class RawBomTableReader - { - /// - /// Reads all visible columns and rows from a BOM table annotation. - /// Returns a list of rows, each row a dictionary of column header → cell value. - /// - public static List> Read(ITableAnnotation table) - { - var rows = new List>(); - - int colCount = table.ColumnCount; - int rowCount = table.RowCount; - - // Build column headers (visible only) - 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(); - foreach (var (colIdx, header) in columns) - { - rowData[header] = table.get_Text(row, colIdx)?.Trim() ?? ""; - } - rows.Add(rowData); - } - - return rows; - } - } -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add ExportDXF/Services/RawBomTableReader.cs -git commit -m "feat: add RawBomTableReader for copying SolidWorks BOM tables to Excel" -``` - ---- - -### Task 9: Update DxfExportService for local export - -**Files:** -- Modify: `ExportDXF/Services/DxfExportService.cs` - -This is the largest change. The service currently calls the API for file uploads, record creation, and BOM item persistence. We replace all of that with: -- Write DXF files directly to the output folder -- Use FilenameTemplateParser for naming -- Use ExcelExportService for revision tracking and xlsx output -- Use LogFileService for logging - -- [ ] **Step 1: Update DxfExportService constructor and dependencies** - -Replace `IFabWorksApiClient` dependency with `ExcelExportService` and `LogFileService`. Remove all API-related fields. - -The constructor should accept: - -```csharp -public DxfExportService( - IPartExporter partExporter, - IDrawingExporter drawingExporter, - IBomExtractor bomExtractor, - ExcelExportService excelExportService, - LogFileService logFileService) -``` - -- [ ] **Step 2: Rewrite ExportAsync to use local output** - -The main export flow becomes: - -```csharp -public async Task ExportAsync(ExportContext 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}"); - - // Read existing cut templates for revision comparison - var existingTemplates = _excelExportService.ReadExistingCutTemplates(xlsxPath); - - var bomItems = new List(); - List> rawBomTable = null; - - try - { - switch (context.ActiveDocument.DocumentType) - { - case DocumentType.Part: - await ExportPartAsync(context, outputFolder, existingTemplates, bomItems); - break; - case DocumentType.Assembly: - await ExportAssemblyAsync(context, outputFolder, existingTemplates, bomItems); - break; - case DocumentType.Drawing: - rawBomTable = await ExportDrawingAsync(context, outputFolder, existingTemplates, bomItems); - break; - } - - // Write Excel file - _excelExportService.Write(xlsxPath, rawBomTable, bomItems); - _logFileService.LogInfo($"Wrote {xlsxPath}"); - } - catch (OperationCanceledException) - { - _logFileService.LogWarning("Export cancelled by user"); - throw; - } - catch (Exception ex) - { - _logFileService.LogError($"Export failed: {ex.Message}"); - throw; - } - finally - { - context.CleanupTemplateDrawing(); - } -} -``` - -- [ ] **Step 3: Rewrite ExportItemsAsync for local file output with revision tracking** - -For each item that is sheet metal: - -1. Export DXF to temp location via `_partExporter` -2. Run EtchBendLines processing -3. Compute content hash -4. Check `existingTemplates` for matching item number: - - **Hash matches** → skip file write, reuse existing filename and revision - - **Hash differs** → increment revision, write file with revision suffix (e.g., `PT03 Rev2.dxf`) - - **New item** → write file, revision = 1 -5. Create BomItem + CutTemplate, invoke callback - -The DXF filename is generated by `FilenameTemplateParser.Evaluate(context.FilenameTemplate, item)`. - -Revision suffix logic: - -```csharp -private string GetDxfFileName(string baseName, int revision) -{ - if (revision <= 1) - return $"{baseName}.dxf"; - return $"{baseName} Rev{revision}.dxf"; -} -``` - -- [ ] **Step 4: Update ExportDrawingAsync to read raw BOM table** - -Before extracting items, read the raw BOM table for the Excel BOM sheet: - -```csharp -private async Task>> ExportDrawingAsync( - ExportContext context, - string outputFolder, - Dictionary existingTemplates, - List bomItems) -{ - var drawingDoc = (DrawingDoc)context.ActiveDocument.NativeDocument; - - // Read raw BOM table for Excel output - var rawBomTable = new List>(); - var bomFeature = (BomFeature)/* get BOM feature from drawing */; - // Use existing BomExtractor to get the table annotations, then RawBomTableReader.Read() - - // Export PDF - await Task.Run(() => _drawingExporter.ExportToPdf(drawingDoc, outputFolder, context)); - - // Extract and export items - var items = _bomExtractor.ExtractFromDrawing(drawingDoc, context.ProgressCallback); - await ExportItemsAsync(items, context, outputFolder, existingTemplates, bomItems); - - return rawBomTable; -} -``` - -Note: The exact SolidWorks API calls to get BOM table annotations should be preserved from the existing BomExtractor code. Add a call to `RawBomTableReader.Read()` alongside the existing item extraction. - -- [ ] **Step 5: Remove all API client calls** - -Search for any remaining references to `_apiClient`, `FabWorksApiClient`, `IFabWorksApiClient`, `ApiExportDetail`, etc. and remove them. The service should have zero references to the API. - -- [ ] **Step 6: Commit** - -```bash -git add -A -git commit -m "refactor: rewrite DxfExportService for local file export with revision tracking" -``` - ---- - -### Task 10: Update MainForm UI - -**Files:** -- Modify: `ExportDXF/Forms/MainForm.cs` -- Modify: `ExportDXF/Forms/MainForm.Designer.cs` - -- [ ] **Step 1: Update MainForm.Designer.cs** - -Replace the equipment and drawing combo boxes with a single format template text box: - -- Remove: `cboEquipment`, `cboDrawing`, `lblEquipment`, `lblDrawing`, `txtTitle` -- Add: `txtFilenameTemplate` (TextBox), `lblFilenameTemplate` (Label with text "Filename Template:") -- Position the template textbox where the dropdowns were, spanning the full width - -- [ ] **Step 2: Update MainForm.cs constructor and fields** - -Replace `IFabWorksApiClient` with `IDrawingInfoExtractor[]` (list of extractors): - -```csharp -private readonly IDxfExportService _exportService; -private readonly IDrawingInfoExtractor[] _extractors; - -public MainForm( - SolidWorksService solidWorksService, - IDxfExportService exportService, - IDrawingInfoExtractor[] extractors) -{ - InitializeComponent(); - _solidWorksService = solidWorksService; - _exportService = exportService; - _extractors = extractors; - // ... rest of init -} -``` - -- [ ] **Step 3: Update document change handler for auto-fill** - -When the active SolidWorks document changes, try each extractor: - -```csharp -private void OnActiveDocumentChanged(SolidWorksDocument doc) -{ - if (doc == null) - { - txtFilenameTemplate.Text = ""; - return; - } - - foreach (var extractor in _extractors) - { - if (extractor.TryExtract(doc.Title, out var info)) - { - txtFilenameTemplate.Text = info.DefaultTemplate; - return; - } - } -} -``` - -- [ ] **Step 4: Update StartExportAsync** - -Build the ExportContext with template and output folder: - -```csharp -private async Task StartExportAsync() -{ - var template = txtFilenameTemplate.Text.Trim(); - if (!FilenameTemplateParser.Validate(template, out var error)) - { - MessageBox.Show(error, "Invalid Template", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } - - var doc = _solidWorksService.GetActiveDocument(); - var sourceDir = Path.GetDirectoryName(doc.FilePath); - var outputFolder = Path.Combine(sourceDir, "Templates"); - - var context = new ExportContext - { - ActiveDocument = doc, - ViewFlipDecider = GetSelectedViewFlipDecider(), - FilenameTemplate = template, - OutputFolder = outputFolder, - CancellationToken = _cancellationTokenSource.Token, - ProgressCallback = (msg, level, file) => AddLogEvent(msg, level, file), - BomItemCallback = bomItem => AddBomItem(bomItem), - SolidWorksApp = _solidWorksService.Application - }; - - await _exportService.ExportAsync(context); -} -``` - -- [ ] **Step 5: Remove all API/DB dropdown loading methods** - -Delete `LoadDrawingDropdownsAsync`, `UpdateDrawingDropdownAsync`, `LookupDrawingInfoFromHistoryAsync`, and any equipment/drawing dropdown event handlers. - -- [ ] **Step 6: Commit** - -```bash -git add -A -git commit -m "refactor: replace equipment/drawing dropdowns with filename template textbox" -``` - ---- - -### Task 11: Update Program.cs and wire dependencies - -**Files:** -- Modify: `ExportDXF/Program.cs` - -- [ ] **Step 1: Rewrite ServiceContainer** - -```csharp -public class ServiceContainer -{ - public MainForm ResolveMainForm() - { - var swService = new SolidWorksService(); - var bomExtractor = new BomExtractor(); - var partExporter = new PartExporter(); - var drawingExporter = new DrawingExporter(); - var excelExportService = new ExcelExportService(); - var logFileService = new LogFileService(); - - var exportService = new DxfExportService( - partExporter, - drawingExporter, - bomExtractor, - excelExportService, - logFileService); - - var extractors = new IDrawingInfoExtractor[] - { - new EquipmentDrawingInfoExtractor(), - new DefaultDrawingInfoExtractor() - }; - - return new MainForm(swService, exportService, extractors); - } -} -``` - -- [ ] **Step 2: Remove HttpClient, API client, DB context factory creation** - -Delete all code related to `HttpClient`, `FabWorksApiClient`, `FabWorksApiUrl`, `ExportDxfDbContext`, and `FileExportService` from Program.cs. - -- [ ] **Step 3: Commit** - -```bash -git add ExportDXF/Program.cs -git commit -m "refactor: rewire Program.cs DI — remove API/DB, add Excel and log services" -``` - ---- - -### Task 12: Build, fix compile errors, and verify - -**Files:** -- Various (fixing any remaining compile errors) - -- [ ] **Step 1: Build the solution** - -```bash -cd ExportDXF -dotnet build ExportDXF.csproj -``` - -- [ ] **Step 2: Fix any remaining compile errors** - -Common issues to watch for: -- Remaining references to `IFabWorksApiClient`, `FabWorksApiClient`, `ApiExportDetail`, etc. -- Remaining references to `ExportRecord`, `ExportDxfDbContext` -- Missing `using` statements for new namespaces -- `ExportContext` property name changes (`FilePrefix` → `FilenameTemplate`, `Equipment` → removed, etc.) -- `DxfExportService` method signature changes - -- [ ] **Step 3: Clean build verification** - -```bash -dotnet build ExportDXF.csproj --no-incremental -``` - -Expected: `Build succeeded. 0 Warning(s) 0 Error(s)` - -- [ ] **Step 4: Final commit** - -```bash -git add -A -git commit -m "fix: resolve remaining compile errors after API removal" -``` - -- [ ] **Step 5: Push to remote** - -```bash -git push origin master -``` diff --git a/docs/superpowers/specs/2026-04-13-remove-api-excel-export-design.md b/docs/superpowers/specs/2026-04-13-remove-api-excel-export-design.md deleted file mode 100644 index 2f2cf34..0000000 --- a/docs/superpowers/specs/2026-04-13-remove-api-excel-export-design.md +++ /dev/null @@ -1,181 +0,0 @@ -# ExportDXF: Remove API, Export to Excel - -**Date:** 2026-04-13 -**Status:** Draft - -## Goal - -Remove the FabWorks API and database dependencies from ExportDXF. Replace with local file output: DXF files + an Excel workbook (BOM + Cut Templates) that downstream tools like OpenNest can import directly. Make the tool self-contained — no server setup required. - -## Approach - -Merge the `feature/fabworks-api` branch into `master` to get its structural improvements (service layer, models, content hashing, EtchBendLines fixes), then remove the API/DB layer and replace with Excel + log file output. - ---- - -## Output Structure - -### Output Folder - -- Default location: `Templates/` folder in the same directory as the source SolidWorks file -- If `Templates/` already exists, the export writes into it (see Revision Handling below) - -### Files Produced - -``` -C:\Projects\4321\ -├── 4321 A01.SLDDRW (source drawing) -└── Templates\ - ├── 4321 A01 PT01.dxf - ├── 4321 A01 PT02.dxf - ├── 4321 A01 PT03.dxf - ├── 4321 A01 PT03 Rev2.dxf (revised file, original kept) - ├── 4321 A01.xlsx - └── 4321 A01.log -``` - -### Excel Workbook - -Written with **ClosedXML 0.104.2** (same version as OpenNest). - -**"BOM" sheet** — exact copy of the SolidWorks BOM table. All visible columns and rows reproduced as-is. Only present when exporting from a Drawing that contains a BOM table. Part and Assembly exports omit this sheet. - -**"Cut Templates" sheet** — one row per sheet metal part: - -| Column | Description | -|--------|-------------| -| Item # | Item number from the BOM or export sequence | -| File Name | DXF filename stem (no path, no extension) | -| Revision | Revision number (1 for initial, increments on change) | -| Thickness | Sheet metal thickness | -| K-Factor | K-Factor value | -| Bend Radius | Default bend radius | -| Content Hash | SHA256 of DXF content (excluding HEADER section) | - -Present for all export types (Part, Assembly, Drawing). - -### Log Files - -**Per-export log:** `Templates/{prefix}.log` -- Timestamped lines: `2026-04-13 14:32:05 [INFO] Exported 4321 A01 PT03.dxf` -- Levels: INFO, WARNING, ERROR -- Appends on re-export - -**App-level log:** `C:\ExportDXF\ExportDXF.log` -- Rolling log capturing all exports across all drawings -- Same format as per-export log - ---- - -## Filename Format Template - -### User Interface - -A single text box replaces the equipment/drawing dropdowns. The user types a format string that controls DXF and xlsx naming. - -**Example:** `4321 A01 PT{item_no:2}` - -### Supported Placeholders - -| Placeholder | Description | Example Output | -|-------------|-------------|----------------| -| `{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` | - -### Naming Rules - -- DXF filename: full template evaluated per part → `4321 A01 PT03.dxf` -- Excel filename: literal text before the first placeholder → `4321 A01.xlsx` -- Log filename: same stem as excel → `4321 A01.log` -- Validation: template must contain `{item_no}` (or `{item_no:N}`) to avoid filename collisions -- If the template starts with a placeholder (no literal prefix), fall back to the document name for the xlsx/log filename - -### Auto-Fill - -On document open, the format template text box is auto-filled via `IDrawingInfoExtractor`. - ---- - -## Drawing Info Extraction - -### Interface - -```csharp -public interface IDrawingInfoExtractor -{ - bool TryExtract(string documentName, out DrawingInfo info); -} - -public class DrawingInfo -{ - public string EquipmentNumber { get; set; } - public string DrawingNumber { get; set; } - public string DefaultTemplate { get; set; } -} -``` - -### Implementations - -**`DefaultDrawingInfoExtractor`** — uses the document name as literal prefix, appends the default suffix from app.config (`DefaultSuffix` setting, default `PT{item_no:2}`). - -Example: document `MyPart.SLDPRT` → template `MyPart {item_no:2}` (or with configured suffix) - -**`EquipmentDrawingInfoExtractor`** — parses the `{EquipmentNo} {DrawingNo}` pattern from document names in AJ's workplace format. Builds template like `4321 A01 PT{item_no:2}`. - -### Resolution - -Extractors are tried in order. First one that returns `true` wins. Falls back to `DefaultDrawingInfoExtractor`. - ---- - -## Revision Handling - -When re-exporting to an existing `Templates` folder: - -1. Read the existing xlsx Cut Templates sheet (if present) -2. For each part being exported, compute the DXF content hash -3. **Hash matches existing row** → skip, leave existing DXF file untouched -4. **Hash differs from existing row** → write new DXF with revision suffix (e.g., `PT03 Rev2.dxf`), update the xlsx row with new filename, new hash, incremented revision number -5. **New part (no existing row)** → write DXF, add new row to xlsx with revision 1 -6. Old revision DXF files are kept in the folder as history - -The BOM sheet (if present) is fully rewritten from the current drawing's BOM table on each export. - ---- - -## What We Keep from feature/fabworks-api - -- Service layer architecture: `DxfExportService`, `IPartExporter`, `IDrawingExporter`, `IBomExtractor` -- Models: `BomItem`, `CutTemplate`, `Item`, `ExportContext`, `LogEvent` -- `ContentHasher` utility (SHA256 excluding DXF HEADER section) -- All 9 EtchBendLines submodule fixes (ACadSharp migration, bend detection, degree symbol, etc.) -- Async export flow with cancellation -- View flip decider -- SolidWorks user input disabling during export - -## What We Remove - -- `FabWorksApiClient` and all API DTOs -- `ExportDxfDbContext`, EF Core migrations, SQL Server connection string -- NuGet packages: `Microsoft.EntityFrameworkCore.SqlServer`, `Microsoft.EntityFrameworkCore.Tools` -- Equipment/Drawing dropdowns from MainForm -- Any FabWorks.Core / FabWorks.Api projects (if present after merge) - -## What We Add - -- `IDrawingInfoExtractor` interface + two implementations -- Format template text box with placeholder parsing -- `ExcelExportService` using ClosedXML — reads/writes BOM + Cut Templates sheets -- `LogFileService` — per-export and app-level log writing -- Output folder logic (Templates folder next to source, revision handling) -- NuGet package: `ClosedXML 0.104.2` -- `DefaultSuffix` setting in app.config - -## UI Changes - -- Replace equipment/drawing combo boxes with a single format template text box -- Keep 3 tabs: Log Events, Bill of Materials, Cut Templates -- Keep Start/Stop button -- Keep View Flip Decider dropdown