Compare commits

47 Commits

Author SHA1 Message Date
bd3e7c2a36 chore: remove .claude/settings.local.json from tracking
Local Claude Code settings are machine-specific and should not be
version controlled. Added .claude/ to .gitignore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 15:58:15 -05:00
b9e84de7c0 feat: move revision tracking to ExportRecord, add perceptual hash comparison, cut list modal, and auto-start API
- Move Revision from Drawing to ExportRecord so each export captures its own revision snapshot
- Add Hamming distance comparison for perceptual hashes (tolerance of 10 bits) to avoid false revision bumps
- Replace CoenM.ImageHash with inline DifferenceHash impl (compatible with ImageSharp 3.x)
- Increase PDF render DPI from 72 to 150 for better hash fidelity
- Add download-dxfs-by-drawing endpoint for cross-export DXF zip downloads
- Prefix DXF filenames with equipment number when no drawing number is present
- Pass original filename to storage service for standalone part exports
- Auto-start FabWorks.Api from ExportDXF client if not already running
- Add cut list modal with copy-to-clipboard in the web UI
- Update PDF hash on existing export records after upload
- Bump static asset cache versions to v3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 15:48:28 -05:00
f6cd91f1b5 docs: add design plans for API and auto-fill features
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:54:21 -05:00
3554bb6110 feat: show revision column in file browser
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:54:13 -05:00
4707e96359 feat: resolve drawing revisions on PDF upload
When a PDF hash is set on an export record, resolve or create the
associated Drawing and bump its revision if the content hash changed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:54:07 -05:00
c5bd7fb4c8 feat: add Drawing entity with revision tracking
Introduce a Drawing table that tracks unique drawing numbers with their
current PDF content hash and revision counter. ExportRecord gains a
DrawingId FK. Includes a data migration to seed Drawings from existing
export records.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:54:01 -05:00
13c61a82a4 fix: log etch line failures instead of silently swallowing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:53:51 -05:00
444a077cbc fix: resize form controls to match window layout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:53:46 -05:00
c4920f933d chore: remove netDxf project from solution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:53:39 -05:00
b472729fda feat: use perceptual hash for PDF change detection
Render PDF page 1 to an image and compute a DifferenceHash instead of
SHA256 on raw file bytes. This ignores metadata/timestamp changes that
SolidWorks varies between exports, preventing false revision bumps on
Drawing entities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 08:52:06 -05:00
5d2948d563 feat: replace text-based DXF hash with geometric content hash
SolidWorks re-exports produce files with identical geometry but different
entity ordering, handle assignments, style names, and floating-point
epsilon values. This caused hash mismatches and unnecessary API updates.

Uses ACadSharp to parse DXF entities and build canonical, sorted
signatures (LINE, ARC, CIRCLE, MTEXT) with coordinates rounded to 4
decimal places. Falls back to raw file hash if parsing fails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 07:40:51 -05:00
71c65e0bf5 feat: auto-populate title from part description when opening documents
Adds a Description property to DrawingInfo that extracts the
descriptive text after the equipment/drawing number, excluding the
file extension. MainForm now sets the title box from this description
when no title was already set by the API history lookup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 05:53:08 -05:00
53aa23f762 refactor: consolidate export detail into drawing detail page
Remove the duplicate export detail page and route exports list
directly to drawing detail. When navigating from exports, the
specific export's BOM items are shown via eid param; from drawings,
items are deduplicated to the latest revision. Add Rev column,
PDF download, and All DXFs download to drawing detail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 17:26:43 -05:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
16dc74c35d test: add FormProgramService tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 06:36:02 -05:00
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
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
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
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
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
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
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
75 changed files with 8751 additions and 561 deletions

View File

@@ -1,10 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git add:*)",
"Bash(git commit:*)"
],
"deny": [],
"ask": []
}
}

3
.gitignore vendored
View File

@@ -245,3 +245,6 @@ ModelManifest.xml
# Test documents # Test documents
TestDocs/ TestDocs/
# Claude Code local settings
.claude/

View File

@@ -1,32 +1,88 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16 # Visual Studio Version 18
VisualStudioVersion = 16.0.29123.88 VisualStudioVersion = 18.3.11512.155 d18.3
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportDXF", "ExportDXF\ExportDXF.csproj", "{05F21D73-FD31-4E77-8D9B-41C86D4D8305}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExportDXF", "ExportDXF\ExportDXF.csproj", "{05F21D73-FD31-4E77-8D9B-41C86D4D8305}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLines\EtchBendLines\EtchBendLines.csproj", "{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EtchBendLines", "EtchBendLines\EtchBendLines\EtchBendLines.csproj", "{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "netDxf", "EtchBendLines\netDxf\netDxf\netDxf.csproj", "{785380E0-CEB9-4C34-82E5-60D0E33E848E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Core", "FabWorks.Core\FabWorks.Core.csproj", "{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Tests", "FabWorks.Tests\FabWorks.Tests.csproj", "{6DD89774-D86B-47E9-B982-2794BD95616A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FabWorks.Api", "FabWorks.Api\FabWorks.Api.csproj", "{9BD571FA-52D8-430D-8843-FEB6EABD421C}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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|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.ActiveCfg = Release|Any CPU
{05F21D73-FD31-4E77-8D9B-41C86D4D8305}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|Any CPU.Build.0 = 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 {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.ActiveCfg = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Debug|Any CPU.Build.0 = Debug|Any CPU {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x64.Build.0 = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.ActiveCfg = Release|Any CPU {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.ActiveCfg = Release|Any CPU
{785380E0-CEB9-4C34-82E5-60D0E33E848E}.Release|Any CPU.Build.0 = Release|Any CPU {229C2FB9-6AD6-4A5D-B83A-D1146573D6F9}.Release|x86.Build.0 = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x64.ActiveCfg = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x64.Build.0 = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x86.ActiveCfg = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Debug|x86.Build.0 = Debug|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|Any CPU.Build.0 = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x64.ActiveCfg = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x64.Build.0 = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x86.ActiveCfg = Release|Any CPU
{24547EE4-2EAA-4A6C-AD94-1117C038D8CD}.Release|x86.Build.0 = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x64.ActiveCfg = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x64.Build.0 = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x86.ActiveCfg = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Debug|x86.Build.0 = Debug|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|Any CPU.Build.0 = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x64.ActiveCfg = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x64.Build.0 = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x86.ActiveCfg = Release|Any CPU
{6DD89774-D86B-47E9-B982-2794BD95616A}.Release|x86.Build.0 = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x64.ActiveCfg = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x64.Build.0 = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x86.ActiveCfg = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Debug|x86.Build.0 = Debug|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|Any CPU.Build.0 = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x64.ActiveCfg = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x64.Build.0 = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x86.ActiveCfg = Release|Any CPU
{9BD571FA-52D8-430D-8843-FEB6EABD421C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace ExportDXF.ApiClient
{
public class FabWorksApiClient : IFabWorksApiClient
{
private readonly HttpClient _http;
public FabWorksApiClient(HttpClient httpClient)
{
_http = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<ApiExportDetail> CreateExportAsync(string drawingNumber, string equipmentNo, string drawingNo, string sourceFilePath, string outputFolder, string title = null)
{
var request = new ApiCreateExportRequest
{
DrawingNumber = drawingNumber,
Title = title,
EquipmentNo = equipmentNo,
DrawingNo = drawingNo,
SourceFilePath = sourceFilePath,
OutputFolder = outputFolder
};
var response = await _http.PostAsJsonAsync("api/exports", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiExportDetail>();
}
public async Task<ApiExportDetail> GetExportBySourceFileAsync(string filePath)
{
var response = await _http.GetAsync($"api/exports/by-source?path={Uri.EscapeDataString(filePath)}");
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiExportDetail>();
}
public async Task<List<string>> GetDrawingNumbersAsync()
{
var response = await _http.GetAsync("api/exports/drawing-numbers");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<string>>();
}
public async Task<List<string>> GetEquipmentNumbersAsync()
{
var response = await _http.GetAsync("api/exports/equipment-numbers");
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<string>>();
}
public async Task<List<string>> GetDrawingNumbersByEquipmentAsync(string equipmentNo = null)
{
var url = "api/exports/drawing-numbers-by-equipment";
if (!string.IsNullOrEmpty(equipmentNo))
url += $"?equipmentNo={Uri.EscapeDataString(equipmentNo)}";
var response = await _http.GetAsync(url);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<string>>();
}
public async Task<string> GetNextItemNumberAsync(string drawingNumber)
{
var response = await _http.GetAsync($"api/exports/next-item-number?drawingNumber={Uri.EscapeDataString(drawingNumber)}");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task UpdatePdfHashAsync(int exportId, string pdfContentHash)
{
var request = new ApiUpdatePdfHashRequest { PdfContentHash = pdfContentHash };
var response = await _http.PatchAsJsonAsync($"api/exports/{exportId}/pdf-hash", request);
response.EnsureSuccessStatusCode();
}
public async Task<string> GetPreviousPdfHashAsync(string drawingNumber, int? excludeId = null)
{
var url = $"api/exports/previous-pdf-hash?drawingNumber={Uri.EscapeDataString(drawingNumber)}";
if (excludeId.HasValue)
url += $"&excludeId={excludeId.Value}";
var response = await _http.GetAsync(url);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
public async Task<ApiBomItem> FindExistingBomItemAsync(int exportId, string partName, string configurationName)
{
var url = $"api/exports/{exportId}/bom-items/find?partName={Uri.EscapeDataString(partName ?? "")}&configurationName={Uri.EscapeDataString(configurationName ?? "")}";
var response = await _http.GetAsync(url);
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiBomItem>();
}
public async Task<ApiBomItem> CreateBomItemAsync(int exportId, ApiBomItem bomItem)
{
var response = await _http.PostAsJsonAsync($"api/exports/{exportId}/bom-items", bomItem);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiBomItem>();
}
public async Task<ApiCutTemplate> GetPreviousCutTemplateAsync(string drawingNumber, string itemNo)
{
var response = await _http.GetAsync($"api/exports/previous-cut-template?drawingNumber={Uri.EscapeDataString(drawingNumber)}&itemNo={Uri.EscapeDataString(itemNo)}");
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiCutTemplate>();
}
public async Task<ApiFileUploadResponse> UploadDxfAsync(string localFilePath, string equipment, string drawingNo, string itemNo, string contentHash)
{
using var content = new MultipartFormDataContent();
using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read);
var fileContent = new StreamContent(fileStream);
content.Add(fileContent, "file", Path.GetFileName(localFilePath));
content.Add(new StringContent(equipment ?? ""), "equipment");
content.Add(new StringContent(drawingNo ?? ""), "drawingNo");
content.Add(new StringContent(itemNo ?? ""), "itemNo");
content.Add(new StringContent(contentHash ?? ""), "contentHash");
var response = await _http.PostAsync("api/files/dxf", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiFileUploadResponse>();
}
public async Task<ApiFileUploadResponse> UploadPdfAsync(string localFilePath, string equipment, string drawingNo, string contentHash, int? exportRecordId = null)
{
using var content = new MultipartFormDataContent();
using var fileStream = new FileStream(localFilePath, FileMode.Open, FileAccess.Read);
var fileContent = new StreamContent(fileStream);
content.Add(fileContent, "file", Path.GetFileName(localFilePath));
content.Add(new StringContent(equipment ?? ""), "equipment");
content.Add(new StringContent(drawingNo ?? ""), "drawingNo");
content.Add(new StringContent(contentHash ?? ""), "contentHash");
if (exportRecordId.HasValue)
content.Add(new StringContent(exportRecordId.Value.ToString()), "exportRecordId");
var response = await _http.PostAsync("api/files/pdf", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ApiFileUploadResponse>();
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
namespace ExportDXF.ApiClient
{
public class ApiExportDetail
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public List<ApiBomItem> BomItems { get; set; } = new();
}
public class ApiBomItem
{
public int ID { get; set; }
public string 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 ApiCutTemplate CutTemplate { get; set; }
public ApiFormProgram FormProgram { get; set; }
}
public class ApiCutTemplate
{
public int Id { get; set; }
public string DxfFilePath { get; set; }
public string ContentHash { get; set; }
public int Revision { get; set; }
public double? Thickness { get; set; }
public double? KFactor { get; set; }
public double? DefaultBendRadius { get; set; }
}
public class ApiFormProgram
{
public int Id { get; set; }
public string ProgramFilePath { get; set; }
public string ContentHash { get; set; }
public string ProgramName { get; set; }
public double? Thickness { get; set; }
public string MaterialType { get; set; }
public double? KFactor { get; set; }
public int BendCount { get; set; }
public string UpperToolNames { get; set; }
public string LowerToolNames { get; set; }
public string SetupNotes { get; set; }
}
public class ApiCreateExportRequest
{
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
}
public class ApiUpdatePdfHashRequest
{
public string PdfContentHash { get; set; }
}
public class ApiFileUploadResponse
{
public string StoredFilePath { get; set; }
public string ContentHash { get; set; }
public string FileName { get; set; }
public bool WasUnchanged { get; set; }
public bool IsNewFile { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ExportDXF.ApiClient
{
public interface IFabWorksApiClient
{
Task<ApiExportDetail> CreateExportAsync(string drawingNumber, string equipmentNo, string drawingNo, string sourceFilePath, string outputFolder, string title = null);
Task<ApiExportDetail> GetExportBySourceFileAsync(string filePath);
Task<List<string>> GetDrawingNumbersAsync();
Task<List<string>> GetEquipmentNumbersAsync();
Task<List<string>> GetDrawingNumbersByEquipmentAsync(string equipmentNo = null);
Task<string> GetNextItemNumberAsync(string drawingNumber);
Task UpdatePdfHashAsync(int exportId, string pdfContentHash);
Task<string> GetPreviousPdfHashAsync(string drawingNumber, int? excludeId = null);
Task<ApiBomItem> FindExistingBomItemAsync(int exportId, string partName, string configurationName);
Task<ApiBomItem> CreateBomItemAsync(int exportId, ApiBomItem bomItem);
Task<ApiCutTemplate> GetPreviousCutTemplateAsync(string drawingNumber, string itemNo);
Task<ApiFileUploadResponse> UploadDxfAsync(string localFilePath, string equipment, string drawingNo, string itemNo, string contentHash);
Task<ApiFileUploadResponse> UploadPdfAsync(string localFilePath, string equipment, string drawingNo, string contentHash, int? exportRecordId = null);
}
}

View File

@@ -1,10 +1,12 @@
using System.Text.RegularExpressions; using System.IO;
using System.Text.RegularExpressions;
namespace ExportDXF namespace ExportDXF
{ {
public class DrawingInfo 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 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; } public string EquipmentNo { get; set; }
@@ -12,8 +14,39 @@ namespace ExportDXF
public string Source { get; set; } public string Source { get; set; }
/// <summary>
/// The descriptive text after the equipment/drawing number (e.g. "Prox switch bracket for drive").
/// </summary>
public string Description
{
get
{
if (string.IsNullOrEmpty(Source) || string.IsNullOrEmpty(EquipmentNo))
return null;
// Strip equipment number (and optional drawing number) from the source to get the description
var prefix = string.IsNullOrEmpty(DrawingNo)
? EquipmentNo
: EquipmentNo + " " + DrawingNo;
var desc = Source;
if (desc.StartsWith(prefix, System.StringComparison.OrdinalIgnoreCase))
desc = desc.Substring(prefix.Length);
// Remove file extension (e.g. ".SLDPRT")
var ext = Path.GetExtension(desc);
if (!string.IsNullOrEmpty(ext))
desc = desc.Substring(0, desc.Length - ext.Length);
desc = desc.Trim();
return string.IsNullOrEmpty(desc) ? null : desc;
}
}
public override string ToString() public override string ToString()
{ {
if (string.IsNullOrEmpty(DrawingNo))
return EquipmentNo ?? string.Empty;
return $"{EquipmentNo} {DrawingNo}"; return $"{EquipmentNo} {DrawingNo}";
} }
@@ -35,7 +68,21 @@ namespace ExportDXF
var match = drawingFormatRegex.Match(input); var match = drawingFormatRegex.Match(input);
if (match.Success == false) 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; return null;
}
var dwg = new DrawingInfo(); var dwg = new DrawingInfo();
@@ -46,4 +93,4 @@ namespace ExportDXF
return dwg; return dwg;
} }
} }
} }

View File

@@ -14,11 +14,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" /> <PackageReference Include="PDFtoImage" Version="4.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11"> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" /> <PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
</ItemGroup> </ItemGroup>

View File

@@ -42,6 +42,8 @@ namespace ExportDXF.Forms
label1 = new System.Windows.Forms.Label(); label1 = new System.Windows.Forms.Label();
label2 = new System.Windows.Forms.Label(); label2 = new System.Windows.Forms.Label();
drawingNoBox = new System.Windows.Forms.ComboBox(); drawingNoBox = new System.Windows.Forms.ComboBox();
titleLabel = new System.Windows.Forms.Label();
titleBox = new System.Windows.Forms.TextBox();
mainTabControl.SuspendLayout(); mainTabControl.SuspendLayout();
logEventsTab.SuspendLayout(); logEventsTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit(); ((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit();
@@ -54,10 +56,10 @@ namespace ExportDXF.Forms
// runButton // runButton
// //
runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
runButton.Location = new System.Drawing.Point(514, 13); runButton.Location = new System.Drawing.Point(508, 12);
runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
runButton.Name = "runButton"; runButton.Name = "runButton";
runButton.Size = new System.Drawing.Size(100, 30); runButton.Size = new System.Drawing.Size(65, 87);
runButton.TabIndex = 11; runButton.TabIndex = 11;
runButton.Text = "Start"; runButton.Text = "Start";
runButton.UseVisualStyleBackColor = true; runButton.UseVisualStyleBackColor = true;
@@ -66,7 +68,7 @@ namespace ExportDXF.Forms
// label3 // label3
// //
label3.AutoSize = true; label3.AutoSize = true;
label3.Location = new System.Drawing.Point(26, 46); label3.Location = new System.Drawing.Point(26, 77);
label3.Name = "label3"; label3.Name = "label3";
label3.Size = new System.Drawing.Size(105, 17); label3.Size = new System.Drawing.Size(105, 17);
label3.TabIndex = 2; label3.TabIndex = 2;
@@ -76,7 +78,7 @@ namespace ExportDXF.Forms
// //
viewFlipDeciderBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; viewFlipDeciderBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
viewFlipDeciderBox.FormattingEnabled = true; viewFlipDeciderBox.FormattingEnabled = true;
viewFlipDeciderBox.Location = new System.Drawing.Point(137, 43); viewFlipDeciderBox.Location = new System.Drawing.Point(137, 74);
viewFlipDeciderBox.Name = "viewFlipDeciderBox"; viewFlipDeciderBox.Name = "viewFlipDeciderBox";
viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25); viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25);
viewFlipDeciderBox.TabIndex = 3; viewFlipDeciderBox.TabIndex = 3;
@@ -87,11 +89,11 @@ namespace ExportDXF.Forms
mainTabControl.Controls.Add(logEventsTab); mainTabControl.Controls.Add(logEventsTab);
mainTabControl.Controls.Add(bomTab); mainTabControl.Controls.Add(bomTab);
mainTabControl.Controls.Add(cutTemplatesTab); mainTabControl.Controls.Add(cutTemplatesTab);
mainTabControl.Location = new System.Drawing.Point(15, 74); mainTabControl.Location = new System.Drawing.Point(15, 105);
mainTabControl.Name = "mainTabControl"; mainTabControl.Name = "mainTabControl";
mainTabControl.Padding = new System.Drawing.Point(20, 5); mainTabControl.Padding = new System.Drawing.Point(20, 5);
mainTabControl.SelectedIndex = 0; mainTabControl.SelectedIndex = 0;
mainTabControl.Size = new System.Drawing.Size(599, 330); mainTabControl.Size = new System.Drawing.Size(910, 492);
mainTabControl.TabIndex = 12; mainTabControl.TabIndex = 12;
// //
// logEventsTab // logEventsTab
@@ -100,7 +102,7 @@ namespace ExportDXF.Forms
logEventsTab.Location = new System.Drawing.Point(4, 30); logEventsTab.Location = new System.Drawing.Point(4, 30);
logEventsTab.Name = "logEventsTab"; logEventsTab.Name = "logEventsTab";
logEventsTab.Padding = new System.Windows.Forms.Padding(3); logEventsTab.Padding = new System.Windows.Forms.Padding(3);
logEventsTab.Size = new System.Drawing.Size(591, 296); logEventsTab.Size = new System.Drawing.Size(902, 458);
logEventsTab.TabIndex = 0; logEventsTab.TabIndex = 0;
logEventsTab.Text = "Log Events"; logEventsTab.Text = "Log Events";
logEventsTab.UseVisualStyleBackColor = true; logEventsTab.UseVisualStyleBackColor = true;
@@ -112,7 +114,7 @@ namespace ExportDXF.Forms
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke; logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
logEventsDataGrid.Location = new System.Drawing.Point(6, 6); logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
logEventsDataGrid.Name = "logEventsDataGrid"; logEventsDataGrid.Name = "logEventsDataGrid";
logEventsDataGrid.Size = new System.Drawing.Size(579, 282); logEventsDataGrid.Size = new System.Drawing.Size(890, 444);
logEventsDataGrid.TabIndex = 0; logEventsDataGrid.TabIndex = 0;
// //
// bomTab // bomTab
@@ -121,7 +123,7 @@ namespace ExportDXF.Forms
bomTab.Location = new System.Drawing.Point(4, 30); bomTab.Location = new System.Drawing.Point(4, 30);
bomTab.Name = "bomTab"; bomTab.Name = "bomTab";
bomTab.Padding = new System.Windows.Forms.Padding(3); bomTab.Padding = new System.Windows.Forms.Padding(3);
bomTab.Size = new System.Drawing.Size(982, 549); bomTab.Size = new System.Drawing.Size(902, 458);
bomTab.TabIndex = 1; bomTab.TabIndex = 1;
bomTab.Text = "Bill Of Materials"; bomTab.Text = "Bill Of Materials";
bomTab.UseVisualStyleBackColor = true; bomTab.UseVisualStyleBackColor = true;
@@ -133,30 +135,30 @@ namespace ExportDXF.Forms
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke; bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
bomDataGrid.Location = new System.Drawing.Point(6, 6); bomDataGrid.Location = new System.Drawing.Point(6, 6);
bomDataGrid.Name = "bomDataGrid"; bomDataGrid.Name = "bomDataGrid";
bomDataGrid.Size = new System.Drawing.Size(970, 535); bomDataGrid.Size = new System.Drawing.Size(890, 444);
bomDataGrid.TabIndex = 1; bomDataGrid.TabIndex = 1;
// //
// cutTemplatesTab // cutTemplatesTab
// //
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid); cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
cutTemplatesTab.Location = new System.Drawing.Point(4, 30); cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
cutTemplatesTab.Name = "cutTemplatesTab"; cutTemplatesTab.Name = "cutTemplatesTab";
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3); cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
cutTemplatesTab.Size = new System.Drawing.Size(982, 549); cutTemplatesTab.Size = new System.Drawing.Size(902, 458);
cutTemplatesTab.TabIndex = 2; cutTemplatesTab.TabIndex = 2;
cutTemplatesTab.Text = "Cut Templates"; cutTemplatesTab.Text = "Cut Templates";
cutTemplatesTab.UseVisualStyleBackColor = true; cutTemplatesTab.UseVisualStyleBackColor = true;
// //
// cutTemplatesDataGrid // cutTemplatesDataGrid
// //
cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; 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.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke; cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6); cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid"; cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
cutTemplatesDataGrid.Size = new System.Drawing.Size(970, 535); cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 447);
cutTemplatesDataGrid.TabIndex = 2; cutTemplatesDataGrid.TabIndex = 2;
// //
// equipmentBox // equipmentBox
// //
equipmentBox.FormattingEnabled = true; equipmentBox.FormattingEnabled = true;
@@ -191,10 +193,28 @@ namespace ExportDXF.Forms
drawingNoBox.Size = new System.Drawing.Size(119, 25); drawingNoBox.Size = new System.Drawing.Size(119, 25);
drawingNoBox.TabIndex = 13; drawingNoBox.TabIndex = 13;
// //
// titleLabel
//
titleLabel.AutoSize = true;
titleLabel.Location = new System.Drawing.Point(99, 46);
titleLabel.Name = "titleLabel";
titleLabel.Size = new System.Drawing.Size(32, 17);
titleLabel.TabIndex = 14;
titleLabel.Text = "Title";
//
// titleBox
//
titleBox.Location = new System.Drawing.Point(137, 43);
titleBox.Name = "titleBox";
titleBox.Size = new System.Drawing.Size(365, 25);
titleBox.TabIndex = 15;
//
// MainForm // MainForm
// //
AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
ClientSize = new System.Drawing.Size(626, 416); ClientSize = new System.Drawing.Size(937, 609);
Controls.Add(titleBox);
Controls.Add(titleLabel);
Controls.Add(drawingNoBox); Controls.Add(drawingNoBox);
Controls.Add(equipmentBox); Controls.Add(equipmentBox);
Controls.Add(mainTabControl); Controls.Add(mainTabControl);
@@ -237,5 +257,7 @@ namespace ExportDXF.Forms
private System.Windows.Forms.Label label1; private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label2;
private System.Windows.Forms.ComboBox drawingNoBox; private System.Windows.Forms.ComboBox drawingNoBox;
private System.Windows.Forms.Label titleLabel;
private System.Windows.Forms.TextBox titleBox;
} }
} }

View File

@@ -1,4 +1,4 @@
using ExportDXF.Data; using ExportDXF.ApiClient;
using ExportDXF.Extensions; using ExportDXF.Extensions;
using ExportDXF.Models; using ExportDXF.Models;
using ExportDXF.Services; using ExportDXF.Services;
@@ -18,15 +18,14 @@ namespace ExportDXF.Forms
{ {
private readonly ISolidWorksService _solidWorksService; private readonly ISolidWorksService _solidWorksService;
private readonly IDxfExportService _exportService; private readonly IDxfExportService _exportService;
private readonly IFileExportService _fileExportService; private readonly IFabWorksApiClient _apiClient;
private readonly Func<ExportDxfDbContext> _dbContextFactory;
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
private readonly BindingList<LogEvent> _logEvents; private readonly BindingList<LogEvent> _logEvents;
private readonly BindingList<BomItem> _bomItems; private readonly BindingList<BomItem> _bomItems;
private readonly BindingList<CutTemplate> _cutTemplates; private readonly BindingList<CutTemplate> _cutTemplates;
private List<DrawingInfo> _allDrawings; private List<DrawingInfo> _allDrawings;
public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFileExportService fileExportService, Func<ExportDxfDbContext> dbContextFactory = null) public MainForm(ISolidWorksService solidWorksService, IDxfExportService exportService, IFabWorksApiClient apiClient)
{ {
InitializeComponent(); InitializeComponent();
_solidWorksService = solidWorksService ?? _solidWorksService = solidWorksService ??
@@ -34,9 +33,8 @@ namespace ExportDXF.Forms
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged; _solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
_exportService = exportService ?? _exportService = exportService ??
throw new ArgumentNullException(nameof(exportService)); throw new ArgumentNullException(nameof(exportService));
_fileExportService = fileExportService ?? _apiClient = apiClient ??
throw new ArgumentNullException(nameof(fileExportService)); throw new ArgumentNullException(nameof(apiClient));
_dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext());
_logEvents = new BindingList<LogEvent>(); _logEvents = new BindingList<LogEvent>();
_bomItems = new BindingList<BomItem>(); _bomItems = new BindingList<BomItem>();
_cutTemplates = new BindingList<CutTemplate>(); _cutTemplates = new BindingList<CutTemplate>();
@@ -70,9 +68,10 @@ namespace ExportDXF.Forms
LogMessage("Connecting to SolidWorks, this may take a minute..."); LogMessage("Connecting to SolidWorks, this may take a minute...");
await _solidWorksService.ConnectAsync(); await _solidWorksService.ConnectAsync();
_solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged; _solidWorksService.ActiveDocumentChanged += OnActiveDocumentChanged;
LogMessage($"Output folder: {_fileExportService.OutputFolder}"); LogMessage("Files will be uploaded to FabWorks API");
await LoadDrawingDropdownsAsync();
LogMessage("Ready"); LogMessage("Ready");
UpdateActiveDocumentDisplay(); await UpdateActiveDocumentDisplayAsync();
runButton.Enabled = true; runButton.Enabled = true;
} }
catch (Exception ex) catch (Exception ex)
@@ -274,74 +273,60 @@ namespace ExportDXF.Forms
} }
private void InitializeDrawingDropdowns() private void InitializeDrawingDropdowns()
{
// Wire up event handler; actual data loading happens in LoadDrawingDropdownsAsync
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
}
private async Task LoadDrawingDropdownsAsync()
{ {
try try
{ {
using (var db = _dbContextFactory()) var equipmentNumbers = await _apiClient.GetEquipmentNumbersAsync();
equipmentBox.Items.Clear();
equipmentBox.Items.Add("");
foreach (var eq in equipmentNumbers)
{ {
// Get all drawing numbers from the database equipmentBox.Items.Add(eq);
var drawingNumbers = db.ExportRecords
.Select(r => r.DrawingNumber)
.Where(d => !string.IsNullOrEmpty(d))
.Distinct()
.ToList();
// 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;
} }
// Clear _allDrawings — drawing list is now loaded on equipment selection
_allDrawings = new List<DrawingInfo>();
await UpdateDrawingDropdownAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
// Database might not exist yet - that's OK // API might not be available yet - that's OK
System.Diagnostics.Debug.WriteLine($"Failed to load drawings from database: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"Failed to load equipment numbers from API: {ex.Message}");
} }
} }
private void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e) private async void EquipmentBox_SelectedIndexChanged(object sender, EventArgs e)
{ {
UpdateDrawingDropdown(); await UpdateDrawingDropdownAsync();
} }
private void UpdateDrawingDropdown() private async Task UpdateDrawingDropdownAsync()
{ {
var selectedEquipment = equipmentBox.SelectedItem?.ToString(); var selectedEquipment = equipmentBox.SelectedItem?.ToString();
var filteredDrawings = string.IsNullOrEmpty(selectedEquipment)
? _allDrawings
: _allDrawings.Where(d => d.EquipmentNo == selectedEquipment).ToList();
drawingNoBox.Items.Clear(); drawingNoBox.Items.Clear();
drawingNoBox.Items.Add(""); // Empty option drawingNoBox.Items.Add("");
foreach (var drawing in filteredDrawings)
try
{ {
drawingNoBox.Items.Add(drawing.DrawingNo); var drawingNumbers = await _apiClient.GetDrawingNumbersByEquipmentAsync(
string.IsNullOrEmpty(selectedEquipment) ? null : selectedEquipment);
foreach (var dn in drawingNumbers)
{
drawingNoBox.Items.Add(dn);
}
}
catch
{
// API might not be available
} }
if (drawingNoBox.Items.Count > 0) if (drawingNoBox.Items.Count > 0)
@@ -380,16 +365,21 @@ namespace ExportDXF.Forms
// Use equipment/drawing values from the UI dropdowns // Use equipment/drawing values from the UI dropdowns
var equipment = equipmentBox.Text?.Trim(); var equipment = equipmentBox.Text?.Trim();
var drawingNo = drawingNoBox.Text?.Trim(); var drawingNo = drawingNoBox.Text?.Trim();
var filePrefix = !string.IsNullOrEmpty(equipment) && !string.IsNullOrEmpty(drawingNo) var filePrefix = !string.IsNullOrEmpty(equipment)
? $"{equipment} {drawingNo}" ? (!string.IsNullOrEmpty(drawingNo) ? $"{equipment} {drawingNo}" : equipment)
: activeDoc.Title; : activeDoc.Title;
var viewFlipDecider = GetSelectedViewFlipDecider(); var viewFlipDecider = GetSelectedViewFlipDecider();
var title = titleBox.Text?.Trim();
var exportContext = new ExportContext var exportContext = new ExportContext
{ {
ActiveDocument = activeDoc, ActiveDocument = activeDoc,
ViewFlipDecider = viewFlipDecider, ViewFlipDecider = viewFlipDecider,
FilePrefix = filePrefix, FilePrefix = filePrefix,
Equipment = equipment,
DrawingNo = drawingNo,
Title = string.IsNullOrEmpty(title) ? null : title,
EquipmentId = null, EquipmentId = null,
CancellationToken = token, CancellationToken = token,
ProgressCallback = (msg, level, file) => LogMessage(msg, level, file), ProgressCallback = (msg, level, file) => LogMessage(msg, level, file),
@@ -401,11 +391,11 @@ namespace ExportDXF.Forms
_cutTemplates.Clear(); _cutTemplates.Clear();
LogMessage($"Started at {DateTime.Now:t}"); LogMessage($"Started at {DateTime.Now:t}");
LogMessage($"Exporting to: {_fileExportService.OutputFolder}"); LogMessage("Exporting (files will be uploaded to API)...");
_solidWorksService.SetCommandInProgress(true); _solidWorksService.SetCommandInProgress(true);
await Task.Run(() => _exportService.Export(exportContext), token); await Task.Run(async () => await _exportService.ExportAsync(exportContext), token);
LogMessage("Done."); LogMessage("Done.");
} }
@@ -452,32 +442,61 @@ namespace ExportDXF.Forms
runButton.Enabled = true; runButton.Enabled = true;
} }
private void OnActiveDocumentChanged(object sender, EventArgs e) private async void OnActiveDocumentChanged(object sender, EventArgs e)
{ {
if (InvokeRequired) if (InvokeRequired)
{ {
Invoke(new Action(() => OnActiveDocumentChanged(sender, e))); Invoke(new Action(async () => await UpdateActiveDocumentDisplayAsync()));
return; return;
} }
UpdateActiveDocumentDisplay(); await UpdateActiveDocumentDisplayAsync();
} }
private void UpdateActiveDocumentDisplay() private async Task UpdateActiveDocumentDisplayAsync()
{ {
var activeDoc = _solidWorksService.GetActiveDocument(); var activeDoc = _solidWorksService.GetActiveDocument();
var docTitle = activeDoc?.Title ?? "No Document Open"; var docTitle = activeDoc?.Title ?? "No Document Open";
this.Text = $"ExportDXF - {docTitle}"; this.Text = $"ExportDXF - {docTitle}";
// Parse the file name and fill Equipment/Drawing dropdowns if (activeDoc == null)
if (activeDoc != null) return;
// Try API first: look up the most recent export for this file path
DrawingInfo drawingInfo = null;
if (!string.IsNullOrEmpty(activeDoc.FilePath))
{ {
var drawingInfo = DrawingInfo.Parse(activeDoc.Title); drawingInfo = await LookupDrawingInfoFromHistoryAsync(activeDoc.FilePath);
if (drawingInfo != null) }
// Fall back to parsing the document title
if (drawingInfo == null)
{
drawingInfo = DrawingInfo.Parse(activeDoc.Title);
}
if (drawingInfo != null)
{
// Detach event to prevent async race when setting equipment
equipmentBox.SelectedIndexChanged -= EquipmentBox_SelectedIndexChanged;
if (!string.IsNullOrEmpty(drawingInfo.EquipmentNo))
{ {
if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo)) if (!equipmentBox.Items.Contains(drawingInfo.EquipmentNo))
equipmentBox.Items.Add(drawingInfo.EquipmentNo); equipmentBox.Items.Add(drawingInfo.EquipmentNo);
equipmentBox.Text = drawingInfo.EquipmentNo; equipmentBox.Text = drawingInfo.EquipmentNo;
}
if (string.IsNullOrEmpty(titleBox.Text) && !string.IsNullOrEmpty(drawingInfo.Description))
titleBox.Text = drawingInfo.Description;
// Load drawings for the selected equipment, then set drawing number
await UpdateDrawingDropdownAsync();
equipmentBox.SelectedIndexChanged += EquipmentBox_SelectedIndexChanged;
if (!string.IsNullOrEmpty(drawingInfo.DrawingNo))
{
if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo)) if (!drawingNoBox.Items.Contains(drawingInfo.DrawingNo))
drawingNoBox.Items.Add(drawingInfo.DrawingNo); drawingNoBox.Items.Add(drawingInfo.DrawingNo);
drawingNoBox.Text = drawingInfo.DrawingNo; drawingNoBox.Text = drawingInfo.DrawingNo;
@@ -485,6 +504,26 @@ namespace ExportDXF.Forms
} }
} }
private async Task<DrawingInfo> LookupDrawingInfoFromHistoryAsync(string filePath)
{
try
{
var dto = await _apiClient.GetExportBySourceFileAsync(filePath);
if (dto != null && !string.IsNullOrEmpty(dto.DrawingNumber))
{
if (!string.IsNullOrEmpty(dto.Title))
titleBox.Text = dto.Title;
return DrawingInfo.Parse(dto.DrawingNumber);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to look up drawing info from API: {ex.Message}");
}
return null;
}
private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null) private void LogMessage(string message, LogLevel level = LogLevel.Info, string file = null)
{ {
AddLogEvent(level, LogAction.Start, message, part: file); AddLogEvent(level, LogAction.Start, message, part: file);

View File

@@ -1,114 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ExportDXF.Migrations
{
/// <inheritdoc />
public partial class ExtractCutTemplate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ContentHash",
table: "BomItems");
migrationBuilder.DropColumn(
name: "CutTemplateName",
table: "BomItems");
migrationBuilder.DropColumn(
name: "DefaultBendRadius",
table: "BomItems");
migrationBuilder.DropColumn(
name: "DxfFilePath",
table: "BomItems");
migrationBuilder.DropColumn(
name: "KFactor",
table: "BomItems");
migrationBuilder.DropColumn(
name: "Thickness",
table: "BomItems");
migrationBuilder.CreateTable(
name: "CutTemplates",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DxfFilePath = table.Column<string>(type: "nvarchar(max)", nullable: true),
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
CutTemplateName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Thickness = table.Column<double>(type: "float", nullable: true),
KFactor = table.Column<double>(type: "float", nullable: true),
DefaultBendRadius = table.Column<double>(type: "float", nullable: true),
BomItemId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CutTemplates", x => x.Id);
table.ForeignKey(
name: "FK_CutTemplates_BomItems_BomItemId",
column: x => x.BomItemId,
principalTable: "BomItems",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CutTemplates_BomItemId",
table: "CutTemplates",
column: "BomItemId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CutTemplates");
migrationBuilder.AddColumn<string>(
name: "ContentHash",
table: "BomItems",
type: "nvarchar(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CutTemplateName",
table: "BomItems",
type: "nvarchar(100)",
maxLength: 100,
nullable: true);
migrationBuilder.AddColumn<double>(
name: "DefaultBendRadius",
table: "BomItems",
type: "float",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "DxfFilePath",
table: "BomItems",
type: "nvarchar(max)",
nullable: true);
migrationBuilder.AddColumn<double>(
name: "KFactor",
table: "BomItems",
type: "float",
nullable: true);
migrationBuilder.AddColumn<double>(
name: "Thickness",
table: "BomItems",
type: "float",
nullable: true);
}
}
}

View File

@@ -32,6 +32,21 @@ namespace ExportDXF.Services
/// </summary> /// </summary>
public string FilePrefix { get; set; } public string FilePrefix { get; set; }
/// <summary>
/// Equipment number from the UI (e.g., "5028").
/// </summary>
public string Equipment { get; set; }
/// <summary>
/// Drawing number from the UI (e.g., "A02", "Misc").
/// </summary>
public string DrawingNo { get; set; }
/// <summary>
/// Optional title/label for the export.
/// </summary>
public string Title { get; set; }
/// <summary> /// <summary>
/// Selected Equipment ID for API operations (optional). /// Selected Equipment ID for API operations (optional).
/// </summary> /// </summary>

View File

@@ -7,10 +7,13 @@ namespace ExportDXF.Models
{ {
public int Id { get; set; } public int Id { get; set; }
public string DrawingNumber { get; set; } public string DrawingNumber { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; } public string SourceFilePath { get; set; }
public string OutputFolder { get; set; } public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; } public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; } public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>(); public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
} }

View File

@@ -61,5 +61,16 @@ namespace ExportDXF.Services
/// The SolidWorks component reference. /// The SolidWorks component reference.
/// </summary> /// </summary>
public Component2 Component { get; set; } 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; }
} }
} }

View File

@@ -1,7 +1,12 @@
using ExportDXF.ApiClient;
using ExportDXF.Forms; using ExportDXF.Forms;
using ExportDXF.Services; using ExportDXF.Services;
using System; using System;
using System.Configuration; using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
namespace ExportDXF namespace ExportDXF
@@ -28,11 +33,11 @@ namespace ExportDXF
/// </summary> /// </summary>
public class ServiceContainer public class ServiceContainer
{ {
private readonly string _outputFolder; private readonly string _apiBaseUrl;
public ServiceContainer() public ServiceContainer()
{ {
_outputFolder = ConfigurationManager.AppSettings["ExportOutputFolder"] ?? @"C:\ExportDXF\Output"; _apiBaseUrl = ConfigurationManager.AppSettings["FabWorksApiUrl"] ?? "http://localhost:5206";
} }
public MainForm ResolveMainForm() public MainForm ResolveMainForm()
@@ -41,16 +46,71 @@ namespace ExportDXF
var bomExtractor = new BomExtractor(); var bomExtractor = new BomExtractor();
var partExporter = new PartExporter(); var partExporter = new PartExporter();
var drawingExporter = new DrawingExporter(); var drawingExporter = new DrawingExporter();
var fileExportService = new FileExportService(_outputFolder);
EnsureApiRunning();
var httpClient = new HttpClient
{
BaseAddress = new Uri(_apiBaseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
var apiClient = new FabWorksApiClient(httpClient);
var exportService = new DxfExportService( var exportService = new DxfExportService(
solidWorksService, solidWorksService,
bomExtractor, bomExtractor,
partExporter, partExporter,
drawingExporter, drawingExporter,
fileExportService); apiClient);
return new MainForm(solidWorksService, exportService, fileExportService); return new MainForm(solidWorksService, exportService, apiClient);
}
private void EnsureApiRunning()
{
// Check if API is already responding
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
{
try
{
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
if (response.IsSuccessStatusCode)
return; // already running
}
catch { }
}
// Find the API executable relative to this assembly
var exeDir = AppContext.BaseDirectory;
var apiExe = Path.GetFullPath(Path.Combine(exeDir, @"..\..\..\FabWorks.Api\bin\Debug\net8.0\FabWorks.Api.exe"));
if (!File.Exists(apiExe))
return; // can't find it, skip
var startInfo = new ProcessStartInfo
{
FileName = apiExe,
WorkingDirectory = Path.GetDirectoryName(apiExe),
UseShellExecute = false,
CreateNoWindow = true
};
Process.Start(startInfo);
// Wait up to 10 seconds for API to become ready
using (var probe = new HttpClient { Timeout = TimeSpan.FromSeconds(2) })
{
for (int i = 0; i < 20; i++)
{
Thread.Sleep(500);
try
{
var response = probe.GetAsync(_apiBaseUrl + "/api/exports?take=1").Result;
if (response.IsSuccessStatusCode)
return;
}
catch { }
}
}
} }
} }
} }

View File

@@ -1,15 +1,15 @@
using ExportDXF.Data; using ExportDXF.ApiClient;
using ExportDXF.Extensions; using ExportDXF.Extensions;
using ExportDXF.ItemExtractors; using ExportDXF.ItemExtractors;
using ExportDXF.Models; using ExportDXF.Models;
using ExportDXF.Utilities; using ExportDXF.Utilities;
using ExportDXF; using ExportDXF;
using Microsoft.EntityFrameworkCore;
using SolidWorks.Interop.sldworks; using SolidWorks.Interop.sldworks;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
namespace ExportDXF.Services namespace ExportDXF.Services
{ {
@@ -19,11 +19,12 @@ namespace ExportDXF.Services
/// Exports the document specified in the context to DXF format. /// Exports the document specified in the context to DXF format.
/// </summary> /// </summary>
/// <param name="context">The export context containing all necessary information.</param> /// <param name="context">The export context containing all necessary information.</param>
void Export(ExportContext context); Task ExportAsync(ExportContext context);
} }
/// <summary> /// <summary>
/// Service responsible for orchestrating the export of SolidWorks documents to DXF format. /// Service responsible for orchestrating the export of SolidWorks documents to DXF format.
/// Files are generated locally in a temp directory, then uploaded to the API for storage and versioning.
/// </summary> /// </summary>
public class DxfExportService : IDxfExportService public class DxfExportService : IDxfExportService
{ {
@@ -31,29 +32,26 @@ namespace ExportDXF.Services
private readonly IBomExtractor _bomExtractor; private readonly IBomExtractor _bomExtractor;
private readonly IPartExporter _partExporter; private readonly IPartExporter _partExporter;
private readonly IDrawingExporter _drawingExporter; private readonly IDrawingExporter _drawingExporter;
private readonly IFileExportService _fileExportService; private readonly IFabWorksApiClient _apiClient;
private readonly Func<ExportDxfDbContext> _dbContextFactory;
public DxfExportService( public DxfExportService(
ISolidWorksService solidWorksService, ISolidWorksService solidWorksService,
IBomExtractor bomExtractor, IBomExtractor bomExtractor,
IPartExporter partExporter, IPartExporter partExporter,
IDrawingExporter drawingExporter, IDrawingExporter drawingExporter,
IFileExportService fileExportService, IFabWorksApiClient apiClient)
Func<ExportDxfDbContext> dbContextFactory = null)
{ {
_solidWorksService = solidWorksService ?? throw new ArgumentNullException(nameof(solidWorksService)); _solidWorksService = solidWorksService ?? throw new ArgumentNullException(nameof(solidWorksService));
_bomExtractor = bomExtractor ?? throw new ArgumentNullException(nameof(bomExtractor)); _bomExtractor = bomExtractor ?? throw new ArgumentNullException(nameof(bomExtractor));
_partExporter = partExporter ?? throw new ArgumentNullException(nameof(partExporter)); _partExporter = partExporter ?? throw new ArgumentNullException(nameof(partExporter));
_drawingExporter = drawingExporter ?? throw new ArgumentNullException(nameof(drawingExporter)); _drawingExporter = drawingExporter ?? throw new ArgumentNullException(nameof(drawingExporter));
_fileExportService = fileExportService ?? throw new ArgumentNullException(nameof(fileExportService)); _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
_dbContextFactory = dbContextFactory ?? (() => new ExportDxfDbContext());
} }
/// <summary> /// <summary>
/// Exports the document specified in the context to DXF format. /// Exports the document specified in the context to DXF format.
/// </summary> /// </summary>
public void Export(ExportContext context) public async Task ExportAsync(ExportContext context)
{ {
if (context == null) if (context == null)
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
@@ -62,23 +60,26 @@ namespace ExportDXF.Services
SetupExportContext(context); SetupExportContext(context);
var startTime = DateTime.Now; var startTime = DateTime.Now;
var tempDir = CreateTempWorkDir();
try try
{ {
_solidWorksService.EnableUserControl(false); _solidWorksService.EnableUserControl(false);
var drawingNumber = ParseDrawingNumber(context);
switch (context.ActiveDocument.DocumentType) switch (context.ActiveDocument.DocumentType)
{ {
case DocumentType.Part: case DocumentType.Part:
ExportPart(context); await ExportPartAsync(context, tempDir, drawingNumber);
break; break;
case DocumentType.Assembly: case DocumentType.Assembly:
ExportAssembly(context); await ExportAssemblyAsync(context, tempDir, drawingNumber);
break; break;
case DocumentType.Drawing: case DocumentType.Drawing:
ExportDrawing(context); await ExportDrawingAsync(context, drawingNumber, tempDir);
break; break;
default: default:
@@ -90,6 +91,7 @@ namespace ExportDXF.Services
{ {
CleanupExportContext(context); CleanupExportContext(context);
_solidWorksService.EnableUserControl(true); _solidWorksService.EnableUserControl(true);
CleanupTempDir(tempDir);
var duration = DateTime.Now - startTime; var duration = DateTime.Now - startTime;
LogProgress(context, $"Run time: {duration.ToReadableFormat()}"); LogProgress(context, $"Run time: {duration.ToReadableFormat()}");
@@ -98,7 +100,7 @@ namespace ExportDXF.Services
#region Export Methods by Document Type #region Export Methods by Document Type
private void ExportPart(ExportContext context) private async Task ExportPartAsync(ExportContext context, string tempDir, string drawingNumber)
{ {
LogProgress(context, "Active document is a Part"); LogProgress(context, "Active document is a Part");
@@ -109,11 +111,54 @@ namespace ExportDXF.Services
return; return;
} }
// Export directly to the output folder var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
_partExporter.ExportSinglePart(part, _fileExportService.OutputFolder, context); var item = _partExporter.ExportSinglePart(part, tempDir, context);
if (item != null)
{
// Check if this part+config already has a BOM item for this drawing
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
var bomItem = new BomItem
{
ExportRecordId = exportRecord?.Id ?? 0,
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 ?? ""
};
// Upload DXF to API and get stored path
if (!string.IsNullOrEmpty(item.LocalTempPath))
{
var uploadResult = await UploadDxfAsync(item, context);
if (uploadResult != null)
{
bomItem.CutTemplate = new CutTemplate
{
DxfFilePath = uploadResult.StoredFilePath,
ContentHash = item.ContentHash,
Thickness = item.Thickness > 0 ? item.Thickness : null,
KFactor = item.KFactor > 0 ? item.KFactor : null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
};
}
}
context.BomItemCallback?.Invoke(bomItem);
if (exportRecord != null)
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
}
} }
private void ExportAssembly(ExportContext context) private async Task ExportAssemblyAsync(ExportContext context, string tempDir, string drawingNumber)
{ {
LogProgress(context, "Active document is an Assembly"); LogProgress(context, "Active document is an Assembly");
LogProgress(context, "Fetching components..."); LogProgress(context, "Fetching components...");
@@ -135,14 +180,33 @@ namespace ExportDXF.Services
LogProgress(context, $"Found {items.Count} item(s)."); LogProgress(context, $"Found {items.Count} item(s).");
// Export directly to the output folder var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
ExportItems(items, _fileExportService.OutputFolder, context);
// Check existing BOM items and reuse item numbers, or assign new ones
var nextNum = int.Parse(await GetNextItemNumberAsync(drawingNumber));
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.ItemNo))
{
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
if (existingItemNo != null)
{
item.ItemNo = existingItemNo;
}
else
{
item.ItemNo = nextNum.ToString();
nextNum++;
}
}
}
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
} }
private void ExportDrawing(ExportContext context) private async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir)
{ {
LogProgress(context, "Active document is a Drawing"); LogProgress(context, "Active document is a Drawing");
LogProgress(context, "Finding BOM tables...");
var drawing = context.ActiveDocument.NativeDocument as DrawingDoc; var drawing = context.ActiveDocument.NativeDocument as DrawingDoc;
if (drawing == null) if (drawing == null)
@@ -151,103 +215,115 @@ namespace ExportDXF.Services
return; return;
} }
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback); // Export drawing to PDF in temp dir
if (items == null || items.Count == 0)
{
LogProgress(context, "Error: Bill of materials not found.", LogLevel.Error);
return;
}
LogProgress(context, $"Found {items.Count} component(s)");
// Determine drawing number for file naming
var drawingNumber = ParseDrawingNumber(context);
// Resolve output folder: /{outputDir}/{equipmentNo}/{drawingNo}/ or flat fallback
var drawingOutputFolder = _fileExportService.GetDrawingOutputFolder(drawingNumber);
// Export drawing to PDF
var tempDir = CreateTempWorkDir();
_drawingExporter.ExportToPdf(drawing, tempDir, context); _drawingExporter.ExportToPdf(drawing, tempDir, context);
// Copy PDF to output folder with versioning // Create export record via API
string pdfStashPath = null; var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
string savedPdfPath = null;
// Upload PDF to API with versioning
try try
{ {
var pdfs = Directory.GetFiles(tempDir, "*.pdf"); var pdfs = Directory.GetFiles(tempDir, "*.pdf");
if (pdfs.Length > 0) if (pdfs.Length > 0)
{ {
// Determine the destination path to stash the existing file var pdfTempPath = pdfs[0];
var pdfFileName = !string.IsNullOrEmpty(drawingNumber) var pdfHash = ContentHasher.ComputePdfContentHash(pdfTempPath);
? $"{drawingNumber}.pdf"
: Path.GetFileName(pdfs[0]);
var pdfDestPath = Path.Combine(drawingOutputFolder, pdfFileName);
pdfStashPath = _fileExportService.StashFile(pdfDestPath); var uploadResult = await _apiClient.UploadPdfAsync(
savedPdfPath = _fileExportService.SavePdfFile(pdfs[0], drawingNumber, drawingOutputFolder); pdfTempPath,
context.Equipment,
context.DrawingNo,
pdfHash,
exportRecord?.Id);
if (exportRecord != null)
await _apiClient.UpdatePdfHashAsync(exportRecord.Id, pdfHash);
if (uploadResult != null)
{
if (uploadResult.WasUnchanged)
LogProgress(context, $"PDF unchanged: {uploadResult.FileName}", LogLevel.Info);
else if (uploadResult.IsNewFile)
LogProgress(context, $"Saved PDF: {uploadResult.FileName}", LogLevel.Info);
else
LogProgress(context, $"PDF updated: {uploadResult.FileName}", LogLevel.Info);
}
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
LogProgress(context, $"PDF save error: {ex.Message}", LogLevel.Error); LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error);
} }
// Create export record in database // Extract BOM items from drawing tables
ExportRecord exportRecord = null; LogProgress(context, "Finding BOM tables...");
try var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
if (items != null && items.Count > 0)
{ {
using (var db = _dbContextFactory()) LogProgress(context, $"Found {items.Count} component(s)");
await ExportItemsAsync(items, tempDir, context, exportRecord?.Id);
}
else
{
// No BOM table — fall back to exporting the part referenced by the drawing views
LogProgress(context, "No BOM table found. Checking drawing views for referenced part...");
var (part, configuration) = GetReferencedPartFromViews(drawing);
if (part == null)
{ {
db.Database.Migrate(); LogProgress(context, "No referenced part found in drawing views.", LogLevel.Warning);
exportRecord = new ExportRecord return;
}
LogProgress(context, $"Found referenced part, exporting as single part...");
var item = _partExporter.ExportSinglePart(part, tempDir, context);
if (item != null)
{
if (!string.IsNullOrEmpty(configuration))
item.Configuration = configuration;
var existingItemNo = await FindExistingItemNoAsync(exportRecord?.Id, item.PartName, item.Configuration);
item.ItemNo = existingItemNo ?? await GetNextItemNumberAsync(drawingNumber);
var bomItem = new BomItem
{ {
DrawingNumber = drawingNumber ?? context.ActiveDocument.Title, ExportRecordId = exportRecord?.Id ?? 0,
SourceFilePath = context.ActiveDocument.FilePath, ItemNo = item.ItemNo,
OutputFolder = drawingOutputFolder, PartNo = item.FileName ?? item.PartName ?? "",
ExportedAt = DateTime.Now, SortOrder = 0,
ExportedBy = System.Environment.UserName Qty = item.Quantity,
TotalQty = item.Quantity,
Description = item.Description ?? "",
PartName = item.PartName ?? "",
ConfigurationName = item.Configuration ?? "",
Material = item.Material ?? ""
}; };
// Handle PDF versioning - compute hash and compare with previous if (!string.IsNullOrEmpty(item.LocalTempPath))
if (savedPdfPath != null)
{ {
HandlePdfVersioning(savedPdfPath, exportRecord.DrawingNumber, exportRecord, context); var uploadResult = await UploadDxfAsync(item, context);
if (uploadResult != null)
// Archive or discard old PDF based on hash comparison
if (pdfStashPath != null)
{ {
var previousRecord = db.ExportRecords bomItem.CutTemplate = new CutTemplate
.Where(r => r.DrawingNumber == exportRecord.DrawingNumber && r.PdfContentHash != null)
.OrderByDescending(r => r.Id)
.FirstOrDefault();
if (previousRecord != null && previousRecord.PdfContentHash == exportRecord.PdfContentHash)
{ {
_fileExportService.DiscardStash(pdfStashPath); DxfFilePath = uploadResult.StoredFilePath,
} ContentHash = item.ContentHash,
else Thickness = item.Thickness > 0 ? item.Thickness : null,
{ KFactor = item.KFactor > 0 ? item.KFactor : null,
_fileExportService.ArchiveFile(pdfStashPath, savedPdfPath); DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
} };
} }
} }
db.ExportRecords.Add(exportRecord); context.BomItemCallback?.Invoke(bomItem);
db.SaveChanges();
LogProgress(context, $"Created export record (ID: {exportRecord.Id})", LogLevel.Info); if (exportRecord != null)
await SaveBomItemAsync(exportRecord.Id, bomItem, context);
} }
} }
catch (Exception ex)
{
// Clean up stash on error
_fileExportService.DiscardStash(pdfStashPath);
LogProgress(context, $"Database error creating export record: {ex.Message}", LogLevel.Error);
}
// Export parts to DXF and save BOM items
ExportItems(items, drawingOutputFolder, context, exportRecord?.Id);
} }
#endregion #endregion
@@ -306,7 +382,7 @@ 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, ExportContext context, int? exportRecordId = null)
{ {
int successCount = 0; int successCount = 0;
int skippedCount = 0; int skippedCount = 0;
@@ -324,7 +400,7 @@ namespace ExportDXF.Services
try try
{ {
// PartExporter will handle template drawing creation through context // PartExporter will handle template drawing creation through context
_partExporter.ExportItem(item, saveDirectory, context); _partExporter.ExportItem(item, tempDir, context);
// Always create BomItem for every item (sheet metal or not) // Always create BomItem for every item (sheet metal or not)
var bomItem = new BomItem var bomItem = new BomItem
@@ -341,23 +417,23 @@ namespace ExportDXF.Services
Material = item.Material ?? "" Material = item.Material ?? ""
}; };
// Only create CutTemplate if DXF was exported successfully // Only upload and create CutTemplate if DXF was exported successfully
if (!string.IsNullOrEmpty(item.FileName)) if (!string.IsNullOrEmpty(item.LocalTempPath))
{ {
successCount++; successCount++;
var dxfPath = Path.Combine(saveDirectory, item.FileName + ".dxf"); var uploadResult = await UploadDxfAsync(item, context);
bomItem.CutTemplate = new CutTemplate if (uploadResult != null)
{ {
DxfFilePath = dxfPath, bomItem.CutTemplate = new CutTemplate
ContentHash = item.ContentHash, {
Thickness = item.Thickness > 0 ? item.Thickness : null, DxfFilePath = uploadResult.StoredFilePath,
KFactor = item.KFactor > 0 ? item.KFactor : null, ContentHash = item.ContentHash,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null Thickness = item.Thickness > 0 ? item.Thickness : null,
}; KFactor = item.KFactor > 0 ? item.KFactor : null,
DefaultBendRadius = item.BendRadius > 0 ? item.BendRadius : null
// Compare hash with previous export to decide archive/discard };
HandleDxfVersioning(item, dxfPath, context); }
} }
else else
{ {
@@ -367,21 +443,10 @@ namespace ExportDXF.Services
// Add to UI // Add to UI
context.BomItemCallback?.Invoke(bomItem); context.BomItemCallback?.Invoke(bomItem);
// Save BOM item to database if we have an export record // Save BOM item via API if we have an export record
if (exportRecordId.HasValue) if (exportRecordId.HasValue)
{ {
try await SaveBomItemAsync(exportRecordId.Value, bomItem, context);
{
using (var db = _dbContextFactory())
{
db.BomItems.Add(bomItem);
db.SaveChanges();
}
}
catch (Exception dbEx)
{
LogProgress(context, $"Database error saving BOM item: {dbEx.Message}", LogLevel.Error);
}
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -398,86 +463,144 @@ namespace ExportDXF.Services
if (exportRecordId.HasValue) if (exportRecordId.HasValue)
{ {
LogProgress(context, $"BOM items saved to database (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info); LogProgress(context, $"BOM items saved (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info);
} }
} }
#endregion #endregion
#region Versioning #region File Upload
private void HandleDxfVersioning(Item item, string dxfPath, ExportContext context) private async Task<ApiFileUploadResponse> UploadDxfAsync(Item item, ExportContext context)
{ {
if (string.IsNullOrEmpty(item.ContentHash))
return;
try try
{ {
using (var db = _dbContextFactory()) var result = await _apiClient.UploadDxfAsync(
{ item.LocalTempPath,
var previousCutTemplate = db.CutTemplates context.Equipment,
.Where(ct => ct.DxfFilePath == dxfPath && ct.ContentHash != null) context.DrawingNo,
.OrderByDescending(ct => ct.Id) item.ItemNo,
.FirstOrDefault(); item.ContentHash);
if (previousCutTemplate != null && previousCutTemplate.ContentHash == item.ContentHash) if (result.WasUnchanged)
{ LogProgress(context, $"DXF unchanged: {result.FileName}", LogLevel.Info);
// Content unchanged - discard the stashed file else if (result.IsNewFile)
_fileExportService.DiscardStash(item.StashedFilePath); LogProgress(context, $"Exported: {result.FileName}", LogLevel.Info);
LogProgress(context, $"DXF unchanged: {item.FileName}.dxf", LogLevel.Info); else
} LogProgress(context, $"DXF updated: {result.FileName}", LogLevel.Info);
else
{ return result;
// Content changed or first export - archive the old file
if (!string.IsNullOrEmpty(item.StashedFilePath))
{
_fileExportService.ArchiveFile(item.StashedFilePath, dxfPath);
LogProgress(context, $"DXF updated, previous version archived: {item.FileName}.dxf", LogLevel.Info);
}
else
{
LogProgress(context, $"Exported: {item.FileName}.dxf", LogLevel.Info);
}
}
}
} }
catch (Exception ex) catch (Exception ex)
{ {
// Don't fail the export if versioning fails - just discard the stash LogProgress(context, $"DXF upload failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
_fileExportService.DiscardStash(item.StashedFilePath); return null;
LogProgress(context, $"Versioning check failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
} }
} }
private void HandlePdfVersioning(string pdfPath, string drawingNumber, ExportRecord exportRecord, ExportContext context) #endregion
#region API Helpers
private async Task<ExportRecord> CreateExportRecordAsync(ExportContext context, string drawingNumber)
{ {
try try
{ {
var newHash = ContentHasher.ComputeFileHash(pdfPath); var dto = await _apiClient.CreateExportAsync(
drawingNumber ?? context.ActiveDocument.Title,
context.Equipment ?? "",
context.DrawingNo ?? "",
context.ActiveDocument.FilePath,
"", // Output folder is now managed by the API
context.Title);
using (var db = _dbContextFactory()) var record = new ExportRecord
{ {
var previousRecord = db.ExportRecords Id = dto.Id,
.Where(r => r.DrawingNumber == drawingNumber && r.PdfContentHash != null) DrawingNumber = dto.DrawingNumber,
.OrderByDescending(r => r.Id) EquipmentNo = dto.EquipmentNo,
.FirstOrDefault(); DrawingNo = dto.DrawingNo,
SourceFilePath = dto.SourceFilePath,
OutputFolder = dto.OutputFolder,
ExportedAt = dto.ExportedAt,
ExportedBy = dto.ExportedBy
};
if (previousRecord != null && previousRecord.PdfContentHash == newHash) LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info);
{ return record;
LogProgress(context, $"PDF unchanged: {Path.GetFileName(pdfPath)}", LogLevel.Info);
}
else
{
LogProgress(context, $"Saved PDF: {Path.GetFileName(pdfPath)}", LogLevel.Info);
}
}
if (exportRecord != null)
exportRecord.PdfContentHash = newHash;
} }
catch (Exception ex) catch (Exception ex)
{ {
LogProgress(context, $"PDF versioning check failed: {ex.Message}", LogLevel.Warning); LogProgress(context, $"API error creating export record: {ex.Message}", LogLevel.Error);
return null;
}
}
private async Task<string> FindExistingItemNoAsync(int? exportRecordId, string partName, string configurationName)
{
if (!exportRecordId.HasValue)
return null;
try
{
var existing = await _apiClient.FindExistingBomItemAsync(exportRecordId.Value, partName, configurationName);
return existing?.ItemNo;
}
catch
{
return null;
}
}
private async Task<string> GetNextItemNumberAsync(string drawingNumber)
{
if (string.IsNullOrEmpty(drawingNumber))
return "1";
try
{
return await _apiClient.GetNextItemNumberAsync(drawingNumber);
}
catch
{
return "1";
}
}
private async Task SaveBomItemAsync(int exportRecordId, BomItem bomItem, ExportContext context)
{
try
{
var apiBomItem = new ApiBomItem
{
ItemNo = bomItem.ItemNo,
PartNo = bomItem.PartNo,
SortOrder = bomItem.SortOrder,
Qty = bomItem.Qty,
TotalQty = bomItem.TotalQty,
Description = bomItem.Description,
PartName = bomItem.PartName,
ConfigurationName = bomItem.ConfigurationName,
Material = bomItem.Material
};
if (bomItem.CutTemplate != null)
{
apiBomItem.CutTemplate = new ApiCutTemplate
{
DxfFilePath = bomItem.CutTemplate.DxfFilePath,
ContentHash = bomItem.CutTemplate.ContentHash,
Thickness = bomItem.CutTemplate.Thickness,
KFactor = bomItem.CutTemplate.KFactor,
DefaultBendRadius = bomItem.CutTemplate.DefaultBendRadius
};
}
await _apiClient.CreateBomItemAsync(exportRecordId, apiBomItem);
}
catch (Exception ex)
{
LogProgress(context, $"API error saving BOM item: {ex.Message}", LogLevel.Error);
} }
} }
@@ -492,9 +615,30 @@ namespace ExportDXF.Services
return path; return path;
} }
private void CleanupTempDir(string tempDir)
{
try
{
if (Directory.Exists(tempDir))
Directory.Delete(tempDir, recursive: true);
}
catch
{
// Best-effort cleanup
}
}
private string ParseDrawingNumber(ExportContext context) private string ParseDrawingNumber(ExportContext context)
{ {
// Prefer prefix (e.g., "5007 A02 PT"), fallback to active document title // Use explicit Equipment/DrawingNo from the UI when available
if (!string.IsNullOrWhiteSpace(context?.Equipment))
{
return !string.IsNullOrWhiteSpace(context?.DrawingNo)
? $"{context.Equipment} {context.DrawingNo}"
: context.Equipment;
}
// Fallback: parse from prefix or document title
var candidate = context?.FilePrefix; var candidate = context?.FilePrefix;
var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate); var info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
if (info == null) if (info == null)
@@ -502,7 +646,7 @@ namespace ExportDXF.Services
var title = context?.ActiveDocument?.Title; var title = context?.ActiveDocument?.Title;
info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title); info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title);
} }
return info != null ? ($"{info.EquipmentNo} {info.DrawingNo}") : null; return info?.ToString();
} }
private void ValidateContext(ExportContext context) private void ValidateContext(ExportContext context)
@@ -514,6 +658,24 @@ namespace ExportDXF.Services
throw new ArgumentException("ProgressCallback cannot be null.", nameof(context)); throw new ArgumentException("ProgressCallback cannot be null.", nameof(context));
} }
private (PartDoc part, string configuration) GetReferencedPartFromViews(DrawingDoc drawing)
{
var view = (IView)drawing.GetFirstView();
// First view is the sheet itself — skip it
view = (IView)view.GetNextView();
while (view != null)
{
var doc = view.ReferencedDocument;
if (doc is PartDoc part)
return (part, view.ReferencedConfiguration);
view = (IView)view.GetNextView();
}
return (null, null);
}
private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null) private void LogProgress(ExportContext context, string message, LogLevel level = LogLevel.Info, string file = null)
{ {
context.ProgressCallback?.Invoke(message, level, file); context.ProgressCallback?.Invoke(message, level, file);

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;
}
}
}

View File

@@ -1,4 +1,4 @@
using ExportDXF.Extensions; using ExportDXF.Extensions;
using ExportDXF.Models; using ExportDXF.Models;
using ExportDXF.Utilities; using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks; using SolidWorks.Interop.sldworks;
@@ -15,24 +15,29 @@ namespace ExportDXF.Services
{ {
/// <summary> /// <summary>
/// Exports a single part document to DXF. /// Exports a single part document to DXF.
/// Returns an Item with export metadata (filename, hash, sheet metal properties), or null if export failed.
/// </summary> /// </summary>
/// <param name="part">The part document to export.</param> /// <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> /// <param name="context">The export context.</param>
void ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context); Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
/// <summary> /// <summary>
/// Exports an item (component from BOM or assembly) to DXF. /// Exports an item (component from BOM or assembly) to DXF.
/// </summary> /// </summary>
/// <param name="item">The item to export.</param> /// <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> /// <param name="context">The export context.</param>
void ExportItem(Item item, string saveDirectory, ExportContext context); void ExportItem(Item item, string saveDirectory, ExportContext context);
} }
public class PartExporter : IPartExporter 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) if (part == null)
throw new ArgumentNullException(nameof(part)); throw new ArgumentNullException(nameof(part));
@@ -49,12 +54,52 @@ namespace ExportDXF.Services
try try
{ {
var fileName = GetSinglePartFileName(model, context.FilePrefix); var fileName = GetSinglePartFileName(model, context.Equipment);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf"); 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(); 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 finally
{ {
@@ -94,7 +139,7 @@ namespace ExportDXF.Services
EnrichItemWithMetadata(item, model, part); EnrichItemWithMetadata(item, model, part);
var fileName = GetItemFileName(item, context.FilePrefix); var fileName = GetItemFileName(item, context);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf"); var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
var templateDrawing = context.GetOrCreateTemplateDrawing(); var templateDrawing = context.GetOrCreateTemplateDrawing();
@@ -102,6 +147,8 @@ namespace ExportDXF.Services
if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, savePath, context)) if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, savePath, context))
{ {
item.FileName = Path.GetFileNameWithoutExtension(savePath); item.FileName = Path.GetFileNameWithoutExtension(savePath);
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
item.LocalTempPath = savePath;
} }
else else
{ {
@@ -187,7 +234,7 @@ namespace ExportDXF.Services
var drawingModel = templateDrawing as ModelDoc2; var drawingModel = templateDrawing as ModelDoc2;
drawingModel.SaveAs(savePath); drawingModel.SaveAs(savePath);
AddEtchLines(savePath); AddEtchLines(savePath, context);
context.ProgressCallback?.Invoke($"Saved to \"{savePath}\"", LogLevel.Info, partTitle); context.ProgressCallback?.Invoke($"Saved to \"{savePath}\"", LogLevel.Info, partTitle);
@@ -254,40 +301,64 @@ namespace ExportDXF.Services
drawing.DeleteSelection(false); drawing.DeleteSelection(false);
} }
private void AddEtchLines(string dxfPath) private void AddEtchLines(string dxfPath, ExportContext context)
{ {
try try
{ {
var etcher = new EtchBendLines.Etcher(); var etcher = new EtchBendLines.Etcher();
etcher.AddEtchLines(dxfPath); etcher.AddEtchLines(dxfPath);
FixDegreeSymbol(dxfPath);
} }
catch (Exception) catch (Exception ex)
{ {
// Silently fail if etch lines can't be added context.ProgressCallback?.Invoke($"Etch lines failed: {ex.Message}", LogLevel.Warning, Path.GetFileName(dxfPath));
} }
} }
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, string equipment)
{ {
var title = model.GetTitle().Replace(".SLDPRT", ""); var title = model.GetTitle().Replace(".SLDPRT", "");
var config = model.ConfigurationManager.ActiveConfiguration.Name; var config = model.ConfigurationManager.ActiveConfiguration.Name;
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase); var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
var name = isDefaultConfig ? title : $"{title} [{config}]"; var name = isDefaultConfig ? title : $"{title} [{config}]";
return prefix + name; return string.IsNullOrWhiteSpace(equipment) ? name : $"{equipment} {name}";
} }
private string GetItemFileName(Item item, string prefix) private string GetItemFileName(Item item, ExportContext context)
{ {
prefix = prefix?.Replace("\"", "''") ?? string.Empty; if (string.IsNullOrWhiteSpace(context.DrawingNo))
if (string.IsNullOrWhiteSpace(item.ItemNo))
{ {
return prefix + item.PartName; // No drawing number: preserve part name, prefix with EquipmentNo
var equipment = context.Equipment;
return string.IsNullOrWhiteSpace(equipment)
? item.PartName
: $"{equipment} {item.PartName}";
} }
if (string.IsNullOrWhiteSpace(item.ItemNo))
return item.PartName;
var prefix = context.FilePrefix?.Replace("\"", "''") ?? string.Empty;
var num = item.ItemNo.PadLeft(2, '0'); var num = item.ItemNo.PadLeft(2, '0');
// Expected format: {DrawingNo} PT{ItemNo} // Expected format: {EquipNo} {DrawingNo} PT{ItemNo}
return string.IsNullOrWhiteSpace(prefix) return string.IsNullOrWhiteSpace(prefix)
? $"PT{num}" ? $"PT{num}"
: $"{prefix} PT{num}"; : $"{prefix} PT{num}";

View File

@@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using ACadSharp.Entities;
using ACadSharp.IO;
using PDFtoImage;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace ExportDXF.Utilities
{
public static class ContentHasher
{
/// <summary>
/// Computes a SHA256 hash of DXF geometry, ignoring entity ordering,
/// handle assignments, style names, and floating-point epsilon differences
/// that SolidWorks changes between re-exports of identical geometry.
/// Falls back to a raw file hash if ACadSharp parsing fails.
/// </summary>
public static string ComputeDxfContentHash(string filePath)
{
try
{
return ComputeGeometricHash(filePath);
}
catch
{
return ComputeFileHash(filePath);
}
}
/// <summary>
/// Computes a perceptual hash of a PDF by rendering page 1 to an image,
/// so only visual changes affect the hash (metadata/timestamp changes are ignored).
/// Falls back to a raw file hash if rendering fails.
/// </summary>
public static string ComputePdfContentHash(string filePath)
{
try
{
using (var pdfStream = File.OpenRead(filePath))
using (var pngStream = new MemoryStream())
{
Conversion.SavePng(pngStream, pdfStream, page: 0,
options: new RenderOptions(Dpi: 150));
pngStream.Position = 0;
using (var image = Image.Load<Rgba32>(pngStream))
{
var hash = ComputeDifferenceHash(image);
return hash.ToString("x16");
}
}
}
catch
{
return ComputeFileHash(filePath);
}
}
/// <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>
/// DifferenceHash: resize to 9x8 grayscale, compare adjacent pixels.
/// Produces a 64-bit hash. Implemented directly against ImageSharp 3.x API
/// (CoenM.ImageHash uses the removed GetPixelRowSpan from ImageSharp 2.x).
/// </summary>
private static ulong ComputeDifferenceHash(Image<Rgba32> image)
{
// Resize to 9 wide x 8 tall for 8x8 = 64 bit comparisons
image.Mutate(ctx => ctx.Resize(9, 8));
ulong hash = 0;
int bit = 0;
for (int y = 0; y < 8; y++)
{
for (int x = 0; x < 8; x++)
{
var left = image[x, y];
var right = image[x + 1, y];
var leftGray = 0.299 * left.R + 0.587 * left.G + 0.114 * left.B;
var rightGray = 0.299 * right.R + 0.587 * right.G + 0.114 * right.B;
if (leftGray > rightGray)
hash |= (1UL << bit);
bit++;
}
}
return hash;
}
private static string ComputeGeometricHash(string filePath)
{
using (var reader = new DxfReader(filePath))
{
var doc = reader.Read();
var signatures = new List<string>();
foreach (var entity in doc.Entities)
{
signatures.Add(GetEntitySignature(entity));
}
signatures.Sort(StringComparer.Ordinal);
var combined = string.Join("\n", signatures);
using (var sha = SHA256.Create())
{
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(combined));
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
}
}
private static string GetEntitySignature(Entity entity)
{
var layer = entity.Layer?.Name ?? "";
switch (entity)
{
case Line line:
return GetLineSignature(line, layer);
case Arc arc:
return GetArcSignature(arc, layer);
case Circle circle:
return GetCircleSignature(circle, layer);
case MText mtext:
return GetMTextSignature(mtext, layer);
default:
return $"{entity.GetType().Name}|{layer}";
}
}
private static string GetLineSignature(Line line, string layer)
{
var p1 = FormatPoint(line.StartPoint.X, line.StartPoint.Y);
var p2 = FormatPoint(line.EndPoint.X, line.EndPoint.Y);
// Normalize endpoint order so direction doesn't affect the hash
if (string.Compare(p1, p2, StringComparison.Ordinal) > 0)
{
var tmp = p1;
p1 = p2;
p2 = tmp;
}
return $"LINE|{layer}|{p1}|{p2}";
}
private static string GetArcSignature(Arc arc, string layer)
{
var center = FormatPoint(arc.Center.X, arc.Center.Y);
var r = R(arc.Radius);
var sa = R(arc.StartAngle);
var ea = R(arc.EndAngle);
return $"ARC|{layer}|{center}|{r}|{sa}|{ea}";
}
private static string GetCircleSignature(Circle circle, string layer)
{
var center = FormatPoint(circle.Center.X, circle.Center.Y);
var r = R(circle.Radius);
return $"CIRCLE|{layer}|{center}|{r}";
}
private static string GetMTextSignature(MText mtext, string layer)
{
var point = FormatPoint(mtext.InsertPoint.X, mtext.InsertPoint.Y);
var text = mtext.Value ?? "";
return $"MTEXT|{layer}|{point}|{text}";
}
private static string R(double value)
{
return Math.Round(value, 4).ToString(CultureInfo.InvariantCulture);
}
private static string FormatPoint(double x, double y)
{
return $"{R(x)},{R(y)}";
}
}
}

View File

@@ -5,11 +5,6 @@
</startup> </startup>
<appSettings> <appSettings>
<add key="MaxBendRadius" value="2.0"/> <add key="MaxBendRadius" value="2.0"/>
<add key="ExportOutputFolder" value="C:\ExportDXF\Output"/> <add key="FabWorksApiUrl" value="http://localhost:5206"/>
</appSettings> </appSettings>
<connectionStrings>
<add name="ExportDxfDb"
connectionString="Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"
providerName="Microsoft.Data.SqlClient"/>
</connectionStrings>
</configuration> </configuration>

View File

@@ -0,0 +1,9 @@
namespace FabWorks.Api.Configuration
{
public class FileStorageOptions
{
public const string SectionName = "FileStorage";
public string OutputFolder { get; set; } = @"C:\ExportDXF\Output";
}
}

View File

@@ -0,0 +1,259 @@
using FabWorks.Api.DTOs;
using FabWorks.Core.Data;
using FabWorks.Core.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/exports/{exportId}/bom-items")]
public class BomItemsController : ControllerBase
{
private readonly FabWorksDbContext _db;
public BomItemsController(FabWorksDbContext db) => _db = db;
[HttpGet("find")]
public async Task<ActionResult<BomItemDto>> FindExisting(int exportId, [FromQuery] string partName, [FromQuery] string configurationName)
{
var export = await _db.ExportRecords.FindAsync(exportId);
if (export == null) return NotFound();
var existing = await _db.BomItems
.Include(b => b.CutTemplate)
.Include(b => b.FormProgram)
.Include(b => b.ExportRecord)
.Where(b => b.ExportRecord.DrawingNumber == export.DrawingNumber
&& b.PartName == (partName ?? "")
&& b.ConfigurationName == (configurationName ?? ""))
.OrderByDescending(b => b.ID)
.FirstOrDefaultAsync();
if (existing == null) return NotFound();
return MapToDto(existing);
}
[HttpGet]
public async Task<ActionResult<List<BomItemDto>>> GetByExport(int exportId)
{
var items = await _db.BomItems
.Include(b => b.CutTemplate)
.Include(b => b.FormProgram)
.Where(b => b.ExportRecordId == exportId)
.OrderBy(b => b.SortOrder)
.ToListAsync();
return items.Select(MapToDto).ToList();
}
[HttpPost]
public async Task<ActionResult<BomItemDto>> Create(int exportId, BomItemDto dto)
{
var export = await _db.ExportRecords.FindAsync(exportId);
if (export == null) return NotFound("Export record not found");
// Look up the latest CutTemplate for this drawing+item across all previous exports
// to determine the revision number
var newContentHash = dto.CutTemplate?.ContentHash;
int revision = await ResolveRevisionAsync(export.DrawingNumber, dto.ItemNo, newContentHash);
// Look for existing BomItem with same PartName + ConfigurationName within this export record
var existing = await _db.BomItems
.Include(b => b.CutTemplate)
.Include(b => b.FormProgram)
.Where(b => b.ExportRecordId == exportId
&& b.PartName == (dto.PartName ?? "")
&& b.ConfigurationName == (dto.ConfigurationName ?? ""))
.OrderByDescending(b => b.ID)
.FirstOrDefaultAsync();
if (existing != null)
{
// Update existing fields
existing.PartNo = dto.PartNo ?? "";
existing.SortOrder = dto.SortOrder;
existing.Qty = dto.Qty;
existing.TotalQty = dto.TotalQty;
existing.Description = dto.Description ?? "";
existing.Material = dto.Material ?? "";
if (dto.CutTemplate != null)
{
if (existing.CutTemplate != null)
{
existing.CutTemplate.DxfFilePath = dto.CutTemplate.DxfFilePath ?? "";
existing.CutTemplate.ContentHash = dto.CutTemplate.ContentHash;
existing.CutTemplate.Revision = revision;
existing.CutTemplate.Thickness = dto.CutTemplate.Thickness;
existing.CutTemplate.KFactor = dto.CutTemplate.KFactor;
existing.CutTemplate.DefaultBendRadius = dto.CutTemplate.DefaultBendRadius;
}
else
{
existing.CutTemplate = new CutTemplate
{
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
ContentHash = dto.CutTemplate.ContentHash,
Revision = revision,
Thickness = dto.CutTemplate.Thickness,
KFactor = dto.CutTemplate.KFactor,
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
};
}
}
if (dto.FormProgram != null)
{
if (existing.FormProgram != null)
{
existing.FormProgram.ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "";
existing.FormProgram.ContentHash = dto.FormProgram.ContentHash;
existing.FormProgram.ProgramName = dto.FormProgram.ProgramName ?? "";
existing.FormProgram.Thickness = dto.FormProgram.Thickness;
existing.FormProgram.MaterialType = dto.FormProgram.MaterialType ?? "";
existing.FormProgram.KFactor = dto.FormProgram.KFactor;
existing.FormProgram.BendCount = dto.FormProgram.BendCount;
existing.FormProgram.UpperToolNames = dto.FormProgram.UpperToolNames ?? "";
existing.FormProgram.LowerToolNames = dto.FormProgram.LowerToolNames ?? "";
existing.FormProgram.SetupNotes = dto.FormProgram.SetupNotes ?? "";
}
else
{
existing.FormProgram = new FormProgram
{
ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "",
ContentHash = dto.FormProgram.ContentHash,
ProgramName = dto.FormProgram.ProgramName ?? "",
Thickness = dto.FormProgram.Thickness,
MaterialType = dto.FormProgram.MaterialType ?? "",
KFactor = dto.FormProgram.KFactor,
BendCount = dto.FormProgram.BendCount,
UpperToolNames = dto.FormProgram.UpperToolNames ?? "",
LowerToolNames = dto.FormProgram.LowerToolNames ?? "",
SetupNotes = dto.FormProgram.SetupNotes ?? ""
};
}
}
await _db.SaveChangesAsync();
return Ok(MapToDto(existing));
}
// No existing match — create new
var item = new BomItem
{
ExportRecordId = exportId,
ItemNo = dto.ItemNo ?? "",
PartNo = dto.PartNo ?? "",
SortOrder = dto.SortOrder,
Qty = dto.Qty,
TotalQty = dto.TotalQty,
Description = dto.Description ?? "",
PartName = dto.PartName ?? "",
ConfigurationName = dto.ConfigurationName ?? "",
Material = dto.Material ?? ""
};
if (dto.CutTemplate != null)
{
item.CutTemplate = new CutTemplate
{
DxfFilePath = dto.CutTemplate.DxfFilePath ?? "",
ContentHash = dto.CutTemplate.ContentHash,
Revision = revision,
Thickness = dto.CutTemplate.Thickness,
KFactor = dto.CutTemplate.KFactor,
DefaultBendRadius = dto.CutTemplate.DefaultBendRadius
};
}
if (dto.FormProgram != null)
{
item.FormProgram = new FormProgram
{
ProgramFilePath = dto.FormProgram.ProgramFilePath ?? "",
ContentHash = dto.FormProgram.ContentHash,
ProgramName = dto.FormProgram.ProgramName ?? "",
Thickness = dto.FormProgram.Thickness,
MaterialType = dto.FormProgram.MaterialType ?? "",
KFactor = dto.FormProgram.KFactor,
BendCount = dto.FormProgram.BendCount,
UpperToolNames = dto.FormProgram.UpperToolNames ?? "",
LowerToolNames = dto.FormProgram.LowerToolNames ?? "",
SetupNotes = dto.FormProgram.SetupNotes ?? ""
};
}
_db.BomItems.Add(item);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetByExport), new { exportId }, MapToDto(item));
}
/// <summary>
/// Determines the revision number for a CutTemplate by looking at the most recent
/// CutTemplate for the same drawing number and item number across all exports.
/// Returns 1 if no previous version exists, the same revision if the hash matches,
/// or previous revision + 1 if the hash changed.
/// </summary>
private async Task<int> ResolveRevisionAsync(string drawingNumber, string itemNo, string contentHash)
{
if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo) || string.IsNullOrEmpty(contentHash))
return 1;
var previous = await _db.CutTemplates
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
&& c.BomItem.ItemNo == itemNo
&& c.ContentHash != null)
.OrderByDescending(c => c.Id)
.Select(c => new { c.ContentHash, c.Revision })
.FirstOrDefaultAsync();
if (previous == null)
return 1;
return previous.ContentHash == contentHash
? previous.Revision
: previous.Revision + 1;
}
private static BomItemDto MapToDto(BomItem b) => new()
{
ID = b.ID,
ItemNo = b.ItemNo,
PartNo = b.PartNo,
SortOrder = b.SortOrder,
Qty = b.Qty,
TotalQty = b.TotalQty,
Description = b.Description,
PartName = b.PartName,
ConfigurationName = b.ConfigurationName,
Material = b.Material,
CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto
{
Id = b.CutTemplate.Id,
DxfFilePath = b.CutTemplate.DxfFilePath,
ContentHash = b.CutTemplate.ContentHash,
Revision = b.CutTemplate.Revision,
Thickness = b.CutTemplate.Thickness,
KFactor = b.CutTemplate.KFactor,
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
},
FormProgram = b.FormProgram == null ? null : new FormProgramDto
{
Id = b.FormProgram.Id,
ProgramFilePath = b.FormProgram.ProgramFilePath,
ContentHash = b.FormProgram.ContentHash,
ProgramName = b.FormProgram.ProgramName,
Thickness = b.FormProgram.Thickness,
MaterialType = b.FormProgram.MaterialType,
KFactor = b.FormProgram.KFactor,
BendCount = b.FormProgram.BendCount,
UpperToolNames = b.FormProgram.UpperToolNames,
LowerToolNames = b.FormProgram.LowerToolNames,
SetupNotes = b.FormProgram.SetupNotes
}
};
}
}

View File

@@ -0,0 +1,470 @@
using System.Globalization;
using System.IO.Compression;
using System.Numerics;
using FabWorks.Api.DTOs;
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using FabWorks.Core.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class ExportsController : ControllerBase
{
private readonly FabWorksDbContext _db;
private readonly IFileStorageService _fileStorage;
public ExportsController(FabWorksDbContext db, IFileStorageService fileStorage)
{
_db = db;
_fileStorage = fileStorage;
}
[HttpGet]
public async Task<ActionResult<object>> List(
[FromQuery] string search = null,
[FromQuery] int skip = 0,
[FromQuery] int take = 50)
{
var query = _db.ExportRecords
.Include(r => r.BomItems)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLower();
query = query.Where(r =>
r.DrawingNumber.ToLower().Contains(term) ||
(r.Title != null && r.Title.ToLower().Contains(term)) ||
r.ExportedBy.ToLower().Contains(term) ||
r.BomItems.Any(b => b.PartName.ToLower().Contains(term) ||
b.Description.ToLower().Contains(term)));
}
var total = await query.CountAsync();
var records = await query
.OrderByDescending(r => r.ExportedAt)
.Skip(skip)
.Take(take)
.ToListAsync();
return new
{
total,
items = records.Select(r => new
{
r.Id,
r.DrawingNumber,
r.Title,
r.SourceFilePath,
r.ExportedAt,
r.ExportedBy,
BomItemCount = r.BomItems?.Count ?? 0
})
};
}
[HttpPost]
public async Task<ActionResult<ExportDetailDto>> Create(CreateExportRequest request)
{
var record = new ExportRecord
{
DrawingNumber = request.DrawingNumber,
Title = request.Title,
EquipmentNo = request.EquipmentNo,
DrawingNo = request.DrawingNo,
SourceFilePath = request.SourceFilePath,
OutputFolder = request.OutputFolder,
ExportedAt = DateTime.Now,
ExportedBy = Environment.UserName
};
_db.ExportRecords.Add(record);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById), new { id = record.Id }, MapToDto(record));
}
[HttpGet("{id}")]
public async Task<ActionResult<ExportDetailDto>> GetById(int id)
{
var record = await _db.ExportRecords
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
.Include(r => r.BomItems).ThenInclude(b => b.FormProgram)
.FirstOrDefaultAsync(r => r.Id == id);
if (record == null) return NotFound();
return MapToDto(record);
}
[HttpGet("by-source")]
public async Task<ActionResult<ExportDetailDto>> GetBySourceFile([FromQuery] string path)
{
var record = await _db.ExportRecords
.Where(r => r.SourceFilePath.ToLower() == path.ToLower()
&& !string.IsNullOrEmpty(r.DrawingNumber))
.OrderByDescending(r => r.Id)
.FirstOrDefaultAsync();
if (record == null) return NotFound();
return MapToDto(record);
}
[HttpGet("by-drawing")]
public async Task<ActionResult<List<ExportDetailDto>>> GetByDrawing([FromQuery] string drawingNumber)
{
var records = await _db.ExportRecords
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
.Include(r => r.BomItems).ThenInclude(b => b.FormProgram)
.Where(r => r.DrawingNumber == drawingNumber)
.OrderByDescending(r => r.ExportedAt)
.ToListAsync();
return records.Select(MapToDto).ToList();
}
[HttpGet("next-item-number")]
public async Task<ActionResult<string>> GetNextItemNumber([FromQuery] string drawingNumber)
{
if (string.IsNullOrEmpty(drawingNumber)) return "1";
var existingItems = await _db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber)
.SelectMany(r => r.BomItems)
.Select(b => b.ItemNo)
.ToListAsync();
int maxNum = 0;
foreach (var itemNo in existingItems)
{
if (int.TryParse(itemNo, out var num) && num > maxNum)
maxNum = num;
}
return (maxNum + 1).ToString();
}
[HttpGet("drawing-numbers")]
public async Task<ActionResult<List<string>>> GetDrawingNumbers()
{
var numbers = await _db.ExportRecords
.Select(r => r.DrawingNumber)
.Where(d => !string.IsNullOrEmpty(d))
.Distinct()
.ToListAsync();
return numbers;
}
[HttpGet("equipment-numbers")]
public async Task<ActionResult<List<string>>> GetEquipmentNumbers()
{
var numbers = await _db.ExportRecords
.Select(r => r.EquipmentNo)
.Where(e => !string.IsNullOrEmpty(e))
.Distinct()
.OrderBy(e => e)
.ToListAsync();
return numbers;
}
[HttpGet("drawing-numbers-by-equipment")]
public async Task<ActionResult<List<string>>> GetDrawingNumbersByEquipment([FromQuery] string equipmentNo)
{
var query = _db.ExportRecords
.Where(r => !string.IsNullOrEmpty(r.DrawingNo));
if (!string.IsNullOrEmpty(equipmentNo))
query = query.Where(r => r.EquipmentNo == equipmentNo);
var numbers = await query
.Select(r => r.DrawingNo)
.Distinct()
.OrderBy(d => d)
.ToListAsync();
return numbers;
}
[HttpGet("previous-pdf-hash")]
public async Task<ActionResult<string>> GetPreviousPdfHash(
[FromQuery] string drawingNumber,
[FromQuery] int? excludeId = null)
{
var hash = await _db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber
&& r.PdfContentHash != null
&& (excludeId == null || r.Id != excludeId))
.OrderByDescending(r => r.Id)
.Select(r => r.PdfContentHash)
.FirstOrDefaultAsync();
if (hash == null) return NotFound();
return hash;
}
[HttpPatch("{id}/pdf-hash")]
public async Task<IActionResult> UpdatePdfHash(int id, [FromBody] UpdatePdfHashRequest request)
{
var record = await _db.ExportRecords.FindAsync(id);
if (record == null) return NotFound();
record.PdfContentHash = request.PdfContentHash;
if (!string.IsNullOrEmpty(record.DrawingNumber) && !string.IsNullOrEmpty(request.PdfContentHash))
{
var (drawing, revision) = await ResolveDrawingAsync(record.DrawingNumber, record.Title, request.PdfContentHash);
record.Drawing = drawing;
record.DrawingRevision = revision;
}
await _db.SaveChangesAsync();
return NoContent();
}
private async Task<(Drawing drawing, int revision)> ResolveDrawingAsync(string drawingNumber, string title, string pdfContentHash)
{
var drawing = await _db.Drawings
.FirstOrDefaultAsync(d => d.DrawingNumber == drawingNumber);
// Get the highest revision recorded for this drawing across all exports
var lastRevision = await _db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber && r.DrawingRevision != null)
.OrderByDescending(r => r.DrawingRevision)
.Select(r => r.DrawingRevision)
.FirstOrDefaultAsync() ?? 0;
if (drawing == null)
{
drawing = new Drawing
{
DrawingNumber = drawingNumber,
Title = title,
PdfContentHash = pdfContentHash
};
_db.Drawings.Add(drawing);
return (drawing, 1);
}
if (!string.IsNullOrEmpty(title))
drawing.Title = title;
if (ArePerceptualHashesSimilar(drawing.PdfContentHash, pdfContentHash))
{
// Hash unchanged — keep same revision
return (drawing, lastRevision == 0 ? 1 : lastRevision);
}
// Hash changed — bump revision and update stored hash
drawing.PdfContentHash = pdfContentHash;
return (drawing, lastRevision + 1);
}
[HttpGet("previous-cut-template")]
public async Task<ActionResult<CutTemplateDto>> GetPreviousCutTemplate(
[FromQuery] string drawingNumber,
[FromQuery] string itemNo)
{
if (string.IsNullOrEmpty(drawingNumber) || string.IsNullOrEmpty(itemNo))
return BadRequest("drawingNumber and itemNo are required.");
var ct = await _db.CutTemplates
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
&& c.BomItem.ItemNo == itemNo
&& c.ContentHash != null)
.OrderByDescending(c => c.Id)
.FirstOrDefaultAsync();
if (ct == null) return NotFound();
return new CutTemplateDto
{
Id = ct.Id,
DxfFilePath = ct.DxfFilePath,
ContentHash = ct.ContentHash,
Revision = ct.Revision,
Thickness = ct.Thickness,
KFactor = ct.KFactor,
DefaultBendRadius = ct.DefaultBendRadius
};
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var record = await _db.ExportRecords
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
.Include(r => r.BomItems).ThenInclude(b => b.FormProgram)
.FirstOrDefaultAsync(r => r.Id == id);
if (record == null) return NotFound();
_db.ExportRecords.Remove(record);
await _db.SaveChangesAsync();
return NoContent();
}
[HttpGet("{id}/download-dxfs")]
public async Task<IActionResult> DownloadAllDxfs(int id)
{
var record = await _db.ExportRecords
.Include(r => r.BomItems).ThenInclude(b => b.CutTemplate)
.FirstOrDefaultAsync(r => r.Id == id);
if (record == null) return NotFound();
var dxfItems = record.BomItems
.Where(b => b.CutTemplate?.ContentHash != null)
.ToList();
if (dxfItems.Count == 0) return NotFound("No DXF files for this export.");
var zipName = $"{record.DrawingNumber ?? $"Export-{id}"} DXFs.zip";
return BuildDxfZip(dxfItems, zipName);
}
[HttpGet("download-dxfs")]
public async Task<IActionResult> DownloadDxfsByDrawing([FromQuery] string drawingNumber)
{
if (string.IsNullOrEmpty(drawingNumber))
return BadRequest("drawingNumber is required.");
var dxfItems = await _db.BomItems
.Include(b => b.CutTemplate)
.Where(b => b.ExportRecord.DrawingNumber == drawingNumber
&& b.CutTemplate != null
&& b.CutTemplate.ContentHash != null)
.ToListAsync();
if (dxfItems.Count == 0) return NotFound("No DXF files for this drawing.");
// Deduplicate by content hash (keep latest)
dxfItems = dxfItems
.GroupBy(b => b.CutTemplate.ContentHash)
.Select(g => g.Last())
.ToList();
var zipName = $"{drawingNumber} DXFs.zip";
return BuildDxfZip(dxfItems, zipName);
}
private FileResult BuildDxfZip(List<BomItem> dxfItems, string zipName)
{
var ms = new MemoryStream();
using (var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
{
var usedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var b in dxfItems)
{
var ct = b.CutTemplate;
var fileName = ct.DxfFilePath?.Split(new[] { '/', '\\' }).LastOrDefault()
?? $"PT{(b.ItemNo ?? "").PadLeft(2, '0')}.dxf";
// Ensure unique names in zip
if (!usedNames.Add(fileName))
{
var baseName = Path.GetFileNameWithoutExtension(fileName);
var ext = Path.GetExtension(fileName);
var counter = 2;
do { fileName = $"{baseName}_{counter++}{ext}"; }
while (!usedNames.Add(fileName));
}
var blobStream = _fileStorage.OpenBlob(ct.ContentHash, "dxf");
if (blobStream == null) continue;
var entry = zip.CreateEntry(fileName, CompressionLevel.Fastest);
using var entryStream = entry.Open();
blobStream.CopyTo(entryStream);
blobStream.Dispose();
}
}
ms.Position = 0;
return File(ms, "application/zip", zipName);
}
/// <summary>
/// Compares two perceptual hashes using Hamming distance.
/// Perceptual hashes (16 hex chars / 64 bits) are compared with a tolerance
/// of up to 10 differing bits (~84% similarity). SHA256 fallback hashes
/// (64 hex chars) use exact comparison.
/// </summary>
private static bool ArePerceptualHashesSimilar(string hash1, string hash2)
{
if (hash1 == hash2) return true;
if (string.IsNullOrEmpty(hash1) || string.IsNullOrEmpty(hash2)) return false;
// Perceptual hashes are 16 hex chars (64-bit DifferenceHash)
// SHA256 fallback hashes are 64 hex chars — require exact match
if (hash1.Length != 16 || hash2.Length != 16)
return false;
if (ulong.TryParse(hash1, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h1) &&
ulong.TryParse(hash2, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var h2))
{
var hammingDistance = BitOperations.PopCount(h1 ^ h2);
return hammingDistance <= 10;
}
return false;
}
private static ExportDetailDto MapToDto(ExportRecord r) => new()
{
Id = r.Id,
DrawingNumber = r.DrawingNumber,
Title = r.Title,
EquipmentNo = r.EquipmentNo,
DrawingNo = r.DrawingNo,
SourceFilePath = r.SourceFilePath,
OutputFolder = r.OutputFolder,
ExportedAt = r.ExportedAt,
ExportedBy = r.ExportedBy,
PdfContentHash = r.PdfContentHash,
BomItems = r.BomItems?.Select(b => new BomItemDto
{
ID = b.ID,
ItemNo = b.ItemNo,
PartNo = b.PartNo,
SortOrder = b.SortOrder,
Qty = b.Qty,
TotalQty = b.TotalQty,
Description = b.Description,
PartName = b.PartName,
ConfigurationName = b.ConfigurationName,
Material = b.Material,
CutTemplate = b.CutTemplate == null ? null : new CutTemplateDto
{
Id = b.CutTemplate.Id,
DxfFilePath = b.CutTemplate.DxfFilePath,
ContentHash = b.CutTemplate.ContentHash,
Revision = b.CutTemplate.Revision,
Thickness = b.CutTemplate.Thickness,
KFactor = b.CutTemplate.KFactor,
DefaultBendRadius = b.CutTemplate.DefaultBendRadius
},
FormProgram = b.FormProgram == null ? null : new FormProgramDto
{
Id = b.FormProgram.Id,
ProgramFilePath = b.FormProgram.ProgramFilePath,
ContentHash = b.FormProgram.ContentHash,
ProgramName = b.FormProgram.ProgramName,
Thickness = b.FormProgram.Thickness,
MaterialType = b.FormProgram.MaterialType,
KFactor = b.FormProgram.KFactor,
BendCount = b.FormProgram.BendCount,
UpperToolNames = b.FormProgram.UpperToolNames,
LowerToolNames = b.FormProgram.LowerToolNames,
SetupNotes = b.FormProgram.SetupNotes
}
}).ToList() ?? new()
};
}
}

View File

@@ -0,0 +1,189 @@
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class FileBrowserController : ControllerBase
{
private readonly IFileStorageService _fileStorage;
private readonly FabWorksDbContext _db;
private readonly FileExtensionContentTypeProvider _contentTypeProvider = new();
public FileBrowserController(IFileStorageService fileStorage, FabWorksDbContext db)
{
_fileStorage = fileStorage;
_db = db;
}
[HttpGet("files")]
public async Task<ActionResult<FileListResult>> ListFiles(
[FromQuery] string search = null,
[FromQuery] string type = null)
{
var files = new List<StoredFileEntry>();
// Query DXF files from CutTemplates
if (type == null || type.Equals("dxf", StringComparison.OrdinalIgnoreCase))
{
var dxfQuery = _db.CutTemplates
.Where(c => c.ContentHash != null)
.Select(c => new
{
c.Id,
c.DxfFilePath,
c.ContentHash,
c.Thickness,
c.Revision,
DrawingNumber = c.BomItem.ExportRecord.DrawingNumber,
CreatedAt = c.BomItem.ExportRecord.ExportedAt
});
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLower();
dxfQuery = dxfQuery.Where(c =>
c.DxfFilePath.ToLower().Contains(term) ||
c.DrawingNumber.ToLower().Contains(term));
}
var dxfResults = await dxfQuery
.OrderByDescending(c => c.CreatedAt)
.Take(500)
.ToListAsync();
// Deduplicate by content hash (keep latest)
var seenDxf = new HashSet<string>();
foreach (var c in dxfResults)
{
if (seenDxf.Contains(c.ContentHash)) continue;
seenDxf.Add(c.ContentHash);
var fileName = c.DxfFilePath?.Split(new[] { '/', '\\' }).LastOrDefault() ?? c.DxfFilePath;
files.Add(new StoredFileEntry
{
FileName = fileName,
ContentHash = c.ContentHash,
FileType = "dxf",
DrawingNumber = c.DrawingNumber,
Thickness = c.Thickness,
Revision = c.Revision,
CreatedAt = c.CreatedAt
});
}
}
// Query PDF files from ExportRecords
if (type == null || type.Equals("pdf", StringComparison.OrdinalIgnoreCase))
{
var pdfQuery = _db.ExportRecords
.Where(r => r.PdfContentHash != null)
.Select(r => new
{
r.Id,
r.DrawingNumber,
r.PdfContentHash,
r.ExportedAt,
r.DrawingRevision
});
if (!string.IsNullOrWhiteSpace(search))
{
var term = search.Trim().ToLower();
pdfQuery = pdfQuery.Where(r =>
r.DrawingNumber.ToLower().Contains(term));
}
var pdfResults = await pdfQuery
.OrderByDescending(r => r.ExportedAt)
.Take(500)
.ToListAsync();
// Deduplicate by content hash
var seenPdf = new HashSet<string>();
foreach (var r in pdfResults)
{
if (seenPdf.Contains(r.PdfContentHash)) continue;
seenPdf.Add(r.PdfContentHash);
files.Add(new StoredFileEntry
{
FileName = $"{r.DrawingNumber}.pdf",
ContentHash = r.PdfContentHash,
FileType = "pdf",
DrawingNumber = r.DrawingNumber,
Revision = r.DrawingRevision,
CreatedAt = r.ExportedAt
});
}
}
return new FileListResult
{
Total = files.Count,
Files = files.OrderByDescending(f => f.CreatedAt).ToList()
};
}
[HttpGet("preview")]
public IActionResult PreviewFile([FromQuery] string hash, [FromQuery] string ext = "dxf")
{
if (string.IsNullOrEmpty(hash) || hash.Length < 4)
return BadRequest("Invalid hash.");
if (!_fileStorage.BlobExists(hash, ext))
return NotFound("File not found.");
var stream = _fileStorage.OpenBlob(hash, ext);
if (stream == null)
return NotFound("File not found.");
var virtualName = $"file.{ext}";
if (!_contentTypeProvider.TryGetContentType(virtualName, out var contentType))
contentType = "application/octet-stream";
return File(stream, contentType);
}
[HttpGet("download")]
public IActionResult DownloadFile([FromQuery] string hash, [FromQuery] string ext = "dxf", [FromQuery] string name = null)
{
if (string.IsNullOrEmpty(hash) || hash.Length < 4)
return BadRequest("Invalid hash.");
if (!_fileStorage.BlobExists(hash, ext))
return NotFound("File not found.");
var stream = _fileStorage.OpenBlob(hash, ext);
if (stream == null)
return NotFound("File not found.");
var fileName = name ?? $"{hash[..8]}.{ext}";
if (!_contentTypeProvider.TryGetContentType(fileName, out var contentType))
contentType = "application/octet-stream";
return File(stream, contentType, fileName);
}
}
public class FileListResult
{
public int Total { get; set; }
public List<StoredFileEntry> Files { get; set; }
}
public class StoredFileEntry
{
public string FileName { get; set; }
public string ContentHash { get; set; }
public string FileType { get; set; }
public string DrawingNumber { get; set; }
public double? Thickness { get; set; }
public int? Revision { get; set; }
public DateTime CreatedAt { get; set; }
}
}

View File

@@ -0,0 +1,93 @@
using FabWorks.Api.DTOs;
using FabWorks.Api.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class FilesController : ControllerBase
{
private readonly IFileStorageService _fileStorage;
private readonly FileExtensionContentTypeProvider _contentTypeProvider = new();
public FilesController(IFileStorageService fileStorage)
{
_fileStorage = fileStorage;
}
[HttpPost("dxf")]
[RequestSizeLimit(50_000_000)] // 50 MB
public async Task<ActionResult<FileUploadResponse>> UploadDxf(
IFormFile file,
[FromForm] string equipment,
[FromForm] string drawingNo,
[FromForm] string itemNo,
[FromForm] string contentHash)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded.");
using var stream = file.OpenReadStream();
var result = await _fileStorage.StoreDxfAsync(stream, equipment, drawingNo, itemNo, contentHash, file.FileName);
return Ok(new FileUploadResponse
{
StoredFilePath = result.FileName,
ContentHash = result.ContentHash,
FileName = result.FileName,
WasUnchanged = result.WasUnchanged,
IsNewFile = result.IsNewFile
});
}
[HttpPost("pdf")]
[RequestSizeLimit(100_000_000)] // 100 MB
public async Task<ActionResult<FileUploadResponse>> UploadPdf(
IFormFile file,
[FromForm] string equipment,
[FromForm] string drawingNo,
[FromForm] string contentHash,
[FromForm] int? exportRecordId = null)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded.");
using var stream = file.OpenReadStream();
var result = await _fileStorage.StorePdfAsync(stream, equipment, drawingNo, contentHash, exportRecordId);
return Ok(new FileUploadResponse
{
StoredFilePath = result.FileName,
ContentHash = result.ContentHash,
FileName = result.FileName,
WasUnchanged = result.WasUnchanged,
IsNewFile = result.IsNewFile
});
}
[HttpGet("blob/{hash}")]
public IActionResult GetBlob(string hash, [FromQuery] string ext = "dxf", [FromQuery] bool download = false, [FromQuery] string name = null)
{
if (string.IsNullOrEmpty(hash) || hash.Length < 4)
return BadRequest("Invalid hash.");
if (!_fileStorage.BlobExists(hash, ext))
return NotFound("Blob not found.");
var stream = _fileStorage.OpenBlob(hash, ext);
if (stream == null)
return NotFound("Blob not found.");
var fileName = !string.IsNullOrEmpty(name) ? name : $"{hash[..8]}.{ext}";
if (!_contentTypeProvider.TryGetContentType(fileName, out var contentType))
contentType = "application/octet-stream";
if (download)
return File(stream, contentType, fileName);
return File(stream, contentType);
}
}
}

View File

@@ -0,0 +1,106 @@
using FabWorks.Api.DTOs;
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Api.Controllers
{
[ApiController]
[Route("api/form-programs")]
public class FormProgramsController : ControllerBase
{
private readonly FabWorksDbContext _db;
private readonly FormProgramService _formService;
public FormProgramsController(FabWorksDbContext db, FormProgramService formService)
{
_db = db;
_formService = formService;
}
[HttpGet("by-drawing")]
public async Task<ActionResult<List<FormProgramDto>>> GetByDrawing([FromQuery] string drawingNumber)
{
var programs = await _db.FormPrograms
.Include(fp => fp.BomItem)
.ThenInclude(b => b.ExportRecord)
.Where(fp => fp.BomItem.ExportRecord.DrawingNumber == drawingNumber)
.ToListAsync();
return programs.Select(fp => new FormProgramDto
{
Id = fp.Id,
ProgramFilePath = fp.ProgramFilePath,
ContentHash = fp.ContentHash,
ProgramName = fp.ProgramName,
Thickness = fp.Thickness,
MaterialType = fp.MaterialType,
KFactor = fp.KFactor,
BendCount = fp.BendCount,
UpperToolNames = fp.UpperToolNames,
LowerToolNames = fp.LowerToolNames,
SetupNotes = fp.SetupNotes
}).ToList();
}
[HttpPost("parse")]
public ActionResult<FormProgramDto> Parse([FromQuery] string filePath)
{
if (!System.IO.File.Exists(filePath))
return NotFound($"File not found: {filePath}");
var fp = _formService.ParseFromFile(filePath);
return new FormProgramDto
{
ProgramFilePath = fp.ProgramFilePath,
ContentHash = fp.ContentHash,
ProgramName = fp.ProgramName,
Thickness = fp.Thickness,
MaterialType = fp.MaterialType,
KFactor = fp.KFactor,
BendCount = fp.BendCount,
UpperToolNames = fp.UpperToolNames,
LowerToolNames = fp.LowerToolNames,
SetupNotes = fp.SetupNotes
};
}
[HttpPost("{bomItemId}")]
public async Task<ActionResult<FormProgramDto>> AttachToItem(int bomItemId, [FromQuery] string filePath)
{
var bomItem = await _db.BomItems
.Include(b => b.FormProgram)
.FirstOrDefaultAsync(b => b.ID == bomItemId);
if (bomItem == null) return NotFound("BOM item not found");
if (!System.IO.File.Exists(filePath))
return NotFound($"File not found: {filePath}");
var fp = _formService.ParseFromFile(filePath);
fp.BomItemId = bomItemId;
if (bomItem.FormProgram != null)
_db.FormPrograms.Remove(bomItem.FormProgram);
bomItem.FormProgram = fp;
await _db.SaveChangesAsync();
return new FormProgramDto
{
Id = fp.Id,
ProgramFilePath = fp.ProgramFilePath,
ContentHash = fp.ContentHash,
ProgramName = fp.ProgramName,
Thickness = fp.Thickness,
MaterialType = fp.MaterialType,
KFactor = fp.KFactor,
BendCount = fp.BendCount,
UpperToolNames = fp.UpperToolNames,
LowerToolNames = fp.LowerToolNames,
SetupNotes = fp.SetupNotes
};
}
}
}

View File

@@ -0,0 +1,17 @@
namespace FabWorks.Api.DTOs
{
public class CreateExportRequest
{
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
}
public class UpdatePdfHashRequest
{
public string PdfContentHash { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
namespace FabWorks.Api.DTOs
{
public class ExportDetailDto
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public List<BomItemDto> BomItems { get; set; } = new();
}
public class BomItemDto
{
public int ID { get; set; }
public string 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 CutTemplateDto CutTemplate { get; set; }
public FormProgramDto FormProgram { get; set; }
}
public class CutTemplateDto
{
public int Id { get; set; }
public string DxfFilePath { get; set; }
public string ContentHash { get; set; }
public int Revision { get; set; }
public double? Thickness { get; set; }
public double? KFactor { get; set; }
public double? DefaultBendRadius { get; set; }
}
public class FormProgramDto
{
public int Id { get; set; }
public string ProgramFilePath { get; set; }
public string ContentHash { get; set; }
public string ProgramName { get; set; }
public double? Thickness { get; set; }
public string MaterialType { get; set; }
public double? KFactor { get; set; }
public int BendCount { get; set; }
public string UpperToolNames { get; set; }
public string LowerToolNames { get; set; }
public string SetupNotes { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace FabWorks.Api.DTOs
{
public class FileUploadResponse
{
public string StoredFilePath { get; set; } // kept for client compat, contains logical filename
public string ContentHash { get; set; }
public string FileName { get; set; }
public bool WasUnchanged { get; set; }
public bool IsNewFile { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FabWorks.Core\FabWorks.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

26
FabWorks.Api/Program.cs Normal file
View File

@@ -0,0 +1,26 @@
using FabWorks.Api.Configuration;
using FabWorks.Api.Services;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<FabWorksDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("FabWorksDb")));
builder.Services.AddSingleton<FormProgramService>();
builder.Services.Configure<FileStorageOptions>(
builder.Configuration.GetSection(FileStorageOptions.SectionName));
builder.Services.AddScoped<IFileStorageService, FileStorageService>();
var app = builder.Build();
app.UseDefaultFiles();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
ctx.Context.Response.Headers.Append("Cache-Control", "no-cache, no-store")
});
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:45483",
"sslPort": 44397
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "",
"applicationUrl": "http://localhost:5206",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "",
"applicationUrl": "https://localhost:7182;http://localhost:5206",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,167 @@
using FabWorks.Api.Configuration;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace FabWorks.Api.Services
{
public class FileUploadResult
{
public string ContentHash { get; set; }
public string FileName { get; set; }
public bool WasUnchanged { get; set; }
public bool IsNewFile { get; set; }
}
public interface IFileStorageService
{
string OutputFolder { get; }
Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash, string originalFileName = null);
Task<FileUploadResult> StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null);
Stream OpenBlob(string contentHash, string extension);
bool BlobExists(string contentHash, string extension);
}
public class FileStorageService : IFileStorageService
{
private readonly FileStorageOptions _options;
private readonly FabWorksDbContext _db;
public string OutputFolder => _options.OutputFolder;
public FileStorageService(IOptions<FileStorageOptions> options, FabWorksDbContext db)
{
_options = options.Value;
_db = db;
var blobRoot = Path.Combine(_options.OutputFolder, "blobs");
if (!Directory.Exists(blobRoot))
Directory.CreateDirectory(blobRoot);
}
public async Task<FileUploadResult> StoreDxfAsync(Stream stream, string equipment, string drawingNo, string itemNo, string contentHash, string originalFileName = null)
{
var fileName = BuildDxfFileName(drawingNo, equipment, itemNo, originalFileName);
// Look up previous hash by drawing number + item number
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
var previousHash = await _db.CutTemplates
.Where(c => c.BomItem.ExportRecord.DrawingNumber == drawingNumber
&& c.BomItem.ItemNo == itemNo
&& c.ContentHash != null)
.OrderByDescending(c => c.Id)
.Select(c => c.ContentHash)
.FirstOrDefaultAsync();
var wasUnchanged = previousHash != null && previousHash == contentHash;
var isNewFile = await StoreBlobAsync(stream, contentHash, "dxf");
return new FileUploadResult
{
ContentHash = contentHash,
FileName = fileName,
WasUnchanged = wasUnchanged,
IsNewFile = isNewFile
};
}
public async Task<FileUploadResult> StorePdfAsync(Stream stream, string equipment, string drawingNo, string contentHash, int? exportRecordId = null)
{
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
var fileName = $"{drawingNumber}.pdf";
// Look up previous PDF hash
var previousHash = await _db.ExportRecords
.Where(r => r.DrawingNumber == drawingNumber
&& r.PdfContentHash != null
&& (exportRecordId == null || r.Id != exportRecordId))
.OrderByDescending(r => r.Id)
.Select(r => r.PdfContentHash)
.FirstOrDefaultAsync();
var wasUnchanged = previousHash != null && previousHash == contentHash;
var isNewFile = await StoreBlobAsync(stream, contentHash, "pdf");
// Update the export record with the PDF content hash
if (exportRecordId.HasValue)
{
var record = await _db.ExportRecords.FindAsync(exportRecordId.Value);
if (record != null)
{
record.PdfContentHash = contentHash;
await _db.SaveChangesAsync();
}
}
return new FileUploadResult
{
ContentHash = contentHash,
FileName = fileName,
WasUnchanged = wasUnchanged,
IsNewFile = isNewFile
};
}
public Stream OpenBlob(string contentHash, string extension)
{
var path = GetBlobPath(contentHash, extension);
if (!File.Exists(path))
return null;
return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
}
public bool BlobExists(string contentHash, string extension)
{
return File.Exists(GetBlobPath(contentHash, extension));
}
private async Task<bool> StoreBlobAsync(Stream stream, string contentHash, string extension)
{
var blobPath = GetBlobPath(contentHash, extension);
if (File.Exists(blobPath))
return false; // blob already exists (dedup)
var dir = Path.GetDirectoryName(blobPath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
using var fileStream = new FileStream(blobPath, FileMode.Create, FileAccess.Write);
await stream.CopyToAsync(fileStream);
return true; // new blob written
}
private string GetBlobPath(string contentHash, string extension)
{
var prefix1 = contentHash[..2];
var prefix2 = contentHash[2..4];
return Path.Combine(_options.OutputFolder, "blobs", prefix1, prefix2, $"{contentHash}.{extension}");
}
private static string BuildDrawingNumber(string equipment, string drawingNo)
{
if (!string.IsNullOrEmpty(equipment) && !string.IsNullOrEmpty(drawingNo))
return $"{equipment} {drawingNo}";
if (!string.IsNullOrEmpty(equipment))
return equipment;
return drawingNo ?? "";
}
private static string BuildDxfFileName(string drawingNo, string equipment, string itemNo, string originalFileName = null)
{
// No drawing number: use the original filename from the client
if (string.IsNullOrEmpty(drawingNo) && !string.IsNullOrEmpty(originalFileName))
{
return originalFileName.EndsWith(".dxf", StringComparison.OrdinalIgnoreCase)
? originalFileName
: originalFileName + ".dxf";
}
var drawingNumber = BuildDrawingNumber(equipment, drawingNo);
var paddedItem = (itemNo ?? "").PadLeft(2, '0');
if (!string.IsNullOrEmpty(drawingNumber) && !string.IsNullOrEmpty(itemNo))
return $"{drawingNumber} PT{paddedItem}.dxf";
return $"PT{paddedItem}.dxf";
}
}
}

View File

@@ -0,0 +1,39 @@
using FabWorks.Core.Models;
using FabWorks.Core.PressBrake;
using System.Security.Cryptography;
namespace FabWorks.Api.Services
{
public class FormProgramService
{
public FormProgram ParseFromFile(string filePath)
{
var pgm = FabWorks.Core.PressBrake.Program.Load(filePath);
var hash = ComputeFileHash(filePath);
return new FormProgram
{
ProgramFilePath = filePath,
ContentHash = hash,
ProgramName = pgm.ProgName ?? "",
Thickness = pgm.MatThick > 0 ? pgm.MatThick : null,
MaterialType = pgm.MatType.ToString(),
KFactor = pgm.KFactor > 0 ? pgm.KFactor : null,
BendCount = pgm.Steps.Count,
UpperToolNames = string.Join(", ", pgm.UpperToolSets
.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()),
LowerToolNames = string.Join(", ", pgm.LowerToolSets
.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)).Distinct()),
SetupNotes = pgm.SetupNotes ?? ""
};
}
private 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();
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"FabWorksDb": "Server=localhost;Database=ExportDxfDb;Trusted_Connection=True;TrustServerCertificate=True;"
},
"FileStorage": {
"OutputFolder": "C:\\ExportDXF\\Output"
}
}

View File

@@ -0,0 +1,824 @@
:root {
--bg-deep: #f0f1f3;
--bg: #f8f9fa;
--surface: #ffffff;
--surface-raised: #ffffff;
--border: #d0d5dd;
--border-subtle: #e4e7ec;
--text: #1a1a1a;
--text-secondary: #475467;
--text-dim: #667085;
--cyan: #0975b0;
--cyan-dim: rgba(9, 117, 176, 0.1);
--cyan-glow: rgba(9, 117, 176, 0.2);
--amber: #b54708;
--amber-dim: rgba(181, 71, 8, 0.08);
--green: #067647;
--green-dim: rgba(6, 118, 71, 0.08);
--red: #d92d20;
--sidebar-w: 64px;
--font-display: 'Outfit', sans-serif;
--font-body: 'IBM Plex Sans', sans-serif;
--font-mono: 'IBM Plex Mono', monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-body);
background: var(--bg);
color: var(--text);
display: flex;
min-height: 100vh;
overflow-x: hidden;
}
/* ─── Sidebar ─── */
.sidebar {
width: var(--sidebar-w);
background: var(--bg-deep);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
align-items: center;
position: fixed;
top: 0; left: 0; bottom: 0;
z-index: 50;
padding-top: 8px;
}
.sidebar-brand {
width: 40px; height: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
position: relative;
}
.sidebar-brand::after {
content: '';
position: absolute;
bottom: -12px;
left: 8px; right: 8px;
height: 1px;
background: var(--border);
}
.sidebar-brand svg {
width: 26px; height: 26px;
color: var(--cyan);
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 4px;
padding-top: 16px;
width: 100%;
}
.nav-item {
display: flex;
align-items: center;
justify-content: center;
width: 44px; height: 44px;
margin: 0 auto;
color: var(--text-dim);
text-decoration: none;
cursor: pointer;
border-radius: 8px;
transition: all 0.2s;
position: relative;
}
.nav-item:hover {
color: var(--text-secondary);
background: var(--surface);
}
.nav-item.active {
color: var(--cyan);
background: var(--cyan-dim);
}
.nav-item.active::before {
content: '';
position: absolute;
left: -10px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 20px;
background: var(--cyan);
border-radius: 0 2px 2px 0;
}
.nav-item svg { width: 20px; height: 20px; }
.nav-tooltip {
position: absolute;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: var(--text);
border: 1px solid var(--border);
color: #fff;
padding: 4px 10px;
border-radius: 4px;
font-size: 13px;
font-family: var(--font-body);
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
z-index: 100;
}
.nav-item:hover .nav-tooltip { opacity: 1; }
/* ─── Main ─── */
.main {
margin-left: var(--sidebar-w);
flex: 1;
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
z-index: 1;
}
.topbar {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 0 32px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 40;
}
.topbar-left {
display: flex;
align-items: center;
gap: 12px;
}
.topbar h2 {
font-family: var(--font-display);
font-size: 18px;
font-weight: 600;
letter-spacing: -0.01em;
}
.topbar-tag {
font-family: var(--font-mono);
font-size: 12px;
color: var(--cyan);
background: var(--cyan-dim);
padding: 2px 8px;
border-radius: 3px;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.page-content {
padding: 28px 32px;
flex: 1;
}
/* ─── Animations ─── */
@keyframes fadeSlideIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in {
animation: fadeSlideIn 0.3s ease forwards;
opacity: 0;
}
.animate-in:nth-child(1) { animation-delay: 0.04s; }
.animate-in:nth-child(2) { animation-delay: 0.08s; }
.animate-in:nth-child(3) { animation-delay: 0.12s; }
.animate-in:nth-child(4) { animation-delay: 0.16s; }
/* ─── Cards ─── */
.card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.card-header {
padding: 14px 18px;
border-bottom: 1px solid var(--border-subtle);
font-family: var(--font-display);
font-weight: 600;
font-size: 14px;
letter-spacing: 0.02em;
display: flex;
align-items: center;
justify-content: space-between;
text-transform: uppercase;
color: var(--text-secondary);
}
.card-body { padding: 18px; }
/* ─── Stats ─── */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 24px;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px;
padding: 18px 20px;
position: relative;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 100%; height: 3px;
background: linear-gradient(90deg, var(--cyan), transparent);
opacity: 0.5;
}
.stat-label {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1.5px;
}
.stat-value {
font-family: var(--font-display);
font-size: 32px;
font-weight: 700;
margin-top: 4px;
color: var(--text);
letter-spacing: -0.02em;
}
.stat-value.stat-sm {
font-size: 15px;
font-weight: 500;
font-family: var(--font-mono);
}
/* ─── Tables ─── */
table { width: 100%; border-collapse: collapse; }
th {
text-align: left;
padding: 10px 16px;
background: var(--bg);
border-bottom: 1px solid var(--border);
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-dim);
font-weight: 600;
white-space: nowrap;
}
td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle);
font-size: 14px;
}
tbody tr { transition: background 0.1s; }
tbody tr:hover td { background: var(--cyan-dim); }
tbody tr:last-child td { border-bottom: none; }
/* ─── Badges ─── */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 3px;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.badge svg { width: 14px; height: 14px; flex-shrink: 0; }
.badge-cyan { background: var(--cyan-dim); color: var(--cyan); }
.badge-amber { background: var(--amber-dim); color: var(--amber); }
.badge-green { background: var(--green-dim); color: var(--green); }
.badge-count {
background: var(--bg);
color: var(--text-secondary);
border: 1px solid var(--border);
}
/* ─── Buttons ─── */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border-radius: 4px;
font-family: var(--font-body);
font-size: 13px;
font-weight: 500;
cursor: pointer;
text-decoration: none;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text-secondary);
transition: all 0.15s;
white-space: nowrap;
}
.btn:hover {
background: var(--bg);
color: var(--text);
border-color: var(--text-dim);
}
.btn svg { width: 14px; height: 14px; }
.btn-cyan {
background: var(--cyan-dim);
color: var(--cyan);
border-color: rgba(9, 117, 176, 0.25);
}
.btn-cyan:hover {
background: rgba(9, 117, 176, 0.15);
border-color: rgba(9, 117, 176, 0.4);
color: var(--cyan);
}
.btn-amber {
background: var(--amber-dim);
color: var(--amber);
border-color: rgba(181, 71, 8, 0.25);
}
.btn-amber:hover {
background: rgba(181, 71, 8, 0.15);
border-color: rgba(181, 71, 8, 0.4);
color: var(--amber);
}
.btn-red {
background: rgba(217, 45, 32, 0.08);
color: var(--red);
border-color: rgba(217, 45, 32, 0.25);
}
.btn-red:hover {
background: rgba(217, 45, 32, 0.15);
border-color: rgba(217, 45, 32, 0.4);
color: var(--red);
}
.btn-sm { padding: 4px 10px; font-size: 12px; }
/* ─── Search ─── */
.search-box {
display: flex;
align-items: center;
gap: 8px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0 12px;
height: 36px;
width: 300px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-box:focus-within {
border-color: var(--cyan);
box-shadow: 0 0 0 2px var(--cyan-dim);
}
.search-box svg {
width: 16px; height: 16px;
color: var(--text-dim);
flex-shrink: 0;
}
.search-box input {
border: none;
outline: none;
font-family: var(--font-body);
font-size: 14px;
width: 100%;
background: transparent;
color: var(--text);
}
.search-box input::placeholder { color: var(--text-dim); }
/* ─── Clickable ─── */
.clickable { cursor: pointer; }
/* ─── Detail sections ─── */
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.detail-field label {
display: block;
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-dim);
margin-bottom: 4px;
}
.detail-field .value {
font-size: 15px;
font-weight: 500;
word-break: break-all;
}
.detail-field .value.mono {
font-family: var(--font-mono);
font-size: 13px;
color: var(--text-secondary);
}
/* ─── Back link ─── */
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--text-dim);
text-decoration: none;
font-size: 13px;
cursor: pointer;
margin-bottom: 20px;
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.5px;
transition: color 0.15s;
}
.back-link:hover { color: var(--cyan); }
.back-link svg { width: 14px; height: 14px; }
/* ─── BOM Expansion ─── */
.bom-expand-row td {
padding: 0 !important;
background: var(--bg) !important;
}
.bom-expand-content {
padding: 16px 16px 16px 48px;
border-left: 3px solid var(--cyan-dim);
margin-left: 16px;
}
.bom-expand-content .info-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 6px 24px;
}
.bom-expand-content .info-item {
font-size: 13px;
padding: 2px 0;
}
.bom-expand-content .info-item .lbl {
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-right: 6px;
}
.bom-expand-content .info-item .val {
font-family: var(--font-mono);
color: var(--text);
}
.bom-section-title {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--cyan);
margin: 14px 0 8px;
display: flex;
align-items: center;
gap: 8px;
}
.bom-section-title svg { width: 14px; height: 14px; flex-shrink: 0; }
.bom-section-title:first-child { margin-top: 0; }
.bom-section-title::after {
content: '';
flex: 1;
height: 1px;
background: var(--border-subtle);
}
/* ─── File Browser ─── */
.breadcrumb {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 13px;
margin-bottom: 16px;
flex-wrap: wrap;
padding: 8px 14px;
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 4px;
}
.breadcrumb a {
color: var(--cyan);
text-decoration: none;
cursor: pointer;
transition: opacity 0.15s;
}
.breadcrumb a:hover { opacity: 0.7; }
.breadcrumb .sep { color: var(--text-dim); font-size: 11px; }
.breadcrumb .current { color: var(--text); font-weight: 500; }
.file-name-cell {
display: flex;
align-items: center;
gap: 10px;
}
.file-name-cell svg { width: 18px; height: 18px; flex-shrink: 0; }
.file-name-cell a {
color: var(--text);
text-decoration: none;
cursor: pointer;
transition: color 0.15s;
}
.file-name-cell a:hover { color: var(--cyan); }
/* ─── Loading / Empty ─── */
.loading, .empty {
text-align: center;
padding: 60px 24px;
color: var(--text-dim);
font-size: 14px;
font-family: var(--font-mono);
}
.loading::before {
content: '';
display: block;
width: 24px;
height: 24px;
border: 2px solid var(--border);
border-top-color: var(--cyan);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 0 auto 12px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ─── Chevron toggle ─── */
.chevron-toggle {
display: inline-flex;
width: 18px; height: 18px;
align-items: center;
justify-content: center;
transition: transform 0.2s;
color: var(--text-dim);
}
.chevron-toggle.open {
transform: rotate(90deg);
color: var(--cyan);
}
/* ─── Drawing cards grid ─── */
.drawings-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.drawing-card {
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px;
padding: 18px 20px;
cursor: pointer;
transition: all 0.2s;
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.drawing-card:hover {
border-color: var(--cyan);
background: var(--cyan-dim);
box-shadow: 0 2px 8px rgba(9, 117, 176, 0.1);
}
.drawing-card-title {
font-family: var(--font-display);
font-size: 16px;
font-weight: 600;
margin-bottom: 2px;
}
.drawing-card-sub {
font-family: var(--font-mono);
font-size: 12px;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 1px;
}
/* ─── Equipment Groups ─── */
.equip-group {
margin-bottom: 16px;
}
.equip-group:last-child { margin-bottom: 0; }
.equip-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--surface);
border: 1px solid var(--border-subtle);
border-radius: 6px 6px 0 0;
cursor: pointer;
transition: all 0.15s;
user-select: none;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.equip-header:hover { background: var(--cyan-dim); }
.equip-header .chevron-toggle { flex-shrink: 0; }
.equip-header-title {
font-family: var(--font-display);
font-size: 16px;
font-weight: 600;
}
.equip-header-number {
font-family: var(--font-mono);
font-size: 15px;
color: var(--cyan);
font-weight: 600;
}
.equip-header-meta {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
}
.equip-header-stat {
font-family: var(--font-mono);
font-size: 13px;
color: var(--text-dim);
}
.equip-header-stat strong {
color: var(--text-secondary);
}
.equip-body {
border: 1px solid var(--border-subtle);
border-top: none;
border-radius: 0 0 6px 6px;
overflow: hidden;
}
.equip-body table { margin: 0; }
.equip-group.collapsed .equip-body { display: none; }
.equip-group.collapsed .equip-header { border-radius: 6px; }
/* ─── Modal ─── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.modal-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
width: 90%;
max-width: 640px;
max-height: 80vh;
display: flex;
flex-direction: column;
animation: fadeSlideIn 0.2s ease forwards;
}
.modal-header {
padding: 14px 18px;
border-bottom: 1px solid var(--border-subtle);
font-family: var(--font-display);
font-weight: 600;
font-size: 14px;
letter-spacing: 0.02em;
display: flex;
align-items: center;
gap: 10px;
text-transform: uppercase;
color: var(--text-secondary);
}
.modal-header svg { width: 16px; height: 16px; }
.modal-body {
overflow-y: auto;
flex: 1;
}
.btn-green {
background: var(--green-dim);
color: var(--green);
border-color: rgba(6, 118, 71, 0.25);
}
.btn-green:hover {
background: rgba(6, 118, 71, 0.15);
border-color: rgba(6, 118, 71, 0.4);
color: var(--green);
}
/* ─── Toast ─── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: var(--text);
color: #fff;
padding: 8px 20px;
border-radius: 4px;
font-family: var(--font-mono);
font-size: 13px;
z-index: 300;
animation: fadeSlideIn 0.2s ease, fadeOut 0.3s ease 2s forwards;
}
@keyframes fadeOut { to { opacity: 0; } }
/* ─── Responsive ─── */
@media (max-width: 768px) {
.sidebar { display: none; }
.main { margin-left: 0; }
.search-box { width: 100%; }
.topbar { padding: 0 16px; }
.page-content { padding: 16px; }
}

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FabWorks</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<aside class="sidebar">
<div class="sidebar-brand">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 20V8l4-4h6l2 2h6a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z"/>
<path d="M8 10v6" opacity="0.5"/>
<path d="M12 8v8" opacity="0.5"/>
<path d="M16 11v3" opacity="0.5"/>
</svg>
</div>
<nav class="sidebar-nav">
<a class="nav-item active" data-page="exports" onclick="router.go('exports')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
<span class="nav-tooltip">Exports</span>
</a>
<a class="nav-item" data-page="drawings" onclick="router.go('drawings')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>
<span class="nav-tooltip">Drawings</span>
</a>
<a class="nav-item" data-page="files" onclick="router.go('files')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<span class="nav-tooltip">Files</span>
</a>
</nav>
</aside>
<div class="main">
<div class="topbar">
<div class="topbar-left">
<h2 id="page-title">Exports</h2>
<span class="topbar-tag" id="page-tag"></span>
</div>
<div id="topbar-actions"></div>
</div>
<div class="page-content" id="page-content"></div>
</div>
<script src="js/icons.js?v=3"></script>
<script src="js/helpers.js?v=3"></script>
<script src="js/components.js?v=3"></script>
<script src="js/pages.js?v=3"></script>
<script src="js/router.js?v=3"></script>
<script>router.init();</script>
</body>
</html>

View File

@@ -0,0 +1,155 @@
/* ─── BOM Detail Expansion ─── */
function renderBomDetails(b) {
let html = '<div class="bom-expand-content">';
if (b.cutTemplate) {
const ct = b.cutTemplate;
const displayName = ct.dxfFilePath?.split(/[/\\]/).pop() || '';
html += `
<div class="bom-section-title">${icons.laser} Cut Template</div>
<div class="info-grid">
<div class="info-item"><span class="lbl">File</span><span class="val">${esc(displayName)}</span></div>
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(ct.thickness)}</span></div>
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${ct.kFactor != null ? ct.kFactor : '\u2014'}</span></div>
<div class="info-item"><span class="lbl">Bend Radius</span><span class="val">${ct.defaultBendRadius != null ? ct.defaultBendRadius.toFixed(4) + '"' : '\u2014'}</span></div>
</div>`;
if (ct.contentHash) {
html += `<div style="margin-top:10px">
<a class="btn btn-cyan btn-sm" href="/api/files/blob/${encodeURIComponent(ct.contentHash)}?ext=dxf&download=true&name=${encodeURIComponent(displayName)}" onclick="event.stopPropagation()">${icons.download} Download DXF</a>
<span style="font-family:var(--font-mono);font-size:13px;color:var(--text-dim);margin-left:8px">${esc(displayName)}</span>
</div>`;
}
}
if (b.formProgram) {
const fp = b.formProgram;
html += `
<div class="bom-section-title">${icons.bend} Form Program</div>
<div class="info-grid">
<div class="info-item"><span class="lbl">Program</span><span class="val">${esc(fp.programName)}</span></div>
<div class="info-item"><span class="lbl">Thickness</span><span class="val">${fmtThickness(fp.thickness)}</span></div>
<div class="info-item"><span class="lbl">Material</span><span class="val">${esc(fp.materialType)}</span></div>
<div class="info-item"><span class="lbl">K-Factor</span><span class="val">${fp.kFactor != null ? fp.kFactor : '\u2014'}</span></div>
<div class="info-item"><span class="lbl">Bends</span><span class="val">${fp.bendCount}</span></div>
<div class="info-item"><span class="lbl">Upper Tools</span><span class="val">${esc(fp.upperToolNames) || '\u2014'}</span></div>
<div class="info-item"><span class="lbl">Lower Tools</span><span class="val">${esc(fp.lowerToolNames) || '\u2014'}</span></div>
</div>
${fp.setupNotes ? `<div style="margin-top:8px;padding:8px 12px;background:var(--amber-dim);border-radius:4px;font-size:13px;font-family:var(--font-mono);color:var(--amber)"><span class="lbl">Setup Notes</span>${esc(fp.setupNotes)}</div>` : ''}`;
}
html += '</div>';
return html;
}
function toggleEquipGroup(id) {
const group = document.getElementById(id);
const icon = document.getElementById(id + '-icon');
if (!group) return;
group.classList.toggle('collapsed');
if (icon) icon.classList.toggle('open', !group.classList.contains('collapsed'));
}
function toggleBomRow(id) {
const row = document.getElementById(id);
const icon = document.getElementById(id + '-icon');
if (!row) return;
const visible = row.style.display !== 'none';
row.style.display = visible ? 'none' : '';
if (icon) icon.classList.toggle('open', !visible);
}
/* ─── Cut List Modal ─── */
function showCutListModal(bomItems) {
const cutItems = bomItems.filter(b => b.cutTemplate);
if (cutItems.length === 0) {
showToast('No cut templates found');
return;
}
const rows = cutItems.map(b => {
const ct = b.cutTemplate;
const name = ct.cutTemplateName || ct.dxfFilePath?.split(/[/\\]/).pop()?.replace(/\.dxf$/i, '') || b.partName || '';
const qty = b.qty ?? '';
return { name, qty };
});
const tableRows = rows.map((r, i) => `
<tr style="animation: fadeSlideIn 0.15s ease ${0.02 * i}s forwards; opacity: 0">
<td style="font-family:var(--font-mono);font-weight:600">${esc(r.name)}</td>
<td style="font-family:var(--font-mono);text-align:center">${r.qty}</td>
</tr>`).join('');
// Remove existing modal if any
const existing = document.getElementById('cut-list-modal');
if (existing) existing.remove();
const modal = document.createElement('div');
modal.id = 'cut-list-modal';
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-panel">
<div class="modal-header">
<span>${icons.laser} Cut List</span>
<span class="badge badge-count">${cutItems.length} templates</span>
<span style="margin-left:auto;display:flex;gap:6px">
<button class="btn btn-cyan btn-sm" onclick="copyCutList()" id="copy-cut-list-btn">${icons.clipboard} Copy</button>
<button class="btn btn-sm" onclick="closeCutListModal()">${icons.close}</button>
</span>
</div>
<div class="modal-body">
<table>
<thead><tr>
<th>Name</th>
<th style="width:60px;text-align:center">Qty</th>
</tr></thead>
<tbody>${tableRows}</tbody>
</table>
</div>
</div>`;
document.body.appendChild(modal);
// Store data for copy
modal._cutData = rows;
// Close on backdrop click
modal.addEventListener('click', e => { if (e.target === modal) closeCutListModal(); });
// Close on Escape
modal._keyHandler = e => { if (e.key === 'Escape') closeCutListModal(); };
document.addEventListener('keydown', modal._keyHandler);
}
function closeCutListModal() {
const modal = document.getElementById('cut-list-modal');
if (!modal) return;
document.removeEventListener('keydown', modal._keyHandler);
modal.remove();
}
function copyCutList() {
const modal = document.getElementById('cut-list-modal');
if (!modal || !modal._cutData) return;
const text = modal._cutData.map(r => `${r.name}\t${r.qty}`).join('\n');
navigator.clipboard.writeText(text).then(() => {
const btn = document.getElementById('copy-cut-list-btn');
if (btn) {
btn.innerHTML = `${icons.check} Copied!`;
btn.classList.remove('btn-cyan');
btn.classList.add('btn-green');
setTimeout(() => {
btn.innerHTML = `${icons.clipboard} Copy`;
btn.classList.remove('btn-green');
btn.classList.add('btn-cyan');
}, 2000);
}
});
}
function showToast(msg) {
const t = document.createElement('div');
t.className = 'toast';
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 2500);
}

View File

@@ -0,0 +1,50 @@
function fmtSize(b) {
if (!b) return '0 B';
const k = 1024, s = ['B','KB','MB','GB'];
const i = Math.floor(Math.log(b) / Math.log(k));
return parseFloat((b / Math.pow(k, i)).toFixed(1)) + ' ' + s[i];
}
function fmtDate(d) {
if (!d) return '';
const dt = new Date(d);
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) +
' ' + dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function fmtThickness(t) {
if (t == null) return '\u2014';
return `<span style="font-family:var(--font-mono)">${t.toFixed(4)}"</span>`;
}
function esc(s) {
return s ? s.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;') : '';
}
function setPage(title, tag = '') {
document.getElementById('page-title').textContent = title;
document.getElementById('page-tag').textContent = tag;
document.getElementById('page-tag').style.display = tag ? '' : 'none';
}
const api = {
async get(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
return r.json();
},
async del(url) {
const r = await fetch(url, { method: 'DELETE' });
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
}
};
async function deleteExport(id) {
if (!confirm('Delete this export record? This cannot be undone.')) return;
try {
await api.del(`/api/exports/${id}`);
router.dispatch();
} catch (err) {
alert('Failed to delete: ' + err.message);
}
}

View File

@@ -0,0 +1,23 @@
const icons = {
search: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
folder: `<svg viewBox="0 0 24 24" fill="var(--amber-dim)" stroke="var(--amber)" stroke-width="1.5"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
fileDxf: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--cyan)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
filePdf: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--red)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
fileGeneric: `<svg viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>`,
download: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
back: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>`,
chevron: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`,
laser: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--cyan)" stroke-width="1.2"><circle cx="8" cy="8" r="2"/><path d="M8 2v3M8 11v3M2 8h3M11 8h3" opacity="0.5"/></svg>`,
bend: `<svg viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.2"><path d="M3 13V7a4 4 0 0 1 4-4h6"/><polyline points="10 6 13 3 10 0" transform="translate(0,2)"/></svg>`,
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
clipboard: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="2" width="6" height="4" rx="1"/><path d="M9 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2h-2"/><line x1="9" y1="12" x2="15" y2="12"/><line x1="9" y1="16" x2="15" y2="16"/></svg>`,
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>`,
close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
};
function fileIcon(name) {
const ext = name.split('.').pop().toLowerCase();
if (ext === 'dxf') return icons.fileDxf;
if (ext === 'pdf') return icons.filePdf;
return icons.fileGeneric;
}

View File

@@ -0,0 +1,393 @@
const pages = {
async exports(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Exports');
const searchVal = params.q || '';
actions.innerHTML = `
<div class="search-box">
${icons.search}
<input type="text" id="export-search" placeholder="Search drawing, part, user..." value="${esc(searchVal)}">
</div>`;
content.innerHTML = `<div class="loading">Loading exports</div>`;
const searchInput = document.getElementById('export-search');
let debounce;
searchInput.addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('exports', { q: searchInput.value }), 400);
});
try {
const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : '';
const data = await api.get(`/api/exports?take=500${searchQ}`);
if (data.items.length === 0) {
content.innerHTML = `<div class="empty">No exports found.</div>`;
return;
}
setPage('Exports', `${data.items.length} exports`);
const rows = data.items.map((e, i) => `
<tr class="clickable" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(e.drawingNumber)}', eid: '${e.id}'})" style="animation: fadeSlideIn 0.2s ease ${0.02 * Math.min(i, 25)}s forwards; opacity: 0">
<td style="font-family:var(--font-mono);color:var(--text-dim);font-size:13px">${e.id}</td>
<td><strong>${esc(e.drawingNumber) || '<span style="color:var(--text-dim)">\u2014</span>'}</strong></td>
<td style="color:var(--text-secondary);font-size:13px">${esc(e.title) || ''}</td>
<td><span class="badge badge-count">${e.bomItemCount}</span></td>
<td style="color:var(--text-secondary)">${esc(e.exportedBy)}</td>
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary);white-space:nowrap">${fmtDate(e.exportedAt)}</td>
<td><button class="btn btn-red btn-sm" onclick="event.stopPropagation();deleteExport(${e.id})">${icons.trash}</button></td>
</tr>`).join('');
content.innerHTML = `
<div class="card animate-in">
<table>
<thead><tr>
<th style="width:50px">#</th>
<th>Drawing</th>
<th>Title</th>
<th style="width:80px">Items</th>
<th>Exported By</th>
<th style="width:180px">Date</th>
<th style="width:50px"></th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
},
async drawings(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Drawings');
const searchVal = (params && params.q) || '';
actions.innerHTML = `
<div class="search-box">
${icons.search}
<input type="text" id="drawing-search" placeholder="Search drawing, part, user..." value="${esc(searchVal)}">
</div>`;
content.innerHTML = `<div class="loading">Loading drawings</div>`;
const searchInput = document.getElementById('drawing-search');
let debounce;
searchInput.addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('drawings', { q: searchInput.value }), 400);
});
try {
const searchQ = searchVal ? `&search=${encodeURIComponent(searchVal)}` : '';
const data = await api.get(`/api/exports?take=500${searchQ}`);
if (data.items.length === 0) {
content.innerHTML = `<div class="empty">No drawings found.</div>`;
return;
}
// Deduplicate: keep only the latest export per drawing number
const seen = new Set();
const unique = data.items.filter(e => {
const dn = e.drawingNumber || '';
if (seen.has(dn)) return false;
seen.add(dn);
return true;
});
// Group by equipment number (first token of drawing number)
const groups = new Map();
unique.forEach(e => {
const dn = e.drawingNumber || '';
const spaceIdx = dn.indexOf(' ');
const equip = spaceIdx > 0 ? dn.substring(0, spaceIdx) : (dn || 'Other');
if (!groups.has(equip)) groups.set(equip, []);
groups.get(equip).push(e);
});
// Sort equipment groups by number descending (most recent equipment first)
const sortedGroups = [...groups.entries()].sort((a, b) => {
const numA = parseInt(a[0]) || 0;
const numB = parseInt(b[0]) || 0;
return numB - numA;
});
const uniqueEquip = sortedGroups.length;
const uniqueDrawings = unique.length;
setPage('Drawings', `${uniqueDrawings} drawings / ${uniqueEquip} equipment`);
const groupsHtml = sortedGroups.map(([equip, items], gi) => {
const totalBom = items.reduce((s, e) => s + e.bomItemCount, 0);
const rows = items.map((e, i) => {
const dn = e.drawingNumber || '';
const spaceIdx = dn.indexOf(' ');
const drawingPart = spaceIdx > 0 ? dn.substring(spaceIdx + 1) : dn;
return `
<tr class="clickable" onclick="router.go('drawing-detail', {id: '${encodeURIComponent(e.drawingNumber)}'})" style="animation: fadeSlideIn 0.2s ease ${0.02 * i}s forwards; opacity: 0">
<td><strong>${esc(drawingPart) || '<span style="color:var(--text-dim)">\u2014</span>'}</strong></td>
<td style="color:var(--text-secondary);font-size:13px">${esc(e.title) || ''}</td>
<td><span class="badge badge-count">${e.bomItemCount}</span></td>
<td style="color:var(--text-secondary)">${esc(e.exportedBy)}</td>
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary);white-space:nowrap">${fmtDate(e.exportedAt)}</td>
</tr>`;
}).join('');
return `
<div class="equip-group animate-in" id="equip-${esc(equip)}" style="animation-delay:${0.04 * gi}s">
<div class="equip-header" onclick="toggleEquipGroup('equip-${esc(equip)}')">
<span class="chevron-toggle open" id="equip-${esc(equip)}-icon">${icons.chevron}</span>
<span class="equip-header-number">${esc(equip)}</span>
<div class="equip-header-meta">
<span class="equip-header-stat"><strong>${items.length}</strong> drawings</span>
<span class="equip-header-stat"><strong>${totalBom}</strong> items</span>
</div>
</div>
<div class="equip-body">
<table>
<thead><tr>
<th>Drawing</th>
<th>Title</th>
<th style="width:80px">Items</th>
<th>Exported By</th>
<th style="width:180px">Latest Export</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>
</div>`;
}).join('');
content.innerHTML = `
<div class="stats-grid">
<div class="stat-card animate-in"><div class="stat-label">Drawings</div><div class="stat-value">${uniqueDrawings}</div></div>
<div class="stat-card animate-in"><div class="stat-label">Equipment</div><div class="stat-value">${uniqueEquip}</div></div>
</div>
${groupsHtml}`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
},
async drawingDetail(drawingEncoded, params) {
const drawingNumber = decodeURIComponent(drawingEncoded);
const exportId = params?.eid ? parseInt(params.eid) : null;
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage(drawingNumber, 'drawing');
actions.innerHTML = '';
content.innerHTML = `<div class="loading">Loading drawing</div>`;
try {
const exports = await api.get(`/api/exports/by-drawing?drawingNumber=${encodeURIComponent(drawingNumber)}`);
if (exports.length === 0) {
content.innerHTML = `
<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>
<div class="empty">No exports found for this drawing.</div>`;
return;
}
let allBom;
const singleExport = exportId ? exports.find(e => e.id === exportId) : null;
if (singleExport) {
// Viewing a specific export - show only its BOM items
allBom = (singleExport.bomItems || []).map(b => ({ ...b, exportId: singleExport.id, exportedAt: singleExport.exportedAt }));
} else {
// Viewing drawing overview - deduplicate by itemNo, keeping latest revision (exports are newest-first)
const bomByItem = new Map();
exports.forEach(exp => {
(exp.bomItems || []).forEach(b => {
if (!bomByItem.has(b.itemNo)) {
bomByItem.set(b.itemNo, { ...b, exportId: exp.id, exportedAt: exp.exportedAt });
}
});
});
allBom = [...bomByItem.values()];
}
// Store for cut list modal
window._currentBom = allBom;
const bomRows = allBom.map((b, i) => {
const hasDetails = b.cutTemplate || b.formProgram;
const toggleId = `dbom-${b.id}`;
return `
<tr class="${hasDetails ? 'clickable' : ''}" ${hasDetails ? `onclick="toggleBomRow('${toggleId}')"` : ''} style="animation: fadeSlideIn 0.25s ease ${0.03 * i}s forwards; opacity: 0">
<td style="width:32px">${hasDetails ? `<span class="chevron-toggle" id="${toggleId}-icon">${icons.chevron}</span>` : ''}</td>
<td style="font-family:var(--font-mono);font-weight:600;color:var(--cyan)">${esc(b.itemNo)}</td>
<td><strong>${esc(b.partName)}</strong></td>
<td style="font-family:var(--font-mono);text-align:center;font-size:13px">${b.cutTemplate?.revision ?? ''}</td>
<td style="color:var(--text-secondary)">${esc(b.description)}</td>
<td><span style="font-family:var(--font-mono);font-size:13px">${esc(b.material)}</span></td>
<td style="font-family:var(--font-mono);text-align:center">${b.qty ?? ''}</td>
<td style="font-family:var(--font-mono);text-align:center">${b.totalQty ?? ''}</td>
<td>
${b.cutTemplate ? `<span class="badge badge-cyan">${icons.laser} DXF</span>` : ''}
${b.formProgram ? `<span class="badge badge-amber">${icons.bend} Form</span>` : ''}
</td>
</tr>
${hasDetails ? `<tr class="bom-expand-row" id="${toggleId}" style="display:none"><td colspan="9">${renderBomDetails(b)}</td></tr>` : ''}`;
}).join('');
const backLink = singleExport
? `<a class="back-link" onclick="router.go('exports')">${icons.back} Back to exports</a>`
: `<a class="back-link" onclick="router.go('drawings')">${icons.back} Back to drawings</a>`;
const statsHtml = singleExport
? `<div class="stats-grid">
<div class="stat-card animate-in"><div class="stat-label">Exported By</div><div class="stat-value stat-sm">${esc(singleExport.exportedBy)}</div></div>
<div class="stat-card animate-in"><div class="stat-label">BOM Items</div><div class="stat-value">${allBom.length}</div></div>
<div class="stat-card animate-in"><div class="stat-label">Exported</div><div class="stat-value stat-sm">${fmtDate(singleExport.exportedAt)}</div></div>
</div>`
: `<div class="stats-grid">
<div class="stat-card animate-in"><div class="stat-label">Exports</div><div class="stat-value">${exports.length}</div></div>
<div class="stat-card animate-in"><div class="stat-label">BOM Items</div><div class="stat-value">${allBom.length}</div></div>
<div class="stat-card animate-in"><div class="stat-label">Latest Export</div><div class="stat-value stat-sm">${fmtDate(exports[0].exportedAt)}</div></div>
</div>`;
const bomHeader = singleExport ? 'BOM Items' : 'All BOM Items';
const activeExport = singleExport || exports[0];
const dxfCount = allBom.filter(b => b.cutTemplate?.contentHash).length;
const pdfHash = activeExport.pdfContentHash;
const pdfName = encodeURIComponent((drawingNumber || 'drawing') + '.pdf');
content.innerHTML = `
${backLink}
${statsHtml}
<div class="card animate-in">
<div class="card-header">
${bomHeader}
<span class="badge badge-count">${allBom.length} items</span>
<span style="margin-left:auto;display:flex;gap:6px">
${dxfCount > 0 ? `<button class="btn btn-sm" onclick="showCutListModal(window._currentBom)">${icons.clipboard} Cut List</button>` : ''}
${pdfHash ? `<a class="btn btn-amber btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(pdfHash)}&ext=pdf&name=${pdfName}">${icons.download} PDF</a>` : ''}
${dxfCount > 0 ? (singleExport
? `<a class="btn btn-cyan btn-sm" href="/api/exports/${activeExport.id}/download-dxfs">${icons.download} All DXFs</a>`
: `<a class="btn btn-cyan btn-sm" href="/api/exports/download-dxfs?drawingNumber=${encodeURIComponent(drawingNumber)}">${icons.download} All DXFs</a>`
) : ''}
</span>
</div>
${allBom.length ? `
<table>
<thead><tr>
<th style="width:32px"></th>
<th style="width:60px">Item</th>
<th>Part Name</th>
<th style="width:45px;text-align:center">Rev</th>
<th>Description</th>
<th>Material</th>
<th style="width:50px;text-align:center">Qty</th>
<th style="width:55px;text-align:center">Total</th>
<th style="width:120px">Data</th>
</tr></thead>
<tbody>${bomRows}</tbody>
</table>` : '<div class="empty">No BOM items.</div>'}
</div>`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
},
async files(params) {
const actions = document.getElementById('topbar-actions');
const content = document.getElementById('page-content');
setPage('Files');
const searchVal = params.q || '';
actions.innerHTML = `
<div style="display:flex;gap:8px;align-items:center">
<div class="search-box">
${icons.search}
<input type="text" id="file-search" placeholder="Search drawing number, filename..." value="${esc(searchVal)}">
</div>
<select id="file-type-filter" style="background:var(--surface);border:1px solid var(--border);border-radius:4px;padding:6px 10px;color:var(--text);font-family:var(--font-body);font-size:14px;height:36px">
<option value="">All types</option>
<option value="dxf">DXF only</option>
<option value="pdf">PDF only</option>
</select>
</div>`;
content.innerHTML = `<div class="loading">Loading files</div>`;
const searchInput = document.getElementById('file-search');
const typeFilter = document.getElementById('file-type-filter');
let debounce;
const refresh = () => {
clearTimeout(debounce);
debounce = setTimeout(() => router.go('files', { q: searchInput.value + (typeFilter.value ? '&type=' + typeFilter.value : '') }), 400);
};
searchInput.addEventListener('input', refresh);
typeFilter.addEventListener('change', refresh);
// Parse search and type from combined param
let searchQ = searchVal;
let typeQ = '';
if (searchVal.includes('&type=')) {
const parts = searchVal.split('&type=');
searchQ = parts[0];
typeQ = parts[1] || '';
searchInput.value = searchQ;
typeFilter.value = typeQ;
}
try {
let url = '/api/filebrowser/files?';
if (searchQ) url += `search=${encodeURIComponent(searchQ)}&`;
if (typeQ) url += `type=${encodeURIComponent(typeQ)}&`;
const data = await api.get(url);
setPage('Files', `${data.total} files`);
if (data.files.length === 0) {
content.innerHTML = `<div class="empty">No files found.</div>`;
return;
}
const rows = data.files.map((f, i) => {
const ext = f.fileType || f.fileName.split('.').pop().toLowerCase();
const hashShort = f.contentHash ? f.contentHash.substring(0, 12) : '';
return `
<tr style="animation: fadeSlideIn 0.25s ease ${0.02 * i}s forwards; opacity: 0">
<td><div class="file-name-cell">${ext === 'pdf' ? icons.filePdf : icons.fileDxf}<a href="/api/filebrowser/download?hash=${encodeURIComponent(f.contentHash)}&ext=${ext}&name=${encodeURIComponent(f.fileName)}">${esc(f.fileName)}</a></div></td>
<td><span class="badge ${ext === 'dxf' ? 'badge-cyan' : 'badge-amber'}">${ext.toUpperCase()}</span></td>
<td style="color:var(--text-secondary)">${esc(f.drawingNumber)}</td>
<td style="font-family:var(--font-mono);font-size:13px;text-align:center;color:var(--text-secondary)">${f.revision != null ? f.revision : '\u2014'}</td>
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${f.thickness != null ? f.thickness.toFixed(4) + '"' : '\u2014'}</td>
<td style="font-family:var(--font-mono);font-size:13px;color:var(--text-secondary)">${fmtDate(f.createdAt)}</td>
<td style="font-family:var(--font-mono);font-size:12px;color:var(--text-dim)">${esc(hashShort)}</td>
<td style="white-space:nowrap">
<a class="btn btn-cyan btn-sm" href="/api/filebrowser/download?hash=${encodeURIComponent(f.contentHash)}&ext=${ext}&name=${encodeURIComponent(f.fileName)}">${icons.download}</a>
</td>
</tr>`;
}).join('');
content.innerHTML = `
<div class="card animate-in">
<table>
<thead><tr>
<th>Name</th>
<th style="width:60px">Type</th>
<th>Drawing</th>
<th style="width:45px;text-align:center">Rev</th>
<th style="width:90px">Thickness</th>
<th style="width:170px">Date</th>
<th style="width:100px">Hash</th>
<th style="width:90px">Actions</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>
</div>`;
} catch (err) {
content.innerHTML = `<div class="empty">Error: ${esc(err.message)}</div>`;
}
}
};

View File

@@ -0,0 +1,36 @@
const router = {
go(page, params = {}) {
const qParts = [];
if (params.q) qParts.push('q=' + encodeURIComponent(params.q));
if (params.eid) qParts.push('eid=' + encodeURIComponent(params.eid));
const hash = page + (params.id ? '/' + params.id : '') + (qParts.length ? '?' + qParts.join('&') : '');
location.hash = hash;
},
parse() {
const h = location.hash.slice(1) || 'exports';
const [path, qs] = h.split('?');
const parts = path.split('/');
const params = {};
if (qs) qs.split('&').forEach(p => { const [k,v] = p.split('='); params[k] = decodeURIComponent(v); });
return { page: parts[0], id: parts[1], params };
},
init() {
window.addEventListener('hashchange', () => this.dispatch());
this.dispatch();
},
dispatch() {
const { page, id, params } = this.parse();
document.querySelectorAll('.nav-item').forEach(el => {
el.classList.toggle('active',
el.dataset.page === page ||
(page === 'drawing-detail' && el.dataset.page === 'drawings'));
});
switch(page) {
case 'exports': pages.exports(params); break;
case 'drawings': pages.drawings(params); break;
case 'drawing-detail': pages.drawingDetail(id, params); break;
case 'files': pages.files(params); break;
default: pages.exports(params);
}
}
};

View File

@@ -1,32 +1,17 @@
using ExportDXF.Models; using FabWorks.Core.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Configuration;
namespace ExportDXF.Data namespace FabWorks.Core.Data
{ {
public class ExportDxfDbContext : DbContext public class FabWorksDbContext : DbContext
{ {
public DbSet<ExportRecord> ExportRecords { get; set; } public DbSet<ExportRecord> ExportRecords { get; set; }
public DbSet<BomItem> BomItems { get; set; } public DbSet<BomItem> BomItems { get; set; }
public DbSet<CutTemplate> CutTemplates { get; set; } public DbSet<CutTemplate> CutTemplates { get; set; }
public DbSet<FormProgram> FormPrograms { get; set; }
public DbSet<Drawing> Drawings { get; set; }
public ExportDxfDbContext() : base() public FabWorksDbContext(DbContextOptions<FabWorksDbContext> options) : base(options) { }
{
}
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) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -36,6 +21,9 @@ namespace ExportDXF.Data
{ {
entity.HasKey(e => e.Id); entity.HasKey(e => e.Id);
entity.Property(e => e.DrawingNumber).HasMaxLength(100); entity.Property(e => e.DrawingNumber).HasMaxLength(100);
entity.Property(e => e.Title).HasMaxLength(200);
entity.Property(e => e.EquipmentNo).HasMaxLength(50);
entity.Property(e => e.DrawingNo).HasMaxLength(50);
entity.Property(e => e.SourceFilePath).HasMaxLength(500); entity.Property(e => e.SourceFilePath).HasMaxLength(500);
entity.Property(e => e.OutputFolder).HasMaxLength(500); entity.Property(e => e.OutputFolder).HasMaxLength(500);
entity.Property(e => e.ExportedBy).HasMaxLength(100); entity.Property(e => e.ExportedBy).HasMaxLength(100);
@@ -45,6 +33,11 @@ namespace ExportDXF.Data
.WithOne(b => b.ExportRecord) .WithOne(b => b.ExportRecord)
.HasForeignKey(b => b.ExportRecordId) .HasForeignKey(b => b.ExportRecordId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Drawing)
.WithMany(d => d.ExportRecords)
.HasForeignKey(e => e.DrawingId)
.OnDelete(DeleteBehavior.SetNull);
}); });
modelBuilder.Entity<BomItem>(entity => modelBuilder.Entity<BomItem>(entity =>
@@ -61,14 +54,41 @@ namespace ExportDXF.Data
.WithOne(ct => ct.BomItem) .WithOne(ct => ct.BomItem)
.HasForeignKey<CutTemplate>(ct => ct.BomItemId) .HasForeignKey<CutTemplate>(ct => ct.BomItemId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.FormProgram)
.WithOne(fp => fp.BomItem)
.HasForeignKey<FormProgram>(fp => fp.BomItemId)
.OnDelete(DeleteBehavior.Cascade);
}); });
modelBuilder.Entity<CutTemplate>(entity => modelBuilder.Entity<CutTemplate>(entity =>
{ {
entity.HasKey(e => e.Id); entity.HasKey(e => e.Id);
entity.Property(e => e.DxfFilePath).HasMaxLength(500);
entity.Property(e => e.CutTemplateName).HasMaxLength(100); entity.Property(e => e.CutTemplateName).HasMaxLength(100);
entity.Property(e => e.ContentHash).HasMaxLength(64); entity.Property(e => e.ContentHash).HasMaxLength(64);
}); });
modelBuilder.Entity<FormProgram>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.ProgramFilePath).HasMaxLength(500);
entity.Property(e => e.ContentHash).HasMaxLength(64);
entity.Property(e => e.ProgramName).HasMaxLength(200);
entity.Property(e => e.MaterialType).HasMaxLength(50);
entity.Property(e => e.UpperToolNames).HasMaxLength(500);
entity.Property(e => e.LowerToolNames).HasMaxLength(500);
entity.Property(e => e.SetupNotes).HasMaxLength(2000);
});
modelBuilder.Entity<Drawing>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.DrawingNumber).HasMaxLength(100);
entity.Property(e => e.Title).HasMaxLength(200);
entity.Property(e => e.PdfContentHash).HasMaxLength(64);
entity.HasIndex(e => e.DrawingNumber).IsUnique();
});
} }
} }
} }

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.11">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using ExportDXF.Data; using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,11 +9,11 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable #nullable disable
namespace ExportDXF.Migrations namespace FabWorks.Core.Migrations
{ {
[DbContext(typeof(ExportDxfDbContext))] [DbContext(typeof(FabWorksDbContext))]
[Migration("20260214195856_ExtractCutTemplate")] [Migration("20260218171742_InitialCreate")]
partial class ExtractCutTemplate partial class InitialCreate
{ {
/// <inheritdoc /> /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder) protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -25,7 +25,7 @@ namespace ExportDXF.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("ExportDXF.Models.BomItem", b => modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -76,7 +76,7 @@ namespace ExportDXF.Migrations
b.ToTable("BomItems"); b.ToTable("BomItems");
}); });
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -99,7 +99,8 @@ namespace ExportDXF.Migrations
.HasColumnType("float"); .HasColumnType("float");
b.Property<string>("DxfFilePath") b.Property<string>("DxfFilePath")
.HasColumnType("nvarchar(max)"); .HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor") b.Property<double?>("KFactor")
.HasColumnType("float"); .HasColumnType("float");
@@ -115,7 +116,7 @@ namespace ExportDXF.Migrations
b.ToTable("CutTemplates"); b.ToTable("CutTemplates");
}); });
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -123,10 +124,18 @@ namespace ExportDXF.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber") b.Property<string>("DrawingNumber")
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("nvarchar(100)"); .HasColumnType("nvarchar(100)");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("ExportedAt") b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@@ -146,14 +155,74 @@ namespace ExportDXF.Migrations
.HasMaxLength(500) .HasMaxLength(500)
.HasColumnType("nvarchar(500)"); .HasColumnType("nvarchar(500)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("ExportRecords"); b.ToTable("ExportRecords");
}); });
modelBuilder.Entity("ExportDXF.Models.BomItem", b => modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{ {
b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord") b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems") .WithMany("BomItems")
.HasForeignKey("ExportRecordId") .HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -162,23 +231,36 @@ namespace ExportDXF.Migrations
b.Navigation("ExportRecord"); b.Navigation("ExportRecord");
}); });
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{ {
b.HasOne("ExportDXF.Models.BomItem", "BomItem") b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate") .WithOne("CutTemplate")
.HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId") .HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("BomItem"); b.Navigation("BomItem");
}); });
modelBuilder.Entity("ExportDXF.Models.BomItem", b => modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{ {
b.Navigation("CutTemplate"); b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
}); });
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{ {
b.Navigation("BomItems"); b.Navigation("BomItems");
}); });

View File

@@ -0,0 +1,151 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FabWorks.Core.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ExportRecords",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DrawingNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
EquipmentNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
DrawingNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
SourceFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
OutputFolder = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
ExportedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
ExportedBy = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
PdfContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ExportRecords", x => x.Id);
});
migrationBuilder.CreateTable(
name: "BomItems",
columns: table => new
{
ID = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ItemNo = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
PartNo = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
SortOrder = table.Column<int>(type: "int", nullable: false),
Qty = table.Column<int>(type: "int", nullable: true),
TotalQty = table.Column<int>(type: "int", nullable: true),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
PartName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
ConfigurationName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Material = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
ExportRecordId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BomItems", x => x.ID);
table.ForeignKey(
name: "FK_BomItems_ExportRecords_ExportRecordId",
column: x => x.ExportRecordId,
principalTable: "ExportRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "CutTemplates",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DxfFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
CutTemplateName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Thickness = table.Column<double>(type: "float", nullable: true),
KFactor = table.Column<double>(type: "float", nullable: true),
DefaultBendRadius = table.Column<double>(type: "float", nullable: true),
BomItemId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CutTemplates", x => x.Id);
table.ForeignKey(
name: "FK_CutTemplates_BomItems_BomItemId",
column: x => x.BomItemId,
principalTable: "BomItems",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "FormPrograms",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
ProgramFilePath = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
ContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
ProgramName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
Thickness = table.Column<double>(type: "float", nullable: true),
MaterialType = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
KFactor = table.Column<double>(type: "float", nullable: true),
BendCount = table.Column<int>(type: "int", nullable: false),
UpperToolNames = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
LowerToolNames = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
SetupNotes = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
BomItemId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_FormPrograms", x => x.Id);
table.ForeignKey(
name: "FK_FormPrograms_BomItems_BomItemId",
column: x => x.BomItemId,
principalTable: "BomItems",
principalColumn: "ID",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_BomItems_ExportRecordId",
table: "BomItems",
column: "ExportRecordId");
migrationBuilder.CreateIndex(
name: "IX_CutTemplates_BomItemId",
table: "CutTemplates",
column: "BomItemId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_FormPrograms_BomItemId",
table: "FormPrograms",
column: "BomItemId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CutTemplates");
migrationBuilder.DropTable(
name: "FormPrograms");
migrationBuilder.DropTable(
name: "BomItems");
migrationBuilder.DropTable(
name: "ExportRecords");
}
}
}

View File

@@ -1,19 +1,22 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using ExportDXF.Data; using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable #nullable disable
namespace ExportDXF.Migrations namespace FabWorks.Core.Migrations
{ {
[DbContext(typeof(ExportDxfDbContext))] [DbContext(typeof(FabWorksDbContext))]
partial class ExportDxfDbContextModelSnapshot : ModelSnapshot [Migration("20260219134027_AddCutTemplateRevision")]
partial class AddCutTemplateRevision
{ {
protected override void BuildModel(ModelBuilder modelBuilder) /// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
@@ -22,7 +25,7 @@ namespace ExportDXF.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("ExportDXF.Models.BomItem", b => modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{ {
b.Property<int>("ID") b.Property<int>("ID")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -73,7 +76,7 @@ namespace ExportDXF.Migrations
b.ToTable("BomItems"); b.ToTable("BomItems");
}); });
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -96,11 +99,15 @@ namespace ExportDXF.Migrations
.HasColumnType("float"); .HasColumnType("float");
b.Property<string>("DxfFilePath") b.Property<string>("DxfFilePath")
.HasColumnType("nvarchar(max)"); .HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor") b.Property<double?>("KFactor")
.HasColumnType("float"); .HasColumnType("float");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<double?>("Thickness") b.Property<double?>("Thickness")
.HasColumnType("float"); .HasColumnType("float");
@@ -112,7 +119,7 @@ namespace ExportDXF.Migrations
b.ToTable("CutTemplates"); b.ToTable("CutTemplates");
}); });
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{ {
b.Property<int>("Id") b.Property<int>("Id")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -120,10 +127,18 @@ namespace ExportDXF.Migrations
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber") b.Property<string>("DrawingNumber")
.HasMaxLength(100) .HasMaxLength(100)
.HasColumnType("nvarchar(100)"); .HasColumnType("nvarchar(100)");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("ExportedAt") b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2"); .HasColumnType("datetime2");
@@ -143,14 +158,74 @@ namespace ExportDXF.Migrations
.HasMaxLength(500) .HasMaxLength(500)
.HasColumnType("nvarchar(500)"); .HasColumnType("nvarchar(500)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id"); b.HasKey("Id");
b.ToTable("ExportRecords"); b.ToTable("ExportRecords");
}); });
modelBuilder.Entity("ExportDXF.Models.BomItem", b => modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{ {
b.HasOne("ExportDXF.Models.ExportRecord", "ExportRecord") b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems") .WithMany("BomItems")
.HasForeignKey("ExportRecordId") .HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@@ -159,23 +234,36 @@ namespace ExportDXF.Migrations
b.Navigation("ExportRecord"); b.Navigation("ExportRecord");
}); });
modelBuilder.Entity("ExportDXF.Models.CutTemplate", b => modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{ {
b.HasOne("ExportDXF.Models.BomItem", "BomItem") b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate") .WithOne("CutTemplate")
.HasForeignKey("ExportDXF.Models.CutTemplate", "BomItemId") .HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.Navigation("BomItem"); b.Navigation("BomItem");
}); });
modelBuilder.Entity("ExportDXF.Models.BomItem", b => modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{ {
b.Navigation("CutTemplate"); b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
}); });
modelBuilder.Entity("ExportDXF.Models.ExportRecord", b => modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{ {
b.Navigation("BomItems"); b.Navigation("BomItems");
}); });

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FabWorks.Core.Migrations
{
/// <inheritdoc />
public partial class AddCutTemplateRevision : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Revision",
table: "CutTemplates",
type: "int",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Revision",
table: "CutTemplates");
}
}
}

View File

@@ -0,0 +1,325 @@
// <auto-generated />
using System;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FabWorks.Core.Migrations
{
[DbContext(typeof(FabWorksDbContext))]
[Migration("20260220125334_AddDrawingEntity")]
partial class AddDrawingEntity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
b.Property<string>("ConfigurationName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Material")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PartName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("PartNo")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("Qty")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("CutTemplateName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<double?>("DefaultBendRadius")
.HasColumnType("float");
b.Property<string>("DxfFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingNumber")
.IsUnique()
.HasFilter("[DrawingNumber] IS NOT NULL");
b.ToTable("Drawings");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("DrawingId")
.HasColumnType("int");
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2");
b.Property<string>("ExportedBy")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("OutputFolder")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingId");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
.WithMany("ExportRecords")
.HasForeignKey("DrawingId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Drawing");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Navigation("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FabWorks.Core.Migrations
{
/// <inheritdoc />
public partial class AddDrawingEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DrawingId",
table: "ExportRecords",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "Drawings",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
DrawingNumber = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
PdfContentHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
Revision = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Drawings", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ExportRecords_DrawingId",
table: "ExportRecords",
column: "DrawingId");
migrationBuilder.CreateIndex(
name: "IX_Drawings_DrawingNumber",
table: "Drawings",
column: "DrawingNumber",
unique: true,
filter: "[DrawingNumber] IS NOT NULL");
migrationBuilder.AddForeignKey(
name: "FK_ExportRecords_Drawings_DrawingId",
table: "ExportRecords",
column: "DrawingId",
principalTable: "Drawings",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ExportRecords_Drawings_DrawingId",
table: "ExportRecords");
migrationBuilder.DropTable(
name: "Drawings");
migrationBuilder.DropIndex(
name: "IX_ExportRecords_DrawingId",
table: "ExportRecords");
migrationBuilder.DropColumn(
name: "DrawingId",
table: "ExportRecords");
}
}
}

View File

@@ -0,0 +1,325 @@
// <auto-generated />
using System;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FabWorks.Core.Migrations
{
[DbContext(typeof(FabWorksDbContext))]
[Migration("20260220130029_SeedDrawingsFromExistingExports")]
partial class SeedDrawingsFromExistingExports
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
b.Property<string>("ConfigurationName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Material")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PartName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("PartNo")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("Qty")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("CutTemplateName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<double?>("DefaultBendRadius")
.HasColumnType("float");
b.Property<string>("DxfFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingNumber")
.IsUnique()
.HasFilter("[DrawingNumber] IS NOT NULL");
b.ToTable("Drawings");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("DrawingId")
.HasColumnType("int");
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2");
b.Property<string>("ExportedBy")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("OutputFolder")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingId");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
.WithMany("ExportRecords")
.HasForeignKey("DrawingId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Drawing");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Navigation("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FabWorks.Core.Migrations
{
/// <inheritdoc />
public partial class SeedDrawingsFromExistingExports : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Create Drawing records from existing ExportRecords (latest hash per DrawingNumber)
migrationBuilder.Sql(@"
INSERT INTO Drawings (DrawingNumber, Title, PdfContentHash, Revision)
SELECT
sub.DrawingNumber,
sub.Title,
sub.PdfContentHash,
1
FROM (
SELECT
e.DrawingNumber,
e.Title,
e.PdfContentHash,
ROW_NUMBER() OVER (PARTITION BY e.DrawingNumber ORDER BY e.Id DESC) AS rn
FROM ExportRecords e
WHERE e.DrawingNumber IS NOT NULL
AND e.PdfContentHash IS NOT NULL
) sub
WHERE sub.rn = 1;
");
// Link ExportRecords to their Drawing
migrationBuilder.Sql(@"
UPDATE er
SET er.DrawingId = d.Id
FROM ExportRecords er
INNER JOIN Drawings d ON d.DrawingNumber = er.DrawingNumber
WHERE er.PdfContentHash IS NOT NULL;
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("UPDATE ExportRecords SET DrawingId = NULL;");
migrationBuilder.Sql("DELETE FROM Drawings;");
}
}
}

View File

@@ -0,0 +1,325 @@
// <auto-generated />
using System;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FabWorks.Core.Migrations
{
[DbContext(typeof(FabWorksDbContext))]
[Migration("20260220171747_MoveRevisionFromDrawingToExportRecord")]
partial class MoveRevisionFromDrawingToExportRecord
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
b.Property<string>("ConfigurationName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Material")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PartName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("PartNo")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("Qty")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("CutTemplateName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<double?>("DefaultBendRadius")
.HasColumnType("float");
b.Property<string>("DxfFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingNumber")
.IsUnique()
.HasFilter("[DrawingNumber] IS NOT NULL");
b.ToTable("Drawings");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("DrawingId")
.HasColumnType("int");
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("DrawingRevision")
.HasColumnType("int");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2");
b.Property<string>("ExportedBy")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("OutputFolder")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingId");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
.WithMany("ExportRecords")
.HasForeignKey("DrawingId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Drawing");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Navigation("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace FabWorks.Core.Migrations
{
/// <inheritdoc />
public partial class MoveRevisionFromDrawingToExportRecord : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Revision",
table: "Drawings");
migrationBuilder.AddColumn<int>(
name: "DrawingRevision",
table: "ExportRecords",
type: "int",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DrawingRevision",
table: "ExportRecords");
migrationBuilder.AddColumn<int>(
name: "Revision",
table: "Drawings",
type: "int",
nullable: false,
defaultValue: 0);
}
}
}

View File

@@ -0,0 +1,322 @@
// <auto-generated />
using System;
using FabWorks.Core.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace FabWorks.Core.Migrations
{
[DbContext(typeof(FabWorksDbContext))]
partial class FabWorksDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.11")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ID"));
b.Property<string>("ConfigurationName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<int>("ExportRecordId")
.HasColumnType("int");
b.Property<string>("ItemNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("Material")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PartName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("PartNo")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("Qty")
.HasColumnType("int");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<int?>("TotalQty")
.HasColumnType("int");
b.HasKey("ID");
b.HasIndex("ExportRecordId");
b.ToTable("BomItems");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("CutTemplateName")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<double?>("DefaultBendRadius")
.HasColumnType("float");
b.Property<string>("DxfFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<int>("Revision")
.HasColumnType("int");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingNumber")
.IsUnique()
.HasFilter("[DrawingNumber] IS NOT NULL");
b.ToTable("Drawings");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int?>("DrawingId")
.HasColumnType("int");
b.Property<string>("DrawingNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("DrawingNumber")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int?>("DrawingRevision")
.HasColumnType("int");
b.Property<string>("EquipmentNo")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<DateTime>("ExportedAt")
.HasColumnType("datetime2");
b.Property<string>("ExportedBy")
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("OutputFolder")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("PdfContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Title")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.HasKey("Id");
b.HasIndex("DrawingId");
b.ToTable("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("BendCount")
.HasColumnType("int");
b.Property<int>("BomItemId")
.HasColumnType("int");
b.Property<string>("ContentHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<double?>("KFactor")
.HasColumnType("float");
b.Property<string>("LowerToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("MaterialType")
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<string>("ProgramFilePath")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("ProgramName")
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("SetupNotes")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<double?>("Thickness")
.HasColumnType("float");
b.Property<string>("UpperToolNames")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("FormPrograms");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.HasOne("FabWorks.Core.Models.ExportRecord", "ExportRecord")
.WithMany("BomItems")
.HasForeignKey("ExportRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExportRecord");
});
modelBuilder.Entity("FabWorks.Core.Models.CutTemplate", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("CutTemplate")
.HasForeignKey("FabWorks.Core.Models.CutTemplate", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.HasOne("FabWorks.Core.Models.Drawing", "Drawing")
.WithMany("ExportRecords")
.HasForeignKey("DrawingId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Drawing");
});
modelBuilder.Entity("FabWorks.Core.Models.FormProgram", b =>
{
b.HasOne("FabWorks.Core.Models.BomItem", "BomItem")
.WithOne("FormProgram")
.HasForeignKey("FabWorks.Core.Models.FormProgram", "BomItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BomItem");
});
modelBuilder.Entity("FabWorks.Core.Models.BomItem", b =>
{
b.Navigation("CutTemplate");
b.Navigation("FormProgram");
});
modelBuilder.Entity("FabWorks.Core.Models.Drawing", b =>
{
b.Navigation("ExportRecords");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,22 @@
namespace FabWorks.Core.Models
{
public class BomItem
{
public int ID { get; set; }
public string 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 int ExportRecordId { get; set; }
public virtual ExportRecord ExportRecord { get; set; }
public virtual CutTemplate CutTemplate { get; set; }
public virtual FormProgram FormProgram { get; set; }
}
}

View File

@@ -0,0 +1,33 @@
using System;
namespace FabWorks.Core.Models
{
public class CutTemplate
{
public int Id { get; set; }
public string DxfFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string CutTemplateName { get; set; } = "";
private double? _thickness;
public double? Thickness
{
get => _thickness;
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public int Revision { get; set; } = 1;
public double? KFactor { get; set; }
private double? _defaultBendRadius;
public double? DefaultBendRadius
{
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public int BomItemId { get; set; }
public virtual BomItem BomItem { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace FabWorks.Core.Models
{
public class Drawing
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string PdfContentHash { get; set; }
public virtual ICollection<ExportRecord> ExportRecords { get; set; } = new List<ExportRecord>();
}
}

View File

@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace FabWorks.Core.Models
{
public class ExportRecord
{
public int Id { get; set; }
public string DrawingNumber { get; set; }
public string Title { get; set; }
public string EquipmentNo { get; set; }
public string DrawingNo { get; set; }
public string SourceFilePath { get; set; }
public string OutputFolder { get; set; }
public DateTime ExportedAt { get; set; }
public string ExportedBy { get; set; }
public string PdfContentHash { get; set; }
public int? DrawingId { get; set; }
public int? DrawingRevision { get; set; }
public virtual Drawing Drawing { get; set; }
public virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
}
}

View File

@@ -0,0 +1,20 @@
namespace FabWorks.Core.Models
{
public class FormProgram
{
public int Id { get; set; }
public string ProgramFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string ProgramName { get; set; } = "";
public double? Thickness { get; set; }
public string MaterialType { get; set; } = "";
public double? KFactor { get; set; }
public int BendCount { get; set; }
public string UpperToolNames { get; set; } = "";
public string LowerToolNames { get; set; } = "";
public string SetupNotes { get; set; } = "";
public int BomItemId { get; set; }
public virtual BomItem BomItem { get; set; }
}
}

View File

@@ -0,0 +1,171 @@
using System;
using System.Xml.Linq;
namespace FabWorks.Core.PressBrake
{
internal static class Extensions
{
private static bool? ToBool(this string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
int intValue;
if (!int.TryParse(s, out intValue))
return null;
return Convert.ToBoolean(intValue);
}
public static bool ToBool(this XAttribute a, bool defaultValue = false)
{
if (a == null)
return defaultValue;
var b = a.Value.ToBool();
return b != null ? b.Value : defaultValue;
}
public static bool? ToBoolOrNull(this XAttribute a)
{
if (a == null)
return null;
return a.Value.ToBool();
}
private static int? ToInt(this string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
int intValue;
if (!int.TryParse(s, out intValue))
return null;
return intValue;
}
public static int ToInt(this XAttribute a, int defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToInt();
return b != null ? b.Value : defaultValue;
}
public static int? ToIntOrNull(this XAttribute a)
{
if (a == null)
return null;
return a.Value.ToInt();
}
public static int ToInt(this XElement a, int defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToInt();
return b != null ? b.Value : defaultValue;
}
public static int? ToIntOrNull(this XElement a)
{
if (a == null)
return null;
return a.Value.ToInt();
}
private static double? ToDouble(this string s)
{
if (string.IsNullOrWhiteSpace(s))
return null;
double d;
if (!double.TryParse(s, out d))
return null;
return d;
}
public static double ToDouble(this XAttribute a, double defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToDouble();
return b != null ? b.Value : defaultValue;
}
public static double? ToDoubleOrNull(this XAttribute a)
{
if (a == null)
return null;
return a.Value.ToDouble();
}
public static double ToDouble(this XElement a, double defaultValue = 0)
{
if (a == null)
return defaultValue;
var b = a.Value.ToDouble();
return b != null ? b.Value : defaultValue;
}
public static double? ToDoubleOrNull(this XElement a)
{
if (a == null)
return null;
return a.Value.ToDouble();
}
public static DateTime? ToDateTime(this XAttribute a)
{
if (a == null || string.IsNullOrWhiteSpace(a.Value))
return null;
DateTime d;
if (!DateTime.TryParse(a.Value, out d))
return null;
return d;
}
public static TimeSpan? ToTimeSpan(this XElement e)
{
if (e == null || string.IsNullOrWhiteSpace(e.Value))
return null;
TimeSpan d;
if (!TimeSpan.TryParse(e.Value, out d))
return null;
return d;
}
public static DateTime RoundDown(this DateTime dt, TimeSpan d)
{
var modTicks = dt.Ticks % d.Ticks;
var delta = -modTicks;
return new DateTime(dt.Ticks + delta, dt.Kind);
}
}
}

View File

@@ -0,0 +1,11 @@
namespace FabWorks.Core.PressBrake
{
public enum MatType
{
MildSteel,
HighStrengthSteel,
Stainless,
SoftAluminum,
HardAluminum
}
}

View File

@@ -0,0 +1,59 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace FabWorks.Core.PressBrake
{
public class Program
{
public Program()
{
UpperToolSets = new List<ToolSetup>();
LowerToolSets = new List<ToolSetup>();
Steps = new List<Step>();
}
public int Version { get; set; }
public string ProgName { get; set; }
public string FilePath { get; set; }
public double MatThick { get; set; }
public MatType MatType { get; set; }
public double KFactor { get; set; }
public string TeachName { get; set; }
public string PartName { get; set; }
public string SetupNotes { get; set; }
public string ProgNotes { get; set; }
public bool RZEnabled { get; set; }
public List<ToolSetup> UpperToolSets { get; set; }
public List<ToolSetup> LowerToolSets { get; set; }
public List<Step> Steps { get; set; }
public static Program Load(string file)
{
var reader = new ProgramReader();
reader.Read(file);
return reader.Program;
}
public static Program Load(Stream stream)
{
var reader = new ProgramReader();
reader.Read(stream);
return reader.Program;
}
}
}

View File

@@ -0,0 +1,149 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Xml.Linq;
namespace FabWorks.Core.PressBrake
{
public class ProgramReader
{
public Program Program { get; set; }
public ProgramReader()
{
Program = new Program();
}
public void Read(string file)
{
var xml = XDocument.Load(file);
Program.FilePath = file;
Read(xml);
}
public void Read(Stream stream)
{
var xml = XDocument.Load(stream);
Read(xml);
}
private void Read(XDocument doc)
{
var data = doc.Root.Element("PressBrakeProgram");
Program.Version = data.Attribute("Version").ToInt();
Program.ProgName = data.Attribute("ProgName")?.Value;
Program.MatThick = data.Attribute("MatThick").ToDouble();
Program.MatType = GetMaterialType(data.Attribute("MatType")?.Value);
Program.KFactor = data.Attribute("KFactor").ToDouble();
Program.TeachName = data.Attribute("TeachName")?.Value;
Program.PartName = data.Attribute("PartName")?.Value;
Program.SetupNotes = data.Attribute("SetupNotes")?.Value;
Program.ProgNotes = data.Attribute("ProgNotes")?.Value;
Program.RZEnabled = Convert.ToBoolean(data.Attribute("RZEnabled").ToInt());
foreach (var item in data.Element("UpperToolSets").Descendants("ToolSetup"))
{
var setup = ReadToolSetup(item);
Program.UpperToolSets.Add(setup);
}
foreach (var item in data.Element("LowerToolSets").Descendants("ToolSetup"))
{
var setup = ReadToolSetup(item);
Program.LowerToolSets.Add(setup);
}
foreach (var item in data.Element("StepData").Descendants("Step"))
{
var step = ReadStep(item);
step.UpperTool = Program.UpperToolSets.FirstOrDefault(t => t.Id == step.UpperID);
step.LowerTool = Program.LowerToolSets.FirstOrDefault(t => t.Id == step.LowerID);
Program.Steps.Add(step);
}
}
private ToolSetup ReadToolSetup(XElement x)
{
var setup = new ToolSetup();
setup.Name = x.Attribute("Name").Value;
setup.Id = x.Attribute("ID").ToInt();
setup.Length = x.Attribute("Length").ToDouble();
setup.StackedHolderType = x.Attribute("StackedHolderType").ToInt();
setup.HolderHeight = x.Attribute("HolderHeight").ToDouble();
foreach (var item in x.Descendants("SegEntry"))
{
var entry = new SegEntry();
entry.SegValue = item.Attribute("SegValue").ToDouble();
setup.Segments.Add(entry);
}
return setup;
}
private Step ReadStep(XElement x)
{
var step = new Step();
step.RevMode = x.Attribute("RevMode").ToInt();
step.RevTons = x.Attribute("RevTons").ToDouble();
step.MaxTons = x.Attribute("MaxTons").ToDouble();
step.RevAbsPos = x.Attribute("RevAbsPos").ToDouble();
step.ActualAng = x.Attribute("ActualAng").ToDouble();
step.AngleAdj = x.Attribute("AngleAdj").ToDouble();
step.BendLen = x.Attribute("BendLen").ToDouble();
step.StrokeLen = x.Attribute("StrokeLen").ToDouble();
step.UpperID = x.Attribute("UpperID").ToInt();
step.LowerID = x.Attribute("LowerID").ToInt();
step.SpdChgDwn = x.Attribute("SpdChgDwn").ToDouble();
step.SpdChgUp = x.Attribute("SpdChgUp").ToDouble();
step.Tilt = x.Attribute("Tilt").ToDouble();
step.FormSpeed = x.Attribute("FormSpeed").ToDouble();
step.XLeft = x.Attribute("XLeft").ToDouble();
step.XRight = x.Attribute("XRight").ToDouble();
step.RLeft = x.Attribute("RLeft").ToDouble();
step.RRight = x.Attribute("RRight").ToDouble();
step.ZLeft = x.Attribute("ZLeft").ToDouble();
step.ZRight = x.Attribute("ZRight").ToDouble();
step.FLeft = x.Attribute("FLeft").ToDouble();
step.FRight = x.Attribute("FRight").ToDouble();
step.SSLeft = x.Attribute("SSLeft").ToDouble();
step.SSRight = x.Attribute("SSRight").ToDouble();
step.ReturnSpd = x.Attribute("ReturnSpd").ToDouble();
step.SideFlgHeight = x.Attribute("SideFlgHeight").ToDouble();
return step;
}
private MatType GetMaterialType(string value)
{
if (value == null)
return MatType.MildSteel;
int i;
if (!int.TryParse(value, out i))
return MatType.MildSteel;
switch (i)
{
case 0:
return MatType.MildSteel;
case 1:
return MatType.HighStrengthSteel;
case 2:
return MatType.Stainless;
case 3:
return MatType.SoftAluminum;
case 4:
return MatType.HardAluminum;
}
return MatType.MildSteel;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace FabWorks.Core.PressBrake
{
public class SegEntry
{
public double SegValue { get; set; }
}
}

View File

@@ -0,0 +1,36 @@
namespace FabWorks.Core.PressBrake
{
public class Step
{
public int RevMode { get; set; }
public double RevTons { get; set; }
public double MaxTons { get; set; }
public double RevAbsPos { get; set; }
public double ActualAng { get; set; }
public double AngleAdj { get; set; }
public double BendLen { get; set; }
public double StrokeLen { get; set; }
public double Tilt { get; set; }
public int UpperID { get; set; }
public int LowerID { get; set; }
public double SpdChgDwn { get; set; }
public double SpdChgUp { get; set; }
public double FormSpeed { get; set; }
public double XLeft { get; set; }
public double XRight { get; set; }
public double RLeft { get; set; }
public double RRight { get; set; }
public double ZLeft { get; set; }
public double ZRight { get; set; }
public double FLeft { get; set; }
public double FRight { get; set; }
public double SSLeft { get; set; }
public double SSRight { get; set; }
public double ReturnSpd { get; set; }
public double SideFlgHeight { get; set; }
public ToolSetup UpperTool { get; set; }
public ToolSetup LowerTool { get; set; }
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace FabWorks.Core.PressBrake
{
public class ToolSetup
{
public ToolSetup()
{
Segments = new List<SegEntry>();
}
public string Name { get; set; }
public int Id { get; set; }
public double Length { get; set; }
public int StackedHolderType { get; set; }
public double HolderHeight { get; set; }
public List<SegEntry> Segments { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FabWorks.Core\FabWorks.Core.csproj" />
<ProjectReference Include="..\FabWorks.Api\FabWorks.Api.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="TestData\**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,53 @@
using FabWorks.Api.Services;
using Xunit;
namespace FabWorks.Tests
{
public class FormProgramServiceTests
{
[Fact]
public void ParseFromFile_SamplePgm_PopulatesMaterialType()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
// ProgName is empty in the sample file, so verify MaterialType instead
Assert.False(string.IsNullOrEmpty(fp.MaterialType));
}
[Fact]
public void ParseFromFile_SamplePgm_PopulatesThickness()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.NotNull(fp.Thickness);
Assert.True(fp.Thickness > 0);
}
[Fact]
public void ParseFromFile_SamplePgm_PopulatesBendCount()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.True(fp.BendCount > 0);
}
[Fact]
public void ParseFromFile_SamplePgm_PopulatesToolNames()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.False(string.IsNullOrEmpty(fp.UpperToolNames));
Assert.False(string.IsNullOrEmpty(fp.LowerToolNames));
}
[Fact]
public void ParseFromFile_SamplePgm_ComputesContentHash()
{
var service = new FormProgramService();
var fp = service.ParseFromFile("TestData/sample.pgm");
Assert.NotNull(fp.ContentHash);
Assert.Equal(64, fp.ContentHash.Length); // SHA256 hex = 64 chars
}
}
}

View File

@@ -0,0 +1,48 @@
using FabWorks.Core.PressBrake;
using Xunit;
namespace FabWorks.Tests.PressBrake
{
public class ProgramReaderTests
{
[Fact]
public void Load_SamplePgm_ParsesProgramAttributes()
{
var pgm = Program.Load("TestData/sample.pgm");
// ProgName may be empty on some exports; verify PartName was parsed instead
Assert.False(string.IsNullOrEmpty(pgm.PartName));
}
[Fact]
public void Load_SamplePgm_ParsesThickness()
{
var pgm = Program.Load("TestData/sample.pgm");
Assert.True(pgm.MatThick > 0);
}
[Fact]
public void Load_SamplePgm_ParsesSteps()
{
var pgm = Program.Load("TestData/sample.pgm");
Assert.NotEmpty(pgm.Steps);
}
[Fact]
public void Load_SamplePgm_ParsesToolSetups()
{
var pgm = Program.Load("TestData/sample.pgm");
Assert.NotEmpty(pgm.UpperToolSets);
Assert.NotEmpty(pgm.LowerToolSets);
}
[Fact]
public void Load_SamplePgm_ResolvesStepToolReferences()
{
var pgm = Program.Load("TestData/sample.pgm");
var step = pgm.Steps[0];
Assert.NotNull(step.UpperTool);
Assert.NotNull(step.LowerTool);
}
}
}

View File

@@ -0,0 +1,593 @@
<?xml version="1.0" ?>
<Document>
<PressBrakeProgram Version="13" ProgName="" TeachName="" PartName="C:\Users\aj.REMCO\Desktop\4980 A05-1 PT02.part" SetupNotes="" ProgNotes="" MatThick="0.06" MatType="2" KFactor="0.42" RZEnabled="1" FEnabled="0" KFactorAuto="0" PartsBetween="0" DryRun="0" LeftFingerType="2" RightFingerType="2" HasPart="1" CBAngMode="1" CBVee="0.75" CBMute="20.752" CBClamp="20.492" CBDieAng="85" CBTopOfDie="20.442" ToolSelLock="0" NumSteps="8" GageType="6">
<UpperToolSets>
<ToolList Count="1">
<ToolSetup Name="50210 with double riser" ID="1" Length="63" NumSegs="4">
<SegStackup>
<SegEntry SegValue="36"/>
<SegEntry SegValue="18"/>
<SegEntry SegValue="8"/>
<SegEntry SegValue="1"/>
</SegStackup>
</ToolSetup>
</ToolList>
</UpperToolSets>
<LowerToolSets>
<ToolList Count="1">
<ToolSetup Name="0.750 x 85 x 144" ID="1" Length="144"/>
</ToolList>
</LowerToolSets>
<StepData>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.0859" StrokeLen="1.7399" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-15.2429" ZRight="15.2429" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" CBTopStop="22.1819" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.5429" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.7179" StrokeLen="8.8696" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="1" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-16.7954" ZRight="0.3125" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.859" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.0859" StrokeLen="1.7399" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-15.2429" ZRight="15.2429" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.1819" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.5429" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="35.7179" StrokeLen="8.8696" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="1" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-0.3125" ZRight="16.7954" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-17.859" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4609" StrokeLen="1.74" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-29.1179" ZRight="29.1179" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.182" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.7304" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4679" StrokeLen="8.8759" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-30.6705" ZRight="30.6704" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.734" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4609" StrokeLen="1.74" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="1.6729" XRight="1.6729" CBTopOfLDie="6.752" RLeft="0.05" RRight="0.05" ZLeft="-29.1179" ZRight="29.1179" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="22.182" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.7304" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
<Step RevMode="0" RevTons="17.5" MaxTons="9999" RevAbsPos="20.2004" ActualAng="90" BendLen="63.4679" StrokeLen="8.8759" UpperID="1" LowerID="1" SpdChgDwn="0.375" SpdChgUp="0.25" FormSpeed="20" CadStep="0" GageMode="0" GageAllowAuto="0" DimensionType="4" GagePause="0.1" XLeft="8.4129" XRight="8.4129" CBTopOfLDie="6.752" RLeft="0.06" RRight="0.06" ZLeft="-30.6704" ZRight="30.6704" FLeft="44" FRight="44" SSLeft="0" SSRight="0" ReturnSpd="20" GuardMute="0" GuardMode="0" SpecialToolMode="0" FingGagePt="1" AdaptRevPos="0" MuteOffset="0.25" SideFlgHeight="8.4199" CBTopStop="24" CBAbsSpdChg="20.877" CBMute="20.752" EndStopDim="-31.734" AutoAdjustment="5" SnapShotTaken="0" XValue="0" YValue="0" ZValue="0" Zoom="0" RotationHeight="0" RotationWidth="0" RotationDepth="0" ScalarValue="0" VectorXValue="0" VectorYValue="0" VectorZValue="0">
<UIDSelectList Size="1">
<UID IntValue="1"/>
</UIDSelectList>
<LIDSelectList Size="1">
<LID IntValue="1"/>
</LIDSelectList>
<ProgOutputs>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
<ProgOutData/>
</ProgOutputs>
</Step>
</StepData>
</PressBrakeProgram>
<PressBrakePart Version="2" FlangeCount="9" MatType="2" KFactor="0.42" KFactorAuto="0" FingerSelPref="3">
<Flanges>
<Flange Version="5" FlangeID="1" FlangeDim="63.3409" FlangeWidth="1.554956" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0" IntBend="0">
<Quaternion Version="1" SVal="1">
<Segment3D XVal="0" YVal="0" ZVal="0"/>
</Quaternion>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="0" YVal="0" ZVal="0"/>
</Quaternion>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="0" YVal="0" ZVal="0"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.460859" BendRadius="0.125" BendSeq="5" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="1" EndFlangeID="2" Rotate180="1" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="5" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="0.117968" YVal="31.73043" ZVal="0"/>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Segment3D XVal="0.000001" YVal="31.73043" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="6" OrgSegListCount="6">
<SegmentList>
<Segment3D XVal="0.117972" YVal="0.059985" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.122918" YVal="0.0625" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.672918" YVal="1.6125" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.672918" YVal="61.848358" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.122918" YVal="63.398358" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.117961" YVal="63.400878" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000003" YVal="0" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="0.0625" ZVal="0.03"/>
<Segment3D XVal="1.672918" YVal="1.6125" ZVal="0.03"/>
<Segment3D XVal="1.672918" YVal="61.848358" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="63.398358" ZVal="0.03"/>
<Segment3D XVal="0" YVal="63.460859" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="2" FlangeDim="63.5859" FlangeWidth="8.1099" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0.707107" YVal="0" ZVal="0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0.707107" YVal="0" ZVal="0.707107"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.467927" BendRadius="0.125" BendSeq="6" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="2" EndFlangeID="3" Rotate180="1" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="6" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109896" YVal="0" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.34583" YVal="0" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="8" OrgSegListCount="8">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.790412" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109896" YVal="-31.790554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109899" YVal="31.790554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104945" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004945" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.790414" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000003" YVal="-31.73043" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="-31.792928" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="-31.792928" ZVal="0.03"/>
<Segment3D XVal="8.345832" YVal="-31.733962" ZVal="0.03"/>
<Segment3D XVal="8.345827" YVal="31.733966" ZVal="0.03"/>
<Segment3D XVal="8.222912" YVal="31.79293" ZVal="0.03"/>
<Segment3D XVal="0.122912" YVal="31.79293" ZVal="0.03"/>
<Segment3D XVal="-0.000003" YVal="31.73043" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="3" FlangeDim="63.3599" FlangeWidth="35.609897" Editable="0" BendCount="3" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0">
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="0">
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.467928" BendRadius="0.125" BendSeq="8" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="4" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="8" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="35.609893" YVal="-0.000003" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="35.845827" YVal="-0.000003" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="35.717928" BendRadius="0.125" BendSeq="2" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="5" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="2" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="17.804945" YVal="-31.679949" ZVal="0"/>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
</Quaternion>
<Segment3D XVal="17.922911" YVal="-31.797916" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="35.717928" BendRadius="0.125" BendSeq="4" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="3" EndFlangeID="6" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="4" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="17.804948" YVal="31.679946" ZVal="0"/>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Segment3D XVal="17.922915" YVal="31.797913" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.677374" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004945" YVal="-31.675001" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00257" YVal="-31.679952" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.607317" YVal="-31.679947" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.604945" YVal="-31.675002" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.609896" YVal="-31.677377" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.609893" YVal="31.67737" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.604948" YVal="31.674998" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="35.607323" YVal="31.679949" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.002576" YVal="31.679944" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="31.674999" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.677374" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0" YVal="-31.733964" ZVal="0.03"/>
<Segment3D XVal="0.122912" YVal="-31.675001" ZVal="0.03"/>
<Segment3D XVal="0.063947" YVal="-31.797916" ZVal="0.03"/>
<Segment3D XVal="35.781875" YVal="-31.797916" ZVal="0.03"/>
<Segment3D XVal="35.722912" YVal="-31.675002" ZVal="0.03"/>
<Segment3D XVal="35.845827" YVal="-31.733967" ZVal="0.03"/>
<Segment3D XVal="35.845827" YVal="31.733961" ZVal="0.03"/>
<Segment3D XVal="35.722915" YVal="31.674998" ZVal="0.03"/>
<Segment3D XVal="35.781879" YVal="31.797912" ZVal="0.03"/>
<Segment3D XVal="0.063951" YVal="31.797913" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="31.674999" ZVal="0.03"/>
<Segment3D XVal="0" YVal="31.733964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="4" FlangeDim="63.5859" FlangeWidth="8.109903" Editable="0" BendCount="1" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0">
<Segment3D XVal="0.707107" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="0">
<Segment3D XVal="1" YVal="0" ZVal="-0"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="63.460859" BendRadius="0.125" BendSeq="7" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="4" EndFlangeID="7" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="7" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109896" YVal="0.000001" ZVal="0"/>
<Quaternion Version="1" SVal="-1">
<Segment3D XVal="0" YVal="0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.34583" YVal="0.000001" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="8" OrgSegListCount="8">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.790554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="-31.792928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="-31.792929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109902" YVal="-31.79041" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109893" YVal="31.790416" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104951" YVal="31.792929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004951" YVal="31.79293" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.790554" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0" YVal="-31.733964" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="-31.792928" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="-31.792929" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="-31.730429" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="31.730431" ZVal="0.03"/>
<Segment3D XVal="8.222918" YVal="31.792929" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="31.79293" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="31.733964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="5" FlangeDim="35.8359" FlangeWidth="8.109898" Editable="0" BendCount="2" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0.5">
<Segment3D XVal="-0.5" YVal="-0.5" ZVal="0.5"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.5">
<Segment3D XVal="-0.5" YVal="-0.5" ZVal="0.5"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="19.16793" BendRadius="0.125" BendSeq="1" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="5" EndFlangeID="8" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="1" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109897" YVal="8.271464" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345831" YVal="8.271464" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="15.917929" BendRadius="0.125" BendSeq="1" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="5" EndFlangeID="8" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="1" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109895" YVal="-9.896465" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345829" YVal="-9.896465" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-17.915554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00495" YVal="-17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.10495" YVal="-17.917928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109895" YVal="-17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109896" YVal="-1.937499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="-1.937499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="-1.312499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="-1.312499" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="17.91793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="17.915555" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000002" YVal="-17.858964" ZVal="0.03"/>
<Segment3D XVal="0.122916" YVal="-17.917929" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-17.917928" ZVal="0.03"/>
<Segment3D XVal="8.345831" YVal="-17.855428" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="-1.937499" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-1.937499" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-1.312499" ZVal="0.03"/>
<Segment3D XVal="8.345831" YVal="-1.312499" ZVal="0.03"/>
<Segment3D XVal="8.345828" YVal="17.85543" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="17.91793" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="17.917929" ZVal="0.03"/>
<Segment3D XVal="-0.000002" YVal="17.858964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="6" FlangeDim="35.8359" FlangeWidth="8.109899" Editable="0" BendCount="2" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0.5">
<Segment3D XVal="-0.5" YVal="0.5" ZVal="0.5"/>
</Quaternion>
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="0.5">
<Segment3D XVal="-0.5" YVal="0.5" ZVal="0.5"/>
</Quaternion>
<BendList>
<Bend Version="9" BendLength="19.167929" BendRadius="0.125" BendSeq="3" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="6" EndFlangeID="9" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="3" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109896" YVal="-8.271465" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345829" YVal="-8.271465" ZVal="0"/>
</Bend>
<Bend Version="9" BendLength="15.917929" BendRadius="0.125" BendSeq="3" BendAllow="0.2359" StartDimOffset="0" EndDimOffset="0" StartFlangeID="6" EndFlangeID="9" Rotate180="0" BendCenter="0" UpperToolID="1" LowerToolID="1" DesiredBendSeq="3" FingerSelect="1" FingerGagePoint="1" IntBend="0" FingerGagePointRight="1">
<Segment3D XVal="8.109897" YVal="9.896464" ZVal="0"/>
<Quaternion Version="1" SVal="1">
<Segment3D XVal="-0" YVal="-0" ZVal="-0"/>
</Quaternion>
<Segment3D XVal="8.345831" YVal="9.896464" ZVal="0"/>
</Bend>
</BendList>
<TopList>
<Feature3D Version="6" SegCount="12" OrgSegListCount="12">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-17.915554" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00495" YVal="-17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.10495" YVal="-17.917928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109895" YVal="-17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109897" YVal="1.312501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="1.312501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104949" YVal="1.937501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="1.937501" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.109898" YVal="17.915413" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="8.104948" YVal="17.91793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="17.917929" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="17.915555" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000002" YVal="-17.858964" ZVal="0.03"/>
<Segment3D XVal="0.122916" YVal="-17.917929" ZVal="0.03"/>
<Segment3D XVal="8.222916" YVal="-17.917928" ZVal="0.03"/>
<Segment3D XVal="8.345831" YVal="-17.855428" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="1.312501" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="1.312501" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="1.937501" ZVal="0.03"/>
<Segment3D XVal="8.34583" YVal="1.937501" ZVal="0.03"/>
<Segment3D XVal="8.345828" YVal="17.85543" ZVal="0.03"/>
<Segment3D XVal="8.222915" YVal="17.91793" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="17.917929" ZVal="0.03"/>
<Segment3D XVal="-0.000002" YVal="17.858964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="7" FlangeDim="63.3409" FlangeWidth="1.554952" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="0" YVal="0" ZVal="1"/>
</Quaternion>
<Quaternion Version="1" SVal="0">
<Segment3D XVal="-0" YVal="0" ZVal="-1"/>
</Quaternion>
<Quaternion Version="1" SVal="-0">
<Segment3D XVal="-1" YVal="-0" ZVal="0"/>
</Quaternion>
<TopList>
<Feature3D Version="6" SegCount="6" OrgSegListCount="6">
<SegmentList>
<Segment3D XVal="-0.000001" YVal="-31.670448" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004951" YVal="-31.66793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554951" YVal="-30.11793" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554948" YVal="30.117928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="31.667928" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="31.670445" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0.000003" YVal="-31.73043" ZVal="0.03"/>
<Segment3D XVal="0.122918" YVal="-31.66793" ZVal="0.03"/>
<Segment3D XVal="1.672918" YVal="-30.11793" ZVal="0.03"/>
<Segment3D XVal="1.672915" YVal="30.117928" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="31.667928" ZVal="0.03"/>
<Segment3D XVal="-0.000003" YVal="31.73043" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="8" FlangeDim="35.5909" FlangeWidth="1.554951" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="-0.707107">
<Segment3D XVal="0" YVal="0" ZVal="0.707107"/>
</Quaternion>
<TopList>
<Feature3D Version="6" SegCount="10" OrgSegListCount="10">
<SegmentList>
<Segment3D XVal="-0" YVal="-9.583965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="-9.583965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="-10.208965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0" YVal="-10.208965" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="-26.06691" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004947" YVal="-26.064394" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554947" YVal="-24.514394" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554949" YVal="7.971464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.00495" YVal="9.521464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0" YVal="9.523981" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="0" YVal="-9.583965" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="-9.583965" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="-10.208965" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="-10.208965" ZVal="0.03"/>
<Segment3D XVal="-0.000001" YVal="-26.126894" ZVal="0.03"/>
<Segment3D XVal="0.122913" YVal="-26.064394" ZVal="0.03"/>
<Segment3D XVal="1.672914" YVal="-24.514394" ZVal="0.03"/>
<Segment3D XVal="1.672916" YVal="7.971464" ZVal="0.03"/>
<Segment3D XVal="0.122916" YVal="9.521464" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="9.583965" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
<Flange Version="5" FlangeID="9" FlangeDim="35.5909" FlangeWidth="1.554949" Editable="0" BendCount="0" TopListCount="1" FlangeWidthDisp="0" FlangeSource="1" OrgSegListCount="0" AdjustValue="0.117967" IntBend="0">
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="-0" ZVal="0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="0" ZVal="-0.707107"/>
</Quaternion>
<Quaternion Version="1" SVal="0.707107">
<Segment3D XVal="-0" YVal="-0" ZVal="0.707107"/>
</Quaternion>
<TopList>
<Feature3D Version="6" SegCount="10" OrgSegListCount="10">
<SegmentList>
<Segment3D XVal="-0" YVal="-9.523981" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="-9.521464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554948" YVal="-7.971464" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="1.554948" YVal="24.514393" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.004948" YVal="26.064393" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0.000001" YVal="26.06691" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0" YVal="10.208964" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="10.208964" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="0.554948" YVal="9.583964" ZVal="0.03" ZCent="0.03"/>
<Segment3D XVal="-0" YVal="9.583964" ZVal="0.03" ZCent="0.03"/>
</SegmentList>
<OrgSegList>
<Segment3D XVal="-0" YVal="-9.583964" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="-9.521464" ZVal="0.03"/>
<Segment3D XVal="1.672915" YVal="-7.971464" ZVal="0.03"/>
<Segment3D XVal="1.672915" YVal="24.514393" ZVal="0.03"/>
<Segment3D XVal="0.122915" YVal="26.064393" ZVal="0.03"/>
<Segment3D XVal="-0.000001" YVal="26.126894" ZVal="0.03"/>
<Segment3D XVal="0" YVal="10.208964" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="10.208964" ZVal="0.03"/>
<Segment3D XVal="0.672915" YVal="9.583964" ZVal="0.03"/>
<Segment3D XVal="-0" YVal="9.583964" ZVal="0.03"/>
</OrgSegList>
</Feature3D>
</TopList>
</Flange>
</Flanges>
</PressBrakePart>
</Document>

View File

@@ -0,0 +1,32 @@
# Auto-fill Equipment/Drawing from Export History
## Problem
When a SolidWorks file is opened that doesn't match the `DrawingInfo` regex (e.g., `Conveyor Frame.sldasm` instead of `5028 A02 Conveyor.slddrw`), the equipment and drawing number dropdowns are left empty even though the file may have been exported before with known values.
## Decision
- **Lookup key:** SolidWorks source file path (`ActiveDocument.FilePath`)
- **Storage:** Query the existing `ExportRecords` table (no new table or migration)
- **Priority:** Database lookup first; fall back to title regex parse if no history found
## Design
### Modify `UpdateActiveDocumentDisplay()` in `MainForm.cs`
When the active document changes:
1. Query the DB for the most recent `ExportRecord` where `SourceFilePath` matches `activeDoc.FilePath` (case-insensitive)
2. If found, parse the stored `DrawingNumber` via `DrawingInfo.Parse()` and auto-fill equipment/drawing dropdowns
3. If not found, fall back to current behavior: `DrawingInfo.Parse(activeDoc.Title)`
### What doesn't change
- No schema changes, no new migration
- Equipment/drawing dropdowns still populated with historical values in `InitializeDrawingDropdowns()`
- Export flow untouched
### Error handling
- DB query wrapped in try/catch so failures don't break the UI
- Case-insensitive path comparison (Windows paths are case-insensitive)

File diff suppressed because it is too large Load Diff