Compare commits

...

82 Commits

Author SHA1 Message Date
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 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
aj 036ab2a55a docs: add context to FixDegreeSymbol workaround
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:42:54 -05:00
aj f9e7ace35d fix: repair double-encoded degree symbol in DXF output
ACadSharp misreads UTF-8 degree symbol (C2 B0) as two ANSI_1252
characters (°) then writes that back out. Post-process the saved
DXF to replace ° with ° so bend notes display correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:31:57 -05:00
aj 622cbf1170 fix: update EtchBendLines submodule with degree symbol fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:28:45 -05:00
aj 4a3f33db33 fix: update EtchBendLines submodule with bend line ByLayer color
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:51:59 -05:00
aj 77d0157370 fix: update EtchBendLines submodule with bend detection fixes
Fixes missing etch lines and incorrect bend layer assignment after
the ACadSharp migration.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 22:32:12 -05:00
aj 4eb13a1aca Added Readme 2026-02-13 22:18:36 -05:00
aj 49051b5e64 refactor: replace CutFab API with local file export and database
Remove CutFabApiClient and DrawingSelectionForm - exports no longer
depend on an external API server. DXF/PDF files are saved directly
to a configurable output folder, and export records are persisted
to a local SQL Server database via EF Core.

Replace Color-based progress logging with LogLevel enum across all
services. Redesign MainForm with equipment/drawing filter dropdowns
populated from export history, log row coloring by severity, and
simplified startup flow in Program.cs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:27:04 -05:00
aj 384fceb047 feat: add local database and file export infrastructure
Add EF Core DbContext with ExportRecord and BomItem entities for
tracking export history locally. Add FileExportService for saving
DXF and PDF files to a configurable output folder.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:26:50 -05:00
aj c4926c6e9f refactor: migrate to .NET 8 SDK-style project format
Convert from .NET Framework 4.8 legacy csproj to .NET 8 SDK-style.
Add ComHelper class for COM Running Object Table access, replacing
Marshal.GetActiveObject which is not available in .NET Core.
Add EF Core and System.Configuration.ConfigurationManager packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 11:26:40 -05:00
aj 6b1a5f0ab6 docs: remove AGENTS.md documentation file
Remove obsolete or unnecessary documentation file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:59:16 -05:00
aj 13009aa15e feat: add async SolidWorks connection at startup
Update service container to asynchronously connect to SolidWorks before showing DrawingSelectionForm:
- Convert ResolveDrawingSelection to async method
- Attempt SolidWorks connection and handle failures gracefully
- Show warning dialog if connection fails but allow user to continue
- Pass SolidWorksService instance to DrawingSelectionForm

This enables the active drawing display feature while maintaining robustness when SolidWorks is unavailable.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:59:06 -05:00
aj 136a571aea feat(ui): display active SolidWorks drawing in DrawingSelectionForm
Add real-time display of the currently active SolidWorks document:
- Show active drawing name with visual status indicators
- Display different states: drawing (green), non-drawing (orange), none (gray), error (red)
- Subscribe to ActiveDocumentChanged events for live updates
- Inject ISolidWorksService dependency for document access

This helps users verify they're working with the correct drawing before export.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:58:48 -05:00
aj 8b1c2b5b1b refactor(ui): improve DrawingSelectionForm layout and responsiveness
- Make form wider (584x315) for better content display
- Add anchor properties to controls for responsive resizing
- Adjust control positions and spacing for improved layout
- Update MainForm tab sizes to match new dimensions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:58:33 -05:00
aj f68bddac93 chore: remove test SolidWorks files from repository
Remove TestDocs/ directory and add to .gitignore. Test files can be maintained locally without bloating the repository history.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 09:56:45 -05:00
aj de4847b834 refactor(ui): update MainForm for drawing-based workflow
Refactored MainForm to work with pre-selected drawing ID and number. Added PDF viewer control, enhanced BOM item management with sheet metal properties, and improved UI layout for drawing-specific operations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 06:47:04 -05:00
aj cbfb9190c5 refactor: update application startup flow
Modified Program.cs to display DrawingSelectionForm at startup before launching MainForm. The selected drawing ID and number are now passed to MainForm constructor.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 06:46:43 -05:00
aj 9b1fbd9fad feat(ui): add drawing selection form
Added DrawingSelectionForm to allow users to select equipment and drawing at application startup, replacing the previous workflow where drawing selection happened within the main form.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 06:46:32 -05:00
aj 51bf3b00dd feat(api): add BOM items and cut templates endpoints
Added GetBomItemsForDrawingAsync and GetCutTemplatesAsync methods to fetch BOM items with sheet metal properties and cut template data from the API.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 06:46:22 -05:00
aj a32bbfa5d9 feat: add BomItem and LogEvent models
Added BomItem model with sheet metal properties (thickness, k-factor, bend radius) and LogEvent model for structured logging with action tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 06:46:11 -05:00
aj 84f0196c97 refactor: remove Excel export functionality
Removed BomExcelExporter service, BomExcelSettings class, BomTemplate.xlsx template, and EPPlus package dependency. This functionality is being replaced with direct API integration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 06:45:58 -05:00
aj 5cf7e1f1e5 chore: add Claude Code configuration and documentation
- Add /organize-commits slash command for logical commit organization
- Add AGENTS.md with repository guidelines and workflows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 17:30:06 -04:00
aj 35ac0fb3f8 feat(api): add sheet metal properties to DXF upload
- Add defaultBendRadius and material parameters to UploadDxfZipAsync
- Pass bend radius and material from BOM items to API
- Enables more complete sheet metal specification in CutFab

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 17:29:56 -04:00
aj cc34fb43b6 refactor(ui): rename controls and improve drawing selection
- Rename form controls from generic names to descriptive ones:
  - button1 → runButton
  - richTextBox1 → logTextBox
  - comboBox1 → viewFlipDeciderBox
  - comboBox2 → drawingNoBox
- Preserve drawing selection when switching equipment
- Auto-populate drawing number from active document

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 17:29:47 -04:00
aj d29d9a0e06 Feature: send sheet metal properties and upload all BOM items
Enhanced DXF export to send thickness and kfactor properties from SolidWorks
to the CutFab API, and ensures all BOM items are uploaded regardless of whether
they have DXF files.

Changes:
- Modified UploadDxfZipAsync to accept and send thickness/kfactor parameters
- Updated DxfExportService to extract thickness/kfactor from Item and pass to API
- Refactored BOM item creation to happen for all items, not just those with DXF files
- This ensures purchased parts, hardware, and other non-sheet-metal items are uploaded

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 00:17:37 -04:00
aj c7f2a51823 Submodule EtchBendLines updated
Submodule EtchBendLines:
    > 89d987f - Refactor Main method to separate responsibilities and improve readability
    > 78ae737 - Refactor Etcher.AddEtchLines into discrete steps and helpers
    > 2391eb7 - Format Bend.ToString() with 2dp and “?” placeholders for nulls
    > dd7443d - Moved GetEtchLines to Etcher class
    > e5daf74 - Extract IsBendLine helper to clean up BendLineExtractor
    > 7740120 - bendNoteRegex tweaks
    > 214cc94 - Culture-safe parsing
2025-10-28 17:35:14 -04:00
aj 5b996be91e refactor(model): rename JobNo to EquipmentNo in DrawingInfo
- Update regex group and ToString to use EquipmentNo

- Prepare for equipment-centric drawing identification
2025-10-28 17:24:27 -04:00
aj 6bddbff08e feat(naming): update DXF filename format to include drawing number and PT##
- Use prefix as drawing number and format as {DrawingNo} PT{ItemNo}

- Default to PT{ItemNo} when no prefix provided
2025-10-28 17:24:21 -04:00
aj 1ec72bc98f feat(export): integrate CutFab API in export flow
- Export to temp directory and auto-upload PDF/DXF

- Resolve or create drawing via API using selected Equipment ID

- Upload DXFs per-part and create BOM items

- Attempt auto-linking of templates after export

- Add EquipmentId to ExportContext
2025-10-28 17:24:16 -04:00
aj b122b88435 feat(ui): add equipment and drawing selectors powered by API\n\n- Inject ICutFabApiClient into MainForm\n- Populate equipment and drawings on load\n- Hook selection changes and pass into export context\n- Resize layout and replace prefix textbox with selectors 2025-10-28 17:24:00 -04:00
aj b677ac8ec9 feat(api): add CutFab API client and configuration\n\n- Add ICutFabApiClient + CutFabApiClient HTTP client\n- Wire base URL via appSettings (CutFab.ApiBaseUrl)\n- Register client in Program and inject into services\n- Add required System.Net.Http and compression references 2025-10-28 17:23:56 -04:00
aj c9a8442a29 Refactored ExportContext 2025-10-01 09:44:07 -04:00
aj a2b89318e1 Changed mm to inches in sheet metal properties 2025-10-01 09:42:22 -04:00
aj f1fc105a1b Set minimum width for BOM description column 2025-10-01 09:42:01 -04:00
aj 58269f9761 Changed BomExcelSettings defaults 2025-10-01 09:40:54 -04:00
aj 4053038632 Fixed unit scales 2025-10-01 09:40:16 -04:00
aj 2d5ffdf5c0 Refactored MainForm 2025-09-29 13:29:50 -04:00
aj 6b37f0f6f7 Refactored BomToExcel 2024-12-23 14:10:12 -05:00
111 changed files with 12717 additions and 1180 deletions
+23
View File
@@ -0,0 +1,23 @@
# Organize Changes into Logical Commits
Analyze all current git changes and organize them into logical, atomic commits. Follow these steps:
1. **Analyze Changes**: Run git status and git diff to see all modified and untracked files
2. **Review Content**: Examine the actual changes in each file to understand what was modified
3. **Group Logically**: Group changes by:
- Feature or bug fix
- Service or component
- Related functionality
- UI changes vs business logic vs API changes
4. **Create Commits**: For each logical group:
- Stage only the relevant files
- Create a descriptive commit message following conventional commit format
- Use prefixes like feat:, fix:, refactor:, chore:, docs:, etc.
5. **Verify**: After all commits, show git log to confirm all changes were committed
Important guidelines:
- Keep commits atomic (one logical change per commit)
- Write clear, descriptive commit messages
- Don't mix unrelated changes in the same commit
- Follow the existing commit message style in the repository
- Include the Claude Code attribution at the end of each commit message
+6
View File
@@ -242,3 +242,9 @@ ModelManifest.xml
.fake/ .fake/
.pfx .pfx
# Test documents
TestDocs/
# Claude Code local settings
.claude/
+1 -1
View File
@@ -1,3 +1,3 @@
[submodule "EtchBendLines"] [submodule "EtchBendLines"]
path = EtchBendLines path = EtchBendLines
url = https://git.nforge.net/aj/etchbendlines.git url = https://git.thecozycat.net/aj/etchbendlines.git
+63 -7
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
+157
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>();
}
}
}
+86
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; }
}
}
+22
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);
}
}
-69
View File
@@ -1,69 +0,0 @@
using OfficeOpenXml;
using System;
using System.Collections.Generic;
using System.IO;
namespace ExportDXF
{
public class BomToExcel
{
public string TemplatePath
{
get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Templates", "BomTemplate.xlsx"); }
}
public void CreateBOMExcelFile(string filepath, IList<Item> items)
{
File.Copy(TemplatePath, filepath, true);
var newFile = new FileInfo(filepath);
using (var pkg = new ExcelPackage(newFile))
{
var workbook = pkg.Workbook;
var partsSheet = workbook.Worksheets["Parts"];
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
var row = i + 2;
var col = 1;
partsSheet.Cells[row, col++].Value = item.ItemNo;
partsSheet.Cells[row, col++].Value = item.FileName;
partsSheet.Cells[row, col++].Value = item.Quantity;
partsSheet.Cells[row, col++].Value = item.Description;
partsSheet.Cells[row, col++].Value = item.PartName;
partsSheet.Cells[row, col++].Value = item.Configuration;
if (item.Thickness > 0)
partsSheet.Cells[row, col].Value = item.Thickness;
col++;
partsSheet.Cells[row, col++].Value = item.Material;
if (item.KFactor > 0)
partsSheet.Cells[row, col].Value = item.KFactor;
col++;
if (item.BendRadius > 0)
partsSheet.Cells[row, col].Value = item.BendRadius;
}
for (int i = 1; i <= 8; i++)
{
var column = partsSheet.Column(i);
if (column.Style.WrapText)
continue;
column.AutoFit();
column.Width += 1;
}
workbook.Calculate();
pkg.Save();
}
}
}
}
+52 -5
View File
@@ -1,20 +1,53 @@
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(@"(?<jobNo>[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 JobNo { get; set; } public string EquipmentNo { get; set; }
public string DrawingNo { get; set; } public string DrawingNo { get; set; }
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()
{ {
return $"{JobNo} {DrawingNo}"; if (string.IsNullOrEmpty(DrawingNo))
return EquipmentNo ?? string.Empty;
return $"{EquipmentNo} {DrawingNo}";
} }
public override bool Equals(object obj) public override bool Equals(object obj)
@@ -35,11 +68,25 @@ 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();
dwg.JobNo = match.Groups["jobNo"].Value; dwg.EquipmentNo = match.Groups["equipmentNo"].Value;
dwg.DrawingNo = match.Groups["dwgNo"].Value; dwg.DrawingNo = match.Groups["dwgNo"].Value;
dwg.Source = input; dwg.Source = input;
+31 -183
View File
@@ -1,196 +1,44 @@
<?xml version="1.0" encoding="utf-8"?> <Project Sdk="Microsoft.NET.Sdk">
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup> <PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <TargetFramework>net8.0-windows</TargetFramework>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{05F21D73-FD31-4E77-8D9B-41C86D4D8305}</ProjectGuid>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
<RootNamespace>ExportDXF</RootNamespace> <RootNamespace>ExportDXF</RootNamespace>
<AssemblyName>ExportDXF</AssemblyName> <AssemblyName>ExportDXF</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion> <ApplicationIcon />
<FileAlignment>512</FileAlignment> <StartupObject />
<IsWebBootstrapper>false</IsWebBootstrapper> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<TargetFrameworkProfile />
<PublishUrl>\\REMCOSRV0\Data\Software\ExportDXF\</PublishUrl>
<Install>true</Install>
<InstallFrom>Unc</InstallFrom>
<UpdateEnabled>true</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<PublisherName>Rogers Engineering</PublisherName>
<CreateWebPageOnPublish>true</CreateWebPageOnPublish>
<WebPage>publish.htm</WebPage>
<ApplicationRevision>6</ApplicationRevision>
<ApplicationVersion>1.6.0.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<PublishWizardCompleted>true</PublishWizardCompleted>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup>
<ManifestCertificateThumbprint>34BB4CCEF0A2D6409091A3AC44083A6F09D1DF82</ManifestCertificateThumbprint>
</PropertyGroup>
<PropertyGroup>
<ManifestKeyFile>ExportDXF_TemporaryKey.pfx</ManifestKeyFile>
</PropertyGroup>
<PropertyGroup>
<GenerateManifests>true</GenerateManifests>
</PropertyGroup>
<PropertyGroup>
<SignManifests>false</SignManifests>
</PropertyGroup>
<PropertyGroup>
<NoWin32Manifest>true</NoWin32Manifest>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="PresentationCore" /> <PackageReference Include="PDFtoImage" Version="4.1.1" />
<Reference Include="SolidWorks.Interop.sldworks, Version=24.1.0.45, Culture=neutral, PublicKeyToken=7c4797c3e4eeac03, processorArchitecture=MSIL"> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
<SpecificVersion>False</SpecificVersion> <PackageReference Include="System.Configuration.ConfigurationManager" Version="8.0.0" />
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\api\redist\SolidWorks.Interop.sldworks.dll</HintPath>
</Reference>
<Reference Include="SolidWorks.Interop.swconst, Version=24.1.0.45, Culture=neutral, PublicKeyToken=19f43e188e4269d8, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\api\redist\SolidWorks.Interop.swconst.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.configuration" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Security" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="SolidWorks.Interop.sldworks">
<HintPath>C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\api\redist\SolidWorks.Interop.sldworks.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="SolidWorks.Interop.swconst">
<HintPath>C:\Program Files\SOLIDWORKS Corp\SOLIDWORKS\api\redist\SolidWorks.Interop.swconst.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EtchBendLines\EtchBendLines\EtchBendLines.csproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Bend.cs" />
<Compile Include="BendDirection.cs" />
<Compile Include="BendOrientation.cs" />
<Compile Include="Bounds.cs" />
<Compile Include="DrawingInfo.cs" />
<Compile Include="BomToExcel.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="ItemExtractors\AssemblyItemExtractor.cs" />
<Compile Include="ItemExtractors\BomColumnIndices.cs" />
<Compile Include="ItemExtractors\BomItemExtractor.cs" />
<Compile Include="ItemExtractors\ItemExtractor.cs" />
<Compile Include="Forms\ViewFlipDeciderComboboxItem.cs" />
<Compile Include="Item.cs" />
<Compile Include="ViewFlipDeciders\AskViewFlipDecider.cs" />
<Compile Include="ViewFlipDeciders\AutoViewFlipDecider.cs" />
<Compile Include="ViewFlipDeciders\IViewFlipDecider.cs" />
<Compile Include="Helper.cs" />
<Compile Include="Forms\MainForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="Forms\MainForm.Designer.cs">
<DependentUpon>MainForm.cs</DependentUpon>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SolidWorksExtensions.cs" />
<Compile Include="Units.cs" />
<Compile Include="ViewFlipDeciders\PreferUpViewFlipDecider.cs" />
<Compile Include="ViewHelper.cs" />
<EmbeddedResource Include="Forms\MainForm.resx">
<DependentUpon>MainForm.cs</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
<DesignTime>True</DesignTime>
</Compile>
<None Include="app.config" />
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<Content Include="Templates\Blank.drwdot"> <Content Include="Templates\Blank.drwdot">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Templates\BomTemplate.xlsx">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="Resources\edit_alt.png" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\play.png" />
</ItemGroup>
<ItemGroup>
<None Include="Resources\stop_alt.png" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.0">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Windows.Installer.4.5">
<Visible>False</Visible>
<ProductName>Windows Installer 4.5</ProductName>
<Install>true</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<PackageReference Include="EPPlus">
<Version>4.5.3.1</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\EtchBendLines\EtchBendLines\EtchBendLines.csproj">
<Project>{229c2fb9-6ad6-4a5d-b83a-d1146573d6f9}</Project>
<Name>EtchBendLines</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project> </Project>
-74
View File
@@ -1,74 +0,0 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace ExportDXF
{
public static class Extensions
{
public static void AppendText(this RichTextBox box, string text, Color color)
{
box.SelectionStart = box.TextLength;
box.SelectionLength = 0;
box.SelectionColor = color;
box.AppendText(text);
box.SelectionColor = box.ForeColor;
}
public static string ToReadableFormat(this TimeSpan ts)
{
var s = new StringBuilder();
if (ts.TotalHours >= 1)
{
var hrs = ts.Hours + ts.Days * 24.0;
s.Append(string.Format("{0}hrs ", hrs));
s.Append(string.Format("{0}min ", ts.Minutes));
s.Append(string.Format("{0}sec", ts.Seconds));
}
else if (ts.TotalMinutes >= 1)
{
s.Append(string.Format("{0}min ", ts.Minutes));
s.Append(string.Format("{0}sec", ts.Seconds));
}
else
{
s.Append(string.Format("{0} seconds", ts.Seconds));
}
return s.ToString();
}
public static string PunctuateList(this IEnumerable<string> stringList)
{
var list = stringList.ToList();
switch (list.Count)
{
case 0:
return string.Empty;
case 1:
return list[0];
case 2:
return string.Format("{0} and {1}", list[0], list[1]);
default:
var s = string.Empty;
for (int i = 0; i < list.Count - 1; i++)
s += list[i] + ", ";
s += "and " + list.Last();
return s;
}
}
}
}
@@ -5,7 +5,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
namespace ExportDXF namespace ExportDXF.Extensions
{ {
public static class SolidWorksExtensions public static class SolidWorksExtensions
{ {
+48
View File
@@ -0,0 +1,48 @@
namespace ExportDXF.Extensions
{
/// <summary>
/// Extension methods for string manipulation.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Extension method to remove XML tags from a string.
/// </summary>
public static string RemoveXmlTags(this string input)
{
return ExportDXF.Utilities.TextHelper.RemoveXmlTags(input);
}
/// <summary>
/// Extension method to clean text (remove XML and normalize whitespace).
/// </summary>
public static string CleanText(this string input)
{
return ExportDXF.Utilities.TextHelper.CleanText(input);
}
/// <summary>
/// Extension method to sanitize a filename.
/// </summary>
public static string SanitizeFileName(this string input)
{
return ExportDXF.Utilities.TextHelper.SanitizeFileName(input);
}
/// <summary>
/// Extension method to truncate a string.
/// </summary>
public static string Truncate(this string input, int maxLength, bool useEllipsis = true)
{
return ExportDXF.Utilities.TextHelper.Truncate(input, maxLength, useEllipsis);
}
/// <summary>
/// Extension method to convert to title case.
/// </summary>
public static string ToTitleCase(this string input)
{
return ExportDXF.Utilities.TextHelper.ToTitleCase(input);
}
}
}
@@ -0,0 +1,31 @@
using System;
namespace ExportDXF.Extensions
{
/// <summary>
/// Extension methods for TimeSpan formatting.
/// </summary>
public static class TimeSpanExtensions
{
/// <summary>
/// Formats a TimeSpan into a human-readable string.
/// </summary>
/// <param name="ts">The TimeSpan to format.</param>
/// <returns>A human-readable duration string.</returns>
public static string ToReadableFormat(this TimeSpan ts)
{
if (ts.TotalHours >= 1)
{
var totalHours = (int)(ts.TotalDays * 24 + ts.Hours);
return $"{totalHours}hrs {ts.Minutes}min {ts.Seconds}sec";
}
if (ts.TotalMinutes >= 1)
{
return $"{ts.Minutes}min {ts.Seconds}sec";
}
return $"{ts.Seconds} seconds";
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using System.Drawing;
using System.Windows.Forms;
namespace ExportDXF.Extensions
{
public static class UIExtensions
{
public static void AppendText(this RichTextBox box, string text, Color color)
{
box.SelectionStart = box.TextLength;
box.SelectionLength = 0;
box.SelectionColor = color;
box.AppendText(text);
box.SelectionColor = box.ForeColor;
}
}
}
@@ -0,0 +1,41 @@
namespace ExportDXF.Extensions
{
/// <summary>
/// Extension methods for unit conversion between SolidWorks (meters) and millimeters.
/// </summary>
public static class UnitConversionExtensions
{
private const double METERS_TO_MM = 1000;
private const double METERS_TO_INCHES = 39.37007874;
/// <summary>
/// Converts a SolidWorks dimension (in meters) to millimeters.
/// </summary>
/// <param name="meters">The value in meters.</param>
/// <returns>The value in millimeters.</returns>
public static double FromSolidWorksToMM(this double meters)
{
return meters * METERS_TO_MM;
}
/// <summary>
/// Converts millimeters to SolidWorks dimension (meters).
/// </summary>
/// <param name="millimeters">The value in millimeters.</param>
/// <returns>The value in meters.</returns>
public static double FromMMToSolidWorks(this double millimeters)
{
return millimeters / METERS_TO_MM;
}
public static double FromSolidWorksToInches(this double meters)
{
return meters * METERS_TO_INCHES;
}
public static double FromInchesToSolidWorks(this double inches)
{
return inches / METERS_TO_INCHES;
}
}
}
+216 -111
View File
@@ -1,4 +1,4 @@
namespace ExportDXF.Forms namespace ExportDXF.Forms
{ {
partial class MainForm partial class MainForm
{ {
@@ -28,131 +28,236 @@
/// </summary> /// </summary>
private void InitializeComponent() private void InitializeComponent()
{ {
this.activeDocTitleBox = new System.Windows.Forms.TextBox(); runButton = new System.Windows.Forms.Button();
this.richTextBox1 = new System.Windows.Forms.RichTextBox(); label3 = new System.Windows.Forms.Label();
this.label1 = new System.Windows.Forms.Label(); viewFlipDeciderBox = new System.Windows.Forms.ComboBox();
this.label2 = new System.Windows.Forms.Label(); mainTabControl = new System.Windows.Forms.TabControl();
this.prefixTextBox = new System.Windows.Forms.TextBox(); logEventsTab = new System.Windows.Forms.TabPage();
this.button1 = new System.Windows.Forms.Button(); logEventsDataGrid = new System.Windows.Forms.DataGridView();
this.label3 = new System.Windows.Forms.Label(); bomTab = new System.Windows.Forms.TabPage();
this.comboBox1 = new System.Windows.Forms.ComboBox(); bomDataGrid = new System.Windows.Forms.DataGridView();
this.SuspendLayout(); cutTemplatesTab = new System.Windows.Forms.TabPage();
cutTemplatesDataGrid = new System.Windows.Forms.DataGridView();
equipmentBox = new System.Windows.Forms.ComboBox();
label1 = new System.Windows.Forms.Label();
label2 = new System.Windows.Forms.Label();
drawingNoBox = new System.Windows.Forms.ComboBox();
titleLabel = new System.Windows.Forms.Label();
titleBox = new System.Windows.Forms.TextBox();
mainTabControl.SuspendLayout();
logEventsTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).BeginInit();
bomTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)bomDataGrid).BeginInit();
cutTemplatesTab.SuspendLayout();
((System.ComponentModel.ISupportInitialize)cutTemplatesDataGrid).BeginInit();
SuspendLayout();
// //
// activeDocTitleBox // runButton
// //
this.activeDocTitleBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) runButton.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right;
| System.Windows.Forms.AnchorStyles.Right))); runButton.Location = new System.Drawing.Point(508, 12);
this.activeDocTitleBox.BackColor = System.Drawing.Color.White; runButton.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.activeDocTitleBox.Location = new System.Drawing.Point(130, 13); runButton.Name = "runButton";
this.activeDocTitleBox.Name = "activeDocTitleBox"; runButton.Size = new System.Drawing.Size(65, 87);
this.activeDocTitleBox.ReadOnly = true; runButton.TabIndex = 11;
this.activeDocTitleBox.Size = new System.Drawing.Size(584, 25); runButton.Text = "Start";
this.activeDocTitleBox.TabIndex = 2; runButton.UseVisualStyleBackColor = true;
this.activeDocTitleBox.TextChanged += new System.EventHandler(this.textBox1_TextChanged); runButton.Click += button1_Click;
//
// richTextBox1
//
this.richTextBox1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.richTextBox1.BackColor = System.Drawing.Color.White;
this.richTextBox1.Location = new System.Drawing.Point(12, 106);
this.richTextBox1.Name = "richTextBox1";
this.richTextBox1.ReadOnly = true;
this.richTextBox1.Size = new System.Drawing.Size(754, 342);
this.richTextBox1.TabIndex = 3;
this.richTextBox1.Text = "";
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(13, 16);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(111, 17);
this.label1.TabIndex = 4;
this.label1.Text = "Active document :";
//
// label2
//
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(23, 47);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(101, 17);
this.label2.TabIndex = 4;
this.label2.Text = "Prefix files with :";
//
// prefixTextBox
//
this.prefixTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.prefixTextBox.Location = new System.Drawing.Point(130, 44);
this.prefixTextBox.Name = "prefixTextBox";
this.prefixTextBox.Size = new System.Drawing.Size(584, 25);
this.prefixTextBox.TabIndex = 2;
//
// button1
//
this.button1.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.button1.Image = global::ExportDXF.Properties.Resources.play;
this.button1.Location = new System.Drawing.Point(720, 13);
this.button1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(46, 56);
this.button1.TabIndex = 0;
this.button1.UseVisualStyleBackColor = true;
this.button1.Click += new System.EventHandler(this.button1_Click);
// //
// label3 // label3
// //
this.label3.AutoSize = true; label3.AutoSize = true;
this.label3.Location = new System.Drawing.Point(12, 78); label3.Location = new System.Drawing.Point(26, 77);
this.label3.Name = "label3"; label3.Name = "label3";
this.label3.Size = new System.Drawing.Size(112, 17); label3.Size = new System.Drawing.Size(105, 17);
this.label3.TabIndex = 4; label3.TabIndex = 2;
this.label3.Text = "View flip decider :"; label3.Text = "View flip decider";
// //
// comboBox1 // viewFlipDeciderBox
// //
this.comboBox1.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; viewFlipDeciderBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.comboBox1.FormattingEnabled = true; viewFlipDeciderBox.FormattingEnabled = true;
this.comboBox1.Location = new System.Drawing.Point(130, 75); viewFlipDeciderBox.Location = new System.Drawing.Point(137, 74);
this.comboBox1.Name = "comboBox1"; viewFlipDeciderBox.Name = "viewFlipDeciderBox";
this.comboBox1.Size = new System.Drawing.Size(353, 25); viewFlipDeciderBox.Size = new System.Drawing.Size(365, 25);
this.comboBox1.TabIndex = 5; viewFlipDeciderBox.TabIndex = 3;
//
// mainTabControl
//
mainTabControl.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
mainTabControl.Controls.Add(logEventsTab);
mainTabControl.Controls.Add(bomTab);
mainTabControl.Controls.Add(cutTemplatesTab);
mainTabControl.Location = new System.Drawing.Point(15, 105);
mainTabControl.Name = "mainTabControl";
mainTabControl.Padding = new System.Drawing.Point(20, 5);
mainTabControl.SelectedIndex = 0;
mainTabControl.Size = new System.Drawing.Size(910, 492);
mainTabControl.TabIndex = 12;
//
// logEventsTab
//
logEventsTab.Controls.Add(logEventsDataGrid);
logEventsTab.Location = new System.Drawing.Point(4, 30);
logEventsTab.Name = "logEventsTab";
logEventsTab.Padding = new System.Windows.Forms.Padding(3);
logEventsTab.Size = new System.Drawing.Size(902, 458);
logEventsTab.TabIndex = 0;
logEventsTab.Text = "Log Events";
logEventsTab.UseVisualStyleBackColor = true;
//
// logEventsDataGrid
//
logEventsDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
logEventsDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
logEventsDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
logEventsDataGrid.Location = new System.Drawing.Point(6, 6);
logEventsDataGrid.Name = "logEventsDataGrid";
logEventsDataGrid.Size = new System.Drawing.Size(890, 444);
logEventsDataGrid.TabIndex = 0;
//
// bomTab
//
bomTab.Controls.Add(bomDataGrid);
bomTab.Location = new System.Drawing.Point(4, 30);
bomTab.Name = "bomTab";
bomTab.Padding = new System.Windows.Forms.Padding(3);
bomTab.Size = new System.Drawing.Size(902, 458);
bomTab.TabIndex = 1;
bomTab.Text = "Bill Of Materials";
bomTab.UseVisualStyleBackColor = true;
//
// bomDataGrid
//
bomDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
bomDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
bomDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
bomDataGrid.Location = new System.Drawing.Point(6, 6);
bomDataGrid.Name = "bomDataGrid";
bomDataGrid.Size = new System.Drawing.Size(890, 444);
bomDataGrid.TabIndex = 1;
//
// cutTemplatesTab
//
cutTemplatesTab.Controls.Add(cutTemplatesDataGrid);
cutTemplatesTab.Location = new System.Drawing.Point(4, 30);
cutTemplatesTab.Name = "cutTemplatesTab";
cutTemplatesTab.Padding = new System.Windows.Forms.Padding(3);
cutTemplatesTab.Size = new System.Drawing.Size(902, 458);
cutTemplatesTab.TabIndex = 2;
cutTemplatesTab.Text = "Cut Templates";
cutTemplatesTab.UseVisualStyleBackColor = true;
//
// cutTemplatesDataGrid
//
cutTemplatesDataGrid.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
cutTemplatesDataGrid.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
cutTemplatesDataGrid.GridColor = System.Drawing.Color.WhiteSmoke;
cutTemplatesDataGrid.Location = new System.Drawing.Point(6, 6);
cutTemplatesDataGrid.Name = "cutTemplatesDataGrid";
cutTemplatesDataGrid.Size = new System.Drawing.Size(890, 447);
cutTemplatesDataGrid.TabIndex = 2;
//
// equipmentBox
//
equipmentBox.FormattingEnabled = true;
equipmentBox.Location = new System.Drawing.Point(137, 12);
equipmentBox.Name = "equipmentBox";
equipmentBox.Size = new System.Drawing.Size(166, 25);
equipmentBox.TabIndex = 13;
//
// label1
//
label1.AutoSize = true;
label1.Location = new System.Drawing.Point(61, 15);
label1.Name = "label1";
label1.Size = new System.Drawing.Size(70, 17);
label1.TabIndex = 2;
label1.Text = "Equipment";
//
// label2
//
label2.AutoSize = true;
label2.Location = new System.Drawing.Point(321, 15);
label2.Name = "label2";
label2.Size = new System.Drawing.Size(56, 17);
label2.TabIndex = 2;
label2.Text = "Drawing";
//
// drawingNoBox
//
drawingNoBox.FormattingEnabled = true;
drawingNoBox.Location = new System.Drawing.Point(383, 12);
drawingNoBox.Name = "drawingNoBox";
drawingNoBox.Size = new System.Drawing.Size(119, 25);
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
// //
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.ClientSize = new System.Drawing.Size(778, 460); ClientSize = new System.Drawing.Size(937, 609);
this.Controls.Add(this.comboBox1); Controls.Add(titleBox);
this.Controls.Add(this.label3); Controls.Add(titleLabel);
this.Controls.Add(this.label2); Controls.Add(drawingNoBox);
this.Controls.Add(this.label1); Controls.Add(equipmentBox);
this.Controls.Add(this.richTextBox1); Controls.Add(mainTabControl);
this.Controls.Add(this.prefixTextBox); Controls.Add(viewFlipDeciderBox);
this.Controls.Add(this.activeDocTitleBox); Controls.Add(label2);
this.Controls.Add(this.button1); Controls.Add(label1);
this.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); Controls.Add(label3);
this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4); Controls.Add(runButton);
this.MaximizeBox = false; Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, 0);
this.Name = "MainForm"; Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; MaximizeBox = false;
this.Text = "ExportDXF"; MinimumSize = new System.Drawing.Size(642, 455);
this.ResumeLayout(false); Name = "MainForm";
this.PerformLayout(); StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
Text = "ExportDXF";
mainTabControl.ResumeLayout(false);
logEventsTab.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)logEventsDataGrid).EndInit();
bomTab.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)bomDataGrid).EndInit();
cutTemplatesTab.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)cutTemplatesDataGrid).EndInit();
ResumeLayout(false);
PerformLayout();
} }
#endregion #endregion
private System.Windows.Forms.Button button1; private System.Windows.Forms.Button runButton;
private System.Windows.Forms.TextBox activeDocTitleBox; private System.Windows.Forms.Label label3;
private System.Windows.Forms.RichTextBox richTextBox1; private System.Windows.Forms.ComboBox viewFlipDeciderBox;
private System.Windows.Forms.TabControl mainTabControl;
private System.Windows.Forms.TabPage logEventsTab;
private System.Windows.Forms.TabPage bomTab;
private System.Windows.Forms.DataGridView logEventsDataGrid;
private System.Windows.Forms.DataGridView bomDataGrid;
private System.Windows.Forms.TabPage cutTemplatesTab;
private System.Windows.Forms.DataGridView cutTemplatesDataGrid;
private System.Windows.Forms.ComboBox equipmentBox;
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.TextBox prefixTextBox; private System.Windows.Forms.ComboBox drawingNoBox;
private System.Windows.Forms.Label label3; private System.Windows.Forms.Label titleLabel;
private System.Windows.Forms.ComboBox comboBox1; private System.Windows.Forms.TextBox titleBox;
} }
} }
File diff suppressed because it is too large Load Diff
-61
View File
@@ -1,61 +0,0 @@
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace ExportDXF
{
public static class Helper
{
public static string GetNumWithSuffix(int i)
{
if (i >= 11 && i <= 13)
return i.ToString() + "th";
var j = i % 10;
switch (j)
{
case 1: return i.ToString() + "st";
case 2: return i.ToString() + "nd";
case 3: return i.ToString() + "rd";
default: return i.ToString() + "th";
}
}
public static void UncheckFlatPatternCornerTreatment(ModelDoc2 model)
{
var flatPattern = model.GetFeatureByTypeName("FlatPattern");
var flatPatternFeatureData = flatPattern.GetDefinition() as FlatPatternFeatureData;
flatPatternFeatureData.CornerTreatment = false;
flatPatternFeatureData.SimplifyBends = true;
var ret = flatPattern.ModifyDefinition(flatPatternFeatureData, model, null);
}
public static bool SetFlatPatternSuppressionState(ModelDoc2 model, swComponentSuppressionState_e suppressionState)
{
var flatPattern = model.GetFeatureByTypeName("FlatPattern");
flatPattern.SetSuppression((int)suppressionState);
return flatPattern.IsSuppressed();
}
public static string RemoveXmlTags(string input)
{
// Define the regular expression pattern to match XML tags
string pattern = @"<[^>]+>";
// Replace all matches of the pattern with an empty string
string result = Regex.Replace(input, pattern, "");
return result;
}
}
}
-29
View File
@@ -1,29 +0,0 @@
using SolidWorks.Interop.sldworks;
namespace ExportDXF
{
public class Item
{
public string ItemNo { get; set; }
public string FileName { get; set; }
public string PartName { get; set; }
public string Configuration { get; set; }
public int Quantity { get; set; }
public string Description { get; set; }
public double Thickness { get; set; }
public double KFactor { get; set; }
public double BendRadius { get; set; }
public string Material { get; set; }
public Component2 Component { get; set; }
}
}
@@ -1,4 +1,6 @@
using SolidWorks.Interop.sldworks; using ExportDXF.Extensions;
using ExportDXF.Services;
using SolidWorks.Interop.sldworks;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
+8 -5
View File
@@ -1,4 +1,7 @@
using SolidWorks.Interop.sldworks; using ExportDXF.Extensions;
using ExportDXF.Services;
using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst; using SolidWorks.Interop.swconst;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -52,7 +55,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.ItemNumber != -1) if (columnIndices.ItemNumber != -1)
{ {
var x = table.DisplayedText[rowIndex, columnIndices.ItemNumber]; var x = table.DisplayedText[rowIndex, columnIndices.ItemNumber];
x = Helper.RemoveXmlTags(x); x = TextHelper.RemoveXmlTags(x);
double d; double d;
@@ -69,7 +72,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.PartNumber != -1) if (columnIndices.PartNumber != -1)
{ {
var x = table.DisplayedText[rowIndex, columnIndices.PartNumber]; var x = table.DisplayedText[rowIndex, columnIndices.PartNumber];
x = Helper.RemoveXmlTags(x); x = TextHelper.RemoveXmlTags(x);
item.PartName = x; item.PartName = x;
@@ -78,7 +81,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.Description != -1) if (columnIndices.Description != -1)
{ {
var x = table.DisplayedText[rowIndex, columnIndices.Description]; var x = table.DisplayedText[rowIndex, columnIndices.Description];
x = Helper.RemoveXmlTags(x); x = TextHelper.RemoveXmlTags(x);
item.Description = x; item.Description = x;
} }
@@ -86,7 +89,7 @@ namespace ExportDXF.ItemExtractors
if (columnIndices.Quantity != -1) if (columnIndices.Quantity != -1)
{ {
var qtyString = table.DisplayedText[rowIndex, columnIndices.Quantity]; var qtyString = table.DisplayedText[rowIndex, columnIndices.Quantity];
qtyString = Helper.RemoveXmlTags(qtyString); qtyString = TextHelper.RemoveXmlTags(qtyString);
int qty = 0; int qty = 0;
int.TryParse(qtyString, out qty); int.TryParse(qtyString, out qty);
+2 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic; using ExportDXF.Services;
using System.Collections.Generic;
namespace ExportDXF.ItemExtractors namespace ExportDXF.ItemExtractors
{ {
+29
View File
@@ -0,0 +1,29 @@
namespace ExportDXF.Models
{
public class BomItem
{
public int ID { get; set; }
public string ItemNo { get; set; } = "";
public string PartNo { get; set; } = "";
public int SortOrder { get; set; }
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; } = "";
// EF Core relationship to ExportRecord
public int ExportRecordId { get; set; }
public virtual ExportRecord ExportRecord { get; set; }
// Optional 1:1 relationship to CutTemplate (only for sheet metal parts)
public virtual CutTemplate CutTemplate { get; set; }
}
public struct Size
{
public double Width { get; set; }
public double Height { get; set; }
}
}
+33
View File
@@ -0,0 +1,33 @@
using System;
namespace ExportDXF.Models
{
public class CutTemplate
{
public int Id { get; set; }
public string DxfFilePath { get; set; } = "";
public string ContentHash { get; set; }
public string CutTemplateName { get; set; } = "";
// Sheet metal properties (moved from BomItem)
private double? _thickness;
public double? Thickness
{
get => _thickness;
set => _thickness = value.HasValue ? Math.Round(value.Value, 8) : null;
}
public double? KFactor { get; set; }
private double? _defaultBendRadius;
public double? DefaultBendRadius
{
get => _defaultBendRadius;
set => _defaultBendRadius = value.HasValue ? Math.Round(value.Value, 8) : null;
}
// FK back to BomItem
public int BomItemId { get; set; }
public virtual BomItem BomItem { get; set; }
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace ExportDXF.Models
{
/// <summary>
/// Enumeration of SolidWorks document types.
/// </summary>
public enum DocumentType
{
Unknown,
Part,
Assembly,
Drawing
}
}
+156
View File
@@ -0,0 +1,156 @@
using ExportDXF.Models;
using ExportDXF.ViewFlipDeciders;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.IO;
using System.Threading;
using System.Windows.Forms;
namespace ExportDXF.Services
{
/// <summary>
/// Context object containing all information needed for an export operation.
/// </summary>
public class ExportContext
{
private const string DRAWING_TEMPLATE_FOLDER = "Templates";
private const string DRAWING_TEMPLATE_FILE = "Blank.drwdot";
/// <summary>
/// The document to be exported.
/// </summary>
public SolidWorksDocument ActiveDocument { get; set; }
/// <summary>
/// The view flip decider to determine if views should be flipped.
/// </summary>
public IViewFlipDecider ViewFlipDecider { get; set; }
/// <summary>
/// Prefix to prepend to exported filenames.
/// </summary>
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>
/// Selected Equipment ID for API operations (optional).
/// </summary>
public int? EquipmentId { get; set; }
/// <summary>
/// Cancellation token for canceling the export operation.
/// </summary>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// Callback for reporting progress and status messages.
/// Parameters: message, level, file (optional)
/// </summary>
public Action<string, LogLevel, string> ProgressCallback { get; set; }
/// <summary>
/// Callback for adding BOM items to the UI.
/// </summary>
public Action<BomItem> BomItemCallback { get; set; }
public void LogProgress(string message, LogLevel level = LogLevel.Info, string file = null)
{
ProgressCallback?.Invoke(message, level, file);
}
public SldWorks SolidWorksApp { get; set; }
public DrawingDoc TemplateDrawing { get; set; }
private string DrawingTemplatePath
{
get
{
return Path.Combine(
Application.StartupPath,
DRAWING_TEMPLATE_FOLDER,
DRAWING_TEMPLATE_FILE);
}
}
public DrawingDoc GetOrCreateTemplateDrawing()
{
if (TemplateDrawing != null)
return TemplateDrawing;
TemplateDrawing = SolidWorksApp.NewDocument(
DrawingTemplatePath,
(int)swDwgPaperSizes_e.swDwgPaperDsize,
1,
1) as DrawingDoc;
return TemplateDrawing;
}
public void CleanupTemplateDrawing()
{
try
{
if (TemplateDrawing == null)
return;
if (SolidWorksApp == null)
{
ProgressCallback?.Invoke("Warning: Cannot cleanup template drawing - SolidWorks app not available", LogLevel.Warning, null);
TemplateDrawing = null;
return;
}
var model = TemplateDrawing as ModelDoc2;
if (model != null)
{
var title = model.GetTitle();
if (!string.IsNullOrEmpty(title))
{
// Close the document without saving
SolidWorksApp.CloseDoc(title);
ProgressCallback?.Invoke("Closed template drawing", LogLevel.Info, null);
}
}
// Clear the reference regardless of success/failure
TemplateDrawing = null;
}
catch (Exception ex)
{
ProgressCallback?.Invoke($"Failed to close template drawing: {ex.Message}", LogLevel.Error, null);
// Still clear the reference to prevent further issues
TemplateDrawing = null;
// Don't throw here as this is cleanup code - log the error but continue
}
}
public void CloseDocument(string title)
{
SolidWorksApp?.CloseDoc(title);
}
public ModelDoc2 CreateDocument(string templatePath, int paperSize, double width, double height)
{
return SolidWorksApp?.NewDocument(templatePath, paperSize, width, height) as ModelDoc2;
}
}
}
+20
View File
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
namespace ExportDXF.Models
{
public class ExportRecord
{
public int Id { get; set; }
public string DrawingNumber { 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 virtual ICollection<BomItem> BomItems { get; set; } = new List<BomItem>();
}
}
+76
View File
@@ -0,0 +1,76 @@
using SolidWorks.Interop.sldworks;
namespace ExportDXF.Services
{
/// <summary>
/// Represents an item extracted from a BOM or assembly.
/// </summary>
public class Item
{
/// <summary>
/// Item number from the BOM.
/// </summary>
public string ItemNo { get; set; }
/// <summary>
/// Part name or file name.
/// </summary>
public string PartName { get; set; }
/// <summary>
/// Configuration name.
/// </summary>
public string Configuration { get; set; }
/// <summary>
/// Item description.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Quantity of this item.
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// Material specification.
/// </summary>
public string Material { get; set; }
/// <summary>
/// Sheet metal thickness in millimeters.
/// </summary>
public double Thickness { get; set; }
/// <summary>
/// Sheet metal K-factor.
/// </summary>
public double KFactor { get; set; }
/// <summary>
/// Bend radius in millimeters.
/// </summary>
public double BendRadius { get; set; }
/// <summary>
/// The exported DXF filename (without path or extension).
/// </summary>
public string FileName { get; set; }
/// <summary>
/// The SolidWorks component reference.
/// </summary>
public Component2 Component { get; set; }
/// <summary>
/// SHA256 content hash of the exported DXF (transient, not persisted).
/// </summary>
public string ContentHash { get; set; }
/// <summary>
/// Full path to the locally-exported DXF temp file (transient, not persisted).
/// Set after successful export; used for upload to the API.
/// </summary>
public string LocalTempPath { get; set; }
}
}
+22
View File
@@ -0,0 +1,22 @@
using System;
namespace ExportDXF.Models
{
public enum LogLevel { Info, Warning, Error }
public enum LogAction { Start, FindBom, CreateFlat, FlipView, SavePdf, UploadPdf, UploadDxf, CreateBomItem }
public sealed class LogEvent
{
public DateTime Time { get; set; } = DateTime.Now;
public LogLevel Level { get; set; }
public string Equipment { get; set; } = "";
public string Drawing { get; set; } = "";
public string Part { get; set; } = "";
public LogAction Action { get; set; }
public string Target { get; set; } = "";
public string Result { get; set; } = "OK";
public int DurationMs { get; set; }
public string Message { get; set; } = "";
}
}
+30
View File
@@ -0,0 +1,30 @@
using ExportDXF.Models;
namespace ExportDXF.Services
{
/// <summary>
/// Represents a SolidWorks document with essential metadata.
/// </summary>
public class SolidWorksDocument
{
/// <summary>
/// The title/name of the document.
/// </summary>
public string Title { get; set; }
/// <summary>
/// The full file path of the document.
/// </summary>
public string FilePath { get; set; }
/// <summary>
/// The type of document (Part, Assembly, or Drawing).
/// </summary>
public DocumentType DocumentType { get; set; }
/// <summary>
/// The native SolidWorks document object (ModelDoc2, PartDoc, etc.).
/// </summary>
public object NativeDocument { get; set; }
}
}
+101 -4
View File
@@ -1,19 +1,116 @@
using System; using ExportDXF.ApiClient;
using ExportDXF.Forms;
using ExportDXF.Services;
using System;
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
{ {
internal static class Program static class Program
{ {
/// <summary> /// <summary>
/// The main entry point for the application. /// The main entry point for the application.
/// </summary> /// </summary>
[STAThread] [STAThread]
private static void Main() static void Main()
{ {
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Forms.MainForm());
var container = new ServiceContainer();
var mainForm = container.ResolveMainForm();
Application.Run(mainForm);
}
}
/// <summary>
/// Simple dependency injection container.
/// </summary>
public class ServiceContainer
{
private readonly string _apiBaseUrl;
public ServiceContainer()
{
_apiBaseUrl = ConfigurationManager.AppSettings["FabWorksApiUrl"] ?? "http://localhost:5206";
}
public MainForm ResolveMainForm()
{
var solidWorksService = new SolidWorksService();
var bomExtractor = new BomExtractor();
var partExporter = new PartExporter();
var drawingExporter = new DrawingExporter();
EnsureApiRunning();
var httpClient = new HttpClient
{
BaseAddress = new Uri(_apiBaseUrl),
Timeout = TimeSpan.FromSeconds(30)
};
var apiClient = new FabWorksApiClient(httpClient);
var exportService = new DxfExportService(
solidWorksService,
bomExtractor,
partExporter,
drawingExporter,
apiClient);
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 { }
}
}
} }
} }
} }
+66
View File
@@ -0,0 +1,66 @@
using ExportDXF.Extensions;
using ExportDXF.ItemExtractors;
using ExportDXF.Models;
using SolidWorks.Interop.sldworks;
using System;
using System.Collections.Generic;
namespace ExportDXF.Services
{
/// <summary>
/// Service for extracting items from a Bill of Materials (BOM).
/// </summary>
public interface IBomExtractor
{
/// <summary>
/// Extracts items from all BOM tables in a drawing document.
/// </summary>
/// <param name="drawing">The drawing document containing BOM tables.</param>
/// <param name="progressCallback">Optional callback for progress updates (message, level, file).</param>
/// <returns>A list of extracted items.</returns>
List<Item> ExtractFromDrawing(DrawingDoc drawing, Action<string, LogLevel, string> progressCallback);
}
public class BomExtractor : IBomExtractor
{
public List<Item> ExtractFromDrawing(DrawingDoc drawing, Action<string, LogLevel, string> progressCallback)
{
if (drawing == null)
throw new ArgumentNullException(nameof(drawing));
var bomTables = drawing.GetBomTables();
if (bomTables.Count == 0)
{
progressCallback?.Invoke("Error: Bill of materials not found.", LogLevel.Error, null);
return new List<Item>();
}
progressCallback?.Invoke($"Found {bomTables.Count} BOM table(s)", LogLevel.Info, null);
var allItems = new List<Item>();
foreach (var bom in bomTables)
{
try
{
var extractor = new BomItemExtractor(bom)
{
SkipHiddenRows = true
};
progressCallback?.Invoke($"Fetching components from {bom.BomFeature.Name}", LogLevel.Info, null);
var items = extractor.GetItems();
allItems.AddRange(items);
}
catch (Exception ex)
{
progressCallback?.Invoke($"Failed to extract: {ex.Message}", LogLevel.Error, bom.BomFeature.Name);
}
}
return allItems;
}
}
}
+115
View File
@@ -0,0 +1,115 @@
using ExportDXF.Models;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Service for exporting drawing documents to PDF format.
/// </summary>
public interface IDrawingExporter
{
/// <summary>
/// Exports a drawing document to PDF format.
/// </summary>
/// <param name="drawing">The drawing document to export.</param>
/// <param name="saveDirectory">The directory where the PDF file will be saved.</param>
/// <param name="context">The export context containing SolidWorks app and callbacks.</param>
void ExportToPdf(DrawingDoc drawing, string saveDirectory, ExportContext context);
}
public class DrawingExporter : IDrawingExporter
{
public void ExportToPdf(DrawingDoc drawing, string saveDirectory, ExportContext context)
{
if (drawing == null)
throw new ArgumentNullException(nameof(drawing));
if (string.IsNullOrWhiteSpace(saveDirectory))
throw new ArgumentException("Save directory cannot be null or empty.", nameof(saveDirectory));
if (context == null)
throw new ArgumentNullException(nameof(context));
if (context.SolidWorksApp == null)
throw new ArgumentException("SolidWorksApp cannot be null in context.", nameof(context));
try
{
var pdfFileName = GetPdfFileName(drawing);
var pdfPath = Path.Combine(saveDirectory, pdfFileName);
var model = drawing as ModelDoc2;
var sldWorks = context.SolidWorksApp;
var exportData = sldWorks.GetExportFileData(
(int)swExportDataFileType_e.swExportPdfData) as ExportPdfData;
if (exportData == null)
{
throw new InvalidOperationException("Failed to get PDF export data from SolidWorks.");
}
exportData.ViewPdfAfterSaving = false;
exportData.SetSheets(
(int)swExportDataSheetsToExport_e.swExportData_ExportAllSheets,
drawing);
context.ProgressCallback?.Invoke("Exporting drawing to PDF", LogLevel.Info, pdfFileName);
int errors = 0;
int warnings = 0;
var modelExtension = model.Extension;
var success = modelExtension.SaveAs(
pdfPath,
(int)swSaveAsVersion_e.swSaveAsCurrentVersion,
(int)swSaveAsOptions_e.swSaveAsOptions_Silent,
exportData,
ref errors,
ref warnings);
if (success && errors == 0)
{
context.ProgressCallback?.Invoke("Saved drawing to PDF", LogLevel.Info, pdfFileName);
}
else if (success && warnings > 0)
{
context.ProgressCallback?.Invoke(
$"PDF export completed with warnings: {warnings}",
LogLevel.Warning, pdfFileName);
}
else
{
context.ProgressCallback?.Invoke(
$"PDF export failed. Errors: {errors}, Warnings: {warnings}",
LogLevel.Error, pdfFileName);
throw new InvalidOperationException($"PDF export failed with {errors} errors and {warnings} warnings.");
}
}
catch (Exception ex)
{
var errorMessage = $"Failed to export PDF: {ex.Message}";
context.ProgressCallback?.Invoke(errorMessage, LogLevel.Error, null);
throw new InvalidOperationException(errorMessage, ex);
}
}
private string GetPdfFileName(DrawingDoc drawing)
{
var model = drawing as ModelDoc2;
var modelFilePath = model.GetPathName();
if (string.IsNullOrEmpty(modelFilePath))
{
// Handle unsaved documents
var title = model.GetTitle();
return string.IsNullOrEmpty(title) ? "Untitled.pdf" : Path.GetFileNameWithoutExtension(title) + ".pdf";
}
return Path.GetFileNameWithoutExtension(modelFilePath) + ".pdf";
}
}
}
+686
View File
@@ -0,0 +1,686 @@
using ExportDXF.ApiClient;
using ExportDXF.Extensions;
using ExportDXF.ItemExtractors;
using ExportDXF.Models;
using ExportDXF.Utilities;
using ExportDXF;
using SolidWorks.Interop.sldworks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace ExportDXF.Services
{
public interface IDxfExportService
{
/// <summary>
/// Exports the document specified in the context to DXF format.
/// </summary>
/// <param name="context">The export context containing all necessary information.</param>
Task ExportAsync(ExportContext context);
}
/// <summary>
/// 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>
public class DxfExportService : IDxfExportService
{
private readonly ISolidWorksService _solidWorksService;
private readonly IBomExtractor _bomExtractor;
private readonly IPartExporter _partExporter;
private readonly IDrawingExporter _drawingExporter;
private readonly IFabWorksApiClient _apiClient;
public DxfExportService(
ISolidWorksService solidWorksService,
IBomExtractor bomExtractor,
IPartExporter partExporter,
IDrawingExporter drawingExporter,
IFabWorksApiClient apiClient)
{
_solidWorksService = solidWorksService ?? throw new ArgumentNullException(nameof(solidWorksService));
_bomExtractor = bomExtractor ?? throw new ArgumentNullException(nameof(bomExtractor));
_partExporter = partExporter ?? throw new ArgumentNullException(nameof(partExporter));
_drawingExporter = drawingExporter ?? throw new ArgumentNullException(nameof(drawingExporter));
_apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
}
/// <summary>
/// Exports the document specified in the context to DXF format.
/// </summary>
public async Task ExportAsync(ExportContext context)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
ValidateContext(context);
SetupExportContext(context);
var startTime = DateTime.Now;
var tempDir = CreateTempWorkDir();
try
{
_solidWorksService.EnableUserControl(false);
var drawingNumber = ParseDrawingNumber(context);
switch (context.ActiveDocument.DocumentType)
{
case DocumentType.Part:
await ExportPartAsync(context, tempDir, drawingNumber);
break;
case DocumentType.Assembly:
await ExportAssemblyAsync(context, tempDir, drawingNumber);
break;
case DocumentType.Drawing:
await ExportDrawingAsync(context, drawingNumber, tempDir);
break;
default:
LogProgress(context, "Unknown document type.", LogLevel.Error);
break;
}
}
finally
{
CleanupExportContext(context);
_solidWorksService.EnableUserControl(true);
CleanupTempDir(tempDir);
var duration = DateTime.Now - startTime;
LogProgress(context, $"Run time: {duration.ToReadableFormat()}");
}
}
#region Export Methods by Document Type
private async Task ExportPartAsync(ExportContext context, string tempDir, string drawingNumber)
{
LogProgress(context, "Active document is a Part");
var part = context.ActiveDocument.NativeDocument as PartDoc;
if (part == null)
{
LogProgress(context, "Failed to get part document.", LogLevel.Error);
return;
}
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
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 async Task ExportAssemblyAsync(ExportContext context, string tempDir, string drawingNumber)
{
LogProgress(context, "Active document is an Assembly");
LogProgress(context, "Fetching components...");
var assembly = context.ActiveDocument.NativeDocument as AssemblyDoc;
if (assembly == null)
{
LogProgress(context, "Failed to get assembly document.", LogLevel.Error);
return;
}
var items = ExtractItemsFromAssembly(assembly, context);
if (items == null || items.Count == 0)
{
LogProgress(context, "No items found in assembly.", LogLevel.Warning);
return;
}
LogProgress(context, $"Found {items.Count} item(s).");
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
// 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 async Task ExportDrawingAsync(ExportContext context, string drawingNumber, string tempDir)
{
LogProgress(context, "Active document is a Drawing");
var drawing = context.ActiveDocument.NativeDocument as DrawingDoc;
if (drawing == null)
{
LogProgress(context, "Failed to get drawing document.", LogLevel.Error);
return;
}
// Export drawing to PDF in temp dir
_drawingExporter.ExportToPdf(drawing, tempDir, context);
// Create export record via API
var exportRecord = await CreateExportRecordAsync(context, drawingNumber);
// Upload PDF to API with versioning
try
{
var pdfs = Directory.GetFiles(tempDir, "*.pdf");
if (pdfs.Length > 0)
{
var pdfTempPath = pdfs[0];
var pdfHash = ContentHasher.ComputePdfContentHash(pdfTempPath);
var uploadResult = await _apiClient.UploadPdfAsync(
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)
{
LogProgress(context, $"PDF upload error: {ex.Message}", LogLevel.Error);
}
// Extract BOM items from drawing tables
LogProgress(context, "Finding BOM tables...");
var items = _bomExtractor.ExtractFromDrawing(drawing, context.ProgressCallback);
if (items != null && items.Count > 0)
{
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)
{
LogProgress(context, "No referenced part found in drawing views.", LogLevel.Warning);
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
{
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 ?? ""
};
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);
}
}
}
#endregion
#region Context Management
private void SetupExportContext(ExportContext context)
{
// Set up SolidWorks application reference
context.SolidWorksApp = _solidWorksService.GetNativeSldWorks();
if (context.SolidWorksApp == null)
{
throw new InvalidOperationException("SolidWorks service is not connected.");
}
// Set up drawing template path
context.TemplateDrawing = null;
LogProgress(context, "Export context initialized");
}
private void CleanupExportContext(ExportContext context)
{
try
{
// Clean up template drawing if it was created
context.CleanupTemplateDrawing();
}
catch (Exception ex)
{
LogProgress(context, $"Warning: Failed to cleanup template drawing: {ex.Message}", LogLevel.Warning);
// Don't throw - this is cleanup code
}
}
#endregion
#region Item Processing
private List<Item> ExtractItemsFromAssembly(AssemblyDoc assembly, ExportContext context)
{
try
{
var extractor = new AssemblyItemExtractor(assembly)
{
TopLevelOnly = false
};
return extractor.GetItems();
}
catch (Exception ex)
{
LogProgress(context, $"Failed to extract items from assembly: {ex.Message}", LogLevel.Error);
return new List<Item>();
}
}
private async Task ExportItemsAsync(List<Item> items, string tempDir, ExportContext context, int? exportRecordId = null)
{
int successCount = 0;
int skippedCount = 0;
int failureCount = 0;
int sortOrder = 0;
foreach (var item in items)
{
if (context.CancellationToken.IsCancellationRequested)
{
LogProgress(context, "Export canceled by user.", LogLevel.Warning);
return;
}
try
{
// PartExporter will handle template drawing creation through context
_partExporter.ExportItem(item, tempDir, context);
// Always create BomItem for every item (sheet metal or not)
var bomItem = new BomItem
{
ExportRecordId = exportRecordId ?? 0,
ItemNo = item.ItemNo ?? "",
PartNo = item.FileName ?? item.PartName ?? "",
SortOrder = sortOrder++,
Qty = item.Quantity,
TotalQty = item.Quantity,
Description = item.Description ?? "",
PartName = item.PartName ?? "",
ConfigurationName = item.Configuration ?? "",
Material = item.Material ?? ""
};
// Only upload and create CutTemplate if DXF was exported successfully
if (!string.IsNullOrEmpty(item.LocalTempPath))
{
successCount++;
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
};
}
}
else
{
skippedCount++;
}
// Add to UI
context.BomItemCallback?.Invoke(bomItem);
// Save BOM item via API if we have an export record
if (exportRecordId.HasValue)
{
await SaveBomItemAsync(exportRecordId.Value, bomItem, context);
}
}
catch (Exception ex)
{
LogProgress(context, $"Error exporting item {item.ItemNo}: {ex.Message}", LogLevel.Error);
failureCount++;
}
}
var summary = $"Export complete: {successCount} exported, {skippedCount} skipped";
if (failureCount > 0)
summary += $", {failureCount} failed";
LogProgress(context, summary, failureCount > 0 ? LogLevel.Warning : LogLevel.Info);
if (exportRecordId.HasValue)
{
LogProgress(context, $"BOM items saved (ExportRecord ID: {exportRecordId.Value})", LogLevel.Info);
}
}
#endregion
#region File Upload
private async Task<ApiFileUploadResponse> UploadDxfAsync(Item item, ExportContext context)
{
try
{
var result = await _apiClient.UploadDxfAsync(
item.LocalTempPath,
context.Equipment,
context.DrawingNo,
item.ItemNo,
item.ContentHash);
if (result.WasUnchanged)
LogProgress(context, $"DXF unchanged: {result.FileName}", LogLevel.Info);
else if (result.IsNewFile)
LogProgress(context, $"Exported: {result.FileName}", LogLevel.Info);
else
LogProgress(context, $"DXF updated: {result.FileName}", LogLevel.Info);
return result;
}
catch (Exception ex)
{
LogProgress(context, $"DXF upload failed for {item.FileName}: {ex.Message}", LogLevel.Warning);
return null;
}
}
#endregion
#region API Helpers
private async Task<ExportRecord> CreateExportRecordAsync(ExportContext context, string drawingNumber)
{
try
{
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);
var record = new ExportRecord
{
Id = dto.Id,
DrawingNumber = dto.DrawingNumber,
EquipmentNo = dto.EquipmentNo,
DrawingNo = dto.DrawingNo,
SourceFilePath = dto.SourceFilePath,
OutputFolder = dto.OutputFolder,
ExportedAt = dto.ExportedAt,
ExportedBy = dto.ExportedBy
};
LogProgress(context, $"Created export record (ID: {record.Id})", LogLevel.Info);
return record;
}
catch (Exception ex)
{
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);
}
}
#endregion
#region Helper Methods
private string CreateTempWorkDir()
{
var path = Path.Combine(Path.GetTempPath(), "ExportDXF-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(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)
{
// 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 info = string.IsNullOrWhiteSpace(candidate) ? null : DrawingInfo.Parse(candidate);
if (info == null)
{
var title = context?.ActiveDocument?.Title;
info = string.IsNullOrWhiteSpace(title) ? null : DrawingInfo.Parse(title);
}
return info?.ToString();
}
private void ValidateContext(ExportContext context)
{
if (context.ActiveDocument == null)
throw new ArgumentException("ActiveDocument cannot be null.", nameof(context));
if (context.ProgressCallback == null)
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)
{
context.ProgressCallback?.Invoke(message, level, file);
}
#endregion
}
}
+387
View File
@@ -0,0 +1,387 @@
using ExportDXF.Extensions;
using ExportDXF.Models;
using ExportDXF.Utilities;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.IO;
namespace ExportDXF.Services
{
/// <summary>
/// Service for exporting parts to DXF format.
/// </summary>
public interface IPartExporter
{
/// <summary>
/// Exports a single part document to DXF.
/// Returns an Item with export metadata (filename, hash, sheet metal properties), or null if export failed.
/// </summary>
/// <param name="part">The part document to export.</param>
/// <param name="saveDirectory">The temp directory where the DXF file will be saved.</param>
/// <param name="context">The export context.</param>
Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context);
/// <summary>
/// Exports an item (component from BOM or assembly) to DXF.
/// </summary>
/// <param name="item">The item to export.</param>
/// <param name="saveDirectory">The temp directory where the DXF file will be saved.</param>
/// <param name="context">The export context.</param>
void ExportItem(Item item, string saveDirectory, ExportContext context);
}
public class PartExporter : IPartExporter
{
public PartExporter()
{
}
public Item ExportSinglePart(PartDoc part, string saveDirectory, ExportContext context)
{
if (part == null)
throw new ArgumentNullException(nameof(part));
if (string.IsNullOrWhiteSpace(saveDirectory))
throw new ArgumentException("Save directory cannot be null or empty.", nameof(saveDirectory));
if (context == null)
throw new ArgumentNullException(nameof(context));
var model = part as ModelDoc2;
var activeConfig = model.GetActiveConfiguration() as SolidWorks.Interop.sldworks.Configuration;
var originalConfigName = activeConfig?.Name;
try
{
var fileName = GetSinglePartFileName(model, context.Equipment);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
// Build result item with metadata
var item = new Item
{
PartName = model.GetTitle()?.Replace(".SLDPRT", "") ?? "",
Configuration = originalConfigName ?? "",
Quantity = 1
};
// Enrich with sheet metal properties and description
var sheetMetalProps = SolidWorksHelper.GetSheetMetalProperties(model);
if (sheetMetalProps != null)
{
item.Thickness = sheetMetalProps.Thickness;
item.KFactor = sheetMetalProps.KFactor;
item.BendRadius = sheetMetalProps.BendRadius;
}
// Get description from custom properties
var configPropMgr = model.Extension.CustomPropertyManager[originalConfigName];
item.Description = configPropMgr?.Get("Description");
if (string.IsNullOrEmpty(item.Description))
{
var docPropMgr = model.Extension.CustomPropertyManager[""];
item.Description = docPropMgr?.Get("Description");
}
item.Description = TextHelper.RemoveXmlTags(item.Description);
// Get material
item.Material = part.GetMaterialPropertyName2(originalConfigName, out _);
context.GetOrCreateTemplateDrawing();
if (ExportPartToDxf(part, originalConfigName, savePath, context))
{
item.FileName = Path.GetFileNameWithoutExtension(savePath);
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
item.LocalTempPath = savePath;
return item;
}
else
{
return null;
}
}
finally
{
if (originalConfigName != null)
{
model.ShowConfiguration(originalConfigName);
}
}
}
public void ExportItem(Item item, string saveDirectory, ExportContext context)
{
if (string.IsNullOrWhiteSpace(saveDirectory))
throw new ArgumentException("Save directory cannot be null or empty.", nameof(saveDirectory));
if (context == null)
throw new ArgumentNullException(nameof(context));
if (item?.Component == null)
{
context.ProgressCallback?.Invoke("Skipped, no component", LogLevel.Warning, $"Item {item?.ItemNo}");
return;
}
context.CancellationToken.ThrowIfCancellationRequested();
item.Component.SetLightweightToResolved();
var model = item.Component.GetModelDoc2() as ModelDoc2;
var part = model as PartDoc;
if (part == null)
{
context.ProgressCallback?.Invoke("Skipped, not a part document", LogLevel.Info, item.PartName);
return;
}
EnrichItemWithMetadata(item, model, part);
var fileName = GetItemFileName(item, context);
var savePath = Path.Combine(saveDirectory, fileName + ".dxf");
var templateDrawing = context.GetOrCreateTemplateDrawing();
if (ExportPartToDxf(part, item.Component.ReferencedConfiguration, savePath, context))
{
item.FileName = Path.GetFileNameWithoutExtension(savePath);
item.ContentHash = Utilities.ContentHasher.ComputeDxfContentHash(savePath);
item.LocalTempPath = savePath;
}
else
{
LogExportFailure(item, context);
}
}
private void EnrichItemWithMetadata(Item item, ModelDoc2 model, PartDoc part)
{
// Get sheet metal properties
var sheetMetalProps = SolidWorksHelper.GetSheetMetalProperties(model);
if (sheetMetalProps != null)
{
item.Thickness = sheetMetalProps.Thickness;
item.KFactor = sheetMetalProps.KFactor;
item.BendRadius = sheetMetalProps.BendRadius;
}
// Get description from custom properties
var config = item.Component.ReferencedConfiguration;
// Try configuration-specific properties first
var configPropertyManager = model.Extension.CustomPropertyManager[config];
item.Description = configPropertyManager?.Get("Description");
// Fall back to document-level properties if no config-specific description
if (string.IsNullOrEmpty(item.Description))
{
var docPropertyManager = model.Extension.CustomPropertyManager[""];
item.Description = docPropertyManager?.Get("Description");
}
item.Description = TextHelper.RemoveXmlTags(item.Description);
// Get material
item.Material = part.GetMaterialPropertyName2(config, out _);
}
private bool ExportPartToDxf(
PartDoc part,
string configName,
string savePath,
ExportContext context)
{
try
{
var model = part as ModelDoc2;
var partTitle = model.GetTitle();
if (!model.IsSheetMetal())
{
context.ProgressCallback?.Invoke("Skipped, not sheet metal", LogLevel.Info, partTitle);
return false;
}
var templateDrawing = context.GetOrCreateTemplateDrawing();
SolidWorksHelper.ConfigureFlatPatternSettings(model);
var sheet = templateDrawing.IGetCurrentSheet();
var modelName = Path.GetFileNameWithoutExtension(model.GetPathName());
sheet.SetName(modelName);
context.ProgressCallback?.Invoke("Creating flat pattern", LogLevel.Info, partTitle);
var view = CreateFlatPatternView(templateDrawing, model, configName);
if (view == null)
{
context.ProgressCallback?.Invoke("Failed to create flat pattern", LogLevel.Error, partTitle);
return false;
}
ConfigureFlatPatternView(view, templateDrawing, model, configName, context);
if (context.ViewFlipDecider?.ShouldFlip(view) == true)
{
context.ProgressCallback?.Invoke("Flipped view", LogLevel.Info, partTitle);
view.FlipView = true;
}
var drawingModel = templateDrawing as ModelDoc2;
drawingModel.SaveAs(savePath);
AddEtchLines(savePath, context);
context.ProgressCallback?.Invoke($"Saved to \"{savePath}\"", LogLevel.Info, partTitle);
DeleteView(drawingModel, view);
return true;
}
catch (Exception ex)
{
context.ProgressCallback?.Invoke($"Export failed: {ex.Message}", LogLevel.Error, null);
return false;
}
}
private SolidWorks.Interop.sldworks.View CreateFlatPatternView(
DrawingDoc drawing,
ModelDoc2 part,
string configName)
{
return drawing.CreateFlatPatternViewFromModelView3(
part.GetPathName(),
configName,
0, 0, 0,
false,
false);
}
private void ConfigureFlatPatternView(
SolidWorks.Interop.sldworks.View view,
DrawingDoc drawing,
ModelDoc2 partModel,
string configName,
ExportContext context)
{
view.ShowSheetMetalBendNotes = true;
var drawingModel = drawing as ModelDoc2;
drawingModel.ViewZoomtofit2();
var flatPatternModel = ViewHelper.GetModelFromView(view);
SolidWorksHelper.SetFlatPatternSuppressionState(
flatPatternModel,
swComponentSuppressionState_e.swComponentFullyResolved);
if (ViewHelper.HasSupressedBends(view))
{
var title = partModel.GetTitle();
context.ProgressCallback?.Invoke("A bend is suppressed, please check flat pattern", LogLevel.Error, title);
}
if (ViewHelper.HideModelSketches(view))
{
// Recreate view without sketches
DeleteView(drawingModel, view);
view = CreateFlatPatternView(drawing, partModel, configName);
view.ShowSheetMetalBendNotes = true;
}
}
private void DeleteView(ModelDoc2 drawing, SolidWorks.Interop.sldworks.View view)
{
drawing.SelectByName(0, view.Name);
drawing.DeleteSelection(false);
}
private void AddEtchLines(string dxfPath, ExportContext context)
{
try
{
var etcher = new EtchBendLines.Etcher();
etcher.AddEtchLines(dxfPath);
FixDegreeSymbol(dxfPath);
}
catch (Exception ex)
{
context.ProgressCallback?.Invoke($"Etch lines failed: {ex.Message}", LogLevel.Warning, Path.GetFileName(dxfPath));
}
}
/// <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 config = model.ConfigurationManager.ActiveConfiguration.Name;
var isDefaultConfig = string.Equals(config, "default", StringComparison.OrdinalIgnoreCase);
var name = isDefaultConfig ? title : $"{title} [{config}]";
return string.IsNullOrWhiteSpace(equipment) ? name : $"{equipment} {name}";
}
private string GetItemFileName(Item item, ExportContext context)
{
if (string.IsNullOrWhiteSpace(context.DrawingNo))
{
// 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');
// Expected format: {EquipNo} {DrawingNo} PT{ItemNo}
return string.IsNullOrWhiteSpace(prefix)
? $"PT{num}"
: $"{prefix} PT{num}";
}
private void LogExportFailure(Item item, ExportContext context)
{
var desc = item.Description?.ToLower() ?? string.Empty;
if (desc.Contains("laser"))
{
context.ProgressCallback?.Invoke(
"Export failed but description says it is laser cut",
LogLevel.Error,
item.PartName);
}
else if (desc.Contains("plasma"))
{
context.ProgressCallback?.Invoke(
"Export failed but description says it is plasma cut",
LogLevel.Error,
item.PartName);
}
}
}
}
+481
View File
@@ -0,0 +1,481 @@
using ExportDXF.Models;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Threading.Tasks;
namespace ExportDXF.Services
{
/// <summary>
/// Helper class to get COM objects from Running Object Table (ROT) in .NET Core/5+
/// </summary>
internal static class ComHelper
{
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable rot);
[DllImport("ole32.dll")]
private static extern int CreateBindCtx(int reserved, out IBindCtx bindCtx);
public static object GetActiveObject(string progId)
{
IRunningObjectTable rot = null;
IEnumMoniker enumMoniker = null;
IBindCtx bindCtx = null;
try
{
if (GetRunningObjectTable(0, out rot) != 0 || rot == null)
return null;
rot.EnumRunning(out enumMoniker);
if (enumMoniker == null)
return null;
if (CreateBindCtx(0, out bindCtx) != 0 || bindCtx == null)
return null;
IMoniker[] monikers = new IMoniker[1];
IntPtr fetched = IntPtr.Zero;
while (enumMoniker.Next(1, monikers, fetched) == 0)
{
monikers[0].GetDisplayName(bindCtx, null, out string displayName);
if (displayName != null && displayName.IndexOf(progId, StringComparison.OrdinalIgnoreCase) >= 0)
{
rot.GetObject(monikers[0], out object obj);
return obj;
}
}
return null;
}
catch
{
return null;
}
finally
{
if (enumMoniker != null) Marshal.ReleaseComObject(enumMoniker);
if (rot != null) Marshal.ReleaseComObject(rot);
if (bindCtx != null) Marshal.ReleaseComObject(bindCtx);
}
}
}
/// <summary>
/// Service for managing SolidWorks application connection and document lifecycle.
/// </summary>
public interface ISolidWorksService : IDisposable
{
/// <summary>
/// Connects to the SolidWorks application asynchronously.
/// </summary>
Task ConnectAsync();
/// <summary>
/// Gets the currently active SolidWorks document.
/// </summary>
/// <returns>The active document or null if no document is open.</returns>
SolidWorksDocument GetActiveDocument();
/// <summary>
/// Enables or disables user control of the SolidWorks application.
/// </summary>
/// <param name="enable">True to enable user control, false to disable.</param>
void EnableUserControl(bool enable);
/// <summary>
/// Sets whether a command is in progress. When true, user input to
/// SolidWorks is disabled and interactive dialogs are suppressed.
/// </summary>
/// <param name="inProgress">True to block user input, false to re-enable.</param>
void SetCommandInProgress(bool inProgress);
/// <summary>
/// Gets the SolidWorks application instance.
/// </summary>
/// <returns>The SldWorks instance.</returns>
SldWorks GetNativeSldWorks();
/// <summary>
/// Closes the document with the specified title.
/// </summary>
/// <param name="documentTitle"></param>
/// <returns></returns>
bool CloseDocument(string documentTitle);
/// <summary>
/// Event raised when the active document changes.
/// </summary>
event EventHandler ActiveDocumentChanged;
}
/// <summary>
/// Service for managing SolidWorks application connection and document lifecycle.
/// </summary>
public class SolidWorksService : ISolidWorksService
{
private SldWorks _sldWorks;
private bool _disposed;
/// <summary>
/// Event raised when the active document changes in SolidWorks.
/// </summary>
public event EventHandler ActiveDocumentChanged;
/// <summary>
/// Connects to the SolidWorks application asynchronously.
/// </summary>
/// <exception cref="InvalidOperationException">Thrown when connection to SolidWorks fails.</exception>
public async Task ConnectAsync()
{
await Task.Run(() =>
{
try
{
// Try to get running instance first
_sldWorks = GetRunningInstance();
// If no running instance, create new one
if (_sldWorks == null)
{
_sldWorks = CreateNewInstance();
}
if (_sldWorks == null)
{
throw new InvalidOperationException(
"Failed to connect to SolidWorks. Please ensure SolidWorks is installed.");
}
// Make SolidWorks visible
_sldWorks.Visible = true;
// Subscribe to document change events
_sldWorks.ActiveModelDocChangeNotify += OnSolidWorksActiveDocChanged;
}
catch (COMException ex)
{
throw new InvalidOperationException(
$"COM error while connecting to SolidWorks: {ex.Message}", ex);
}
});
}
/// <summary>
/// Gets the currently active SolidWorks document.
/// </summary>
/// <returns>The active document wrapper, or null if no document is open.</returns>
public SolidWorksDocument GetActiveDocument()
{
if (_sldWorks == null)
return null;
var model = _sldWorks.ActiveDoc as ModelDoc2;
if (model == null)
return null;
return CreateDocumentWrapper(model);
}
/// <summary>
/// Enables or disables user control of the SolidWorks application.
/// When disabled, SolidWorks won't show dialogs or allow user interaction.
/// </summary>
/// <param name="enable">True to enable user control, false to disable.</param>
public void EnableUserControl(bool enable)
{
if (_sldWorks != null)
{
_sldWorks.UserControl = enable;
}
}
/// <inheritdoc />
public void SetCommandInProgress(bool inProgress)
{
if (_sldWorks != null)
{
_sldWorks.CommandInProgress = inProgress;
}
}
/// <summary>
/// Gets the native SolidWorks application instance.
/// Use this when you need direct access to the SolidWorks API.
/// </summary>
/// <returns>The SldWorks instance, or null if not connected.</returns>
public SldWorks GetNativeSldWorks()
{
return _sldWorks;
}
/// <summary>
/// Checks if SolidWorks is connected and ready.
/// </summary>
public bool IsConnected => _sldWorks != null;
/// <summary>
/// Gets the version of the connected SolidWorks instance.
/// </summary>
public string GetSolidWorksVersion()
{
if (_sldWorks == null)
return null;
try
{
return _sldWorks.RevisionNumber();
}
catch
{
return "Unknown";
}
}
/// <summary>
/// Closes a document by its title.
/// </summary>
/// <param name="documentTitle">The title of the document to close.</param>
/// <returns>True if successfully closed, false otherwise.</returns>
public bool CloseDocument(string documentTitle)
{
if (_sldWorks == null || string.IsNullOrWhiteSpace(documentTitle))
return false;
try
{
_sldWorks.CloseDoc(documentTitle);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Creates a new document from a template.
/// </summary>
/// <param name="templatePath">Path to the template file.</param>
/// <param name="paperSize">Paper size for drawings.</param>
/// <param name="width">Width dimension.</param>
/// <param name="height">Height dimension.</param>
/// <returns>The created document, or null if creation failed.</returns>
public ModelDoc2 CreateDocument(string templatePath, int paperSize, double width, double height)
{
if (_sldWorks == null)
return null;
try
{
return _sldWorks.NewDocument(templatePath, paperSize, width, height) as ModelDoc2;
}
catch
{
return null;
}
}
/// <summary>
/// Opens an existing document.
/// </summary>
/// <param name="filePath">Full path to the document.</param>
/// <param name="options">Open options.</param>
/// <param name="configuration">Configuration to open (empty for default).</param>
/// <returns>The opened document, or null if open failed.</returns>
public ModelDoc2 OpenDocument(string filePath, int options = 0, string configuration = "")
{
if (_sldWorks == null || string.IsNullOrWhiteSpace(filePath))
return null;
try
{
int errors = 0;
int warnings = 0;
var doc = _sldWorks.OpenDoc6(
filePath,
(int)GetDocumentType(filePath),
options,
configuration,
ref errors,
ref warnings);
return doc as ModelDoc2;
}
catch
{
return null;
}
}
#region Private Methods
private SldWorks GetRunningInstance()
{
try
{
return ComHelper.GetActiveObject("SldWorks.Application") as SldWorks;
}
catch (COMException)
{
// No running instance
return null;
}
}
private SldWorks CreateNewInstance()
{
try
{
var type = Type.GetTypeFromProgID("SldWorks.Application");
if (type == null)
return null;
return Activator.CreateInstance(type) as SldWorks;
}
catch
{
return null;
}
}
private SolidWorksDocument CreateDocumentWrapper(ModelDoc2 model)
{
return new SolidWorksDocument
{
Title = model.GetTitle(),
FilePath = model.GetPathName(),
DocumentType = DetermineDocumentType(model),
NativeDocument = model
};
}
private DocumentType DetermineDocumentType(ModelDoc2 model)
{
if (model is PartDoc) return DocumentType.Part;
if (model is AssemblyDoc) return DocumentType.Assembly;
if (model is DrawingDoc) return DocumentType.Drawing;
return DocumentType.Unknown;
}
private swDocumentTypes_e GetDocumentType(string filePath)
{
var extension = System.IO.Path.GetExtension(filePath)?.ToLowerInvariant();
switch (extension)
{
case ".sldprt":
return swDocumentTypes_e.swDocPART;
case ".sldasm":
return swDocumentTypes_e.swDocASSEMBLY;
case ".slddrw":
return swDocumentTypes_e.swDocDRAWING;
default:
return swDocumentTypes_e.swDocNONE;
}
}
private int OnSolidWorksActiveDocChanged()
{
try
{
ActiveDocumentChanged?.Invoke(this, EventArgs.Empty);
}
catch
{
// Swallow exceptions from event handlers
}
return 1; // SolidWorks expects 1 to be returned
}
#endregion
#region IDisposable Implementation
/// <summary>
/// Releases all resources used by the SolidWorksService.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing">True to release both managed and unmanaged resources;
/// false to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// Unsubscribe from events
if (_sldWorks != null)
{
try
{
_sldWorks.ActiveModelDocChangeNotify -= OnSolidWorksActiveDocChanged;
}
catch
{
// Ignore errors during cleanup
}
}
}
// Release COM object
if (_sldWorks != null)
{
try
{
Marshal.ReleaseComObject(_sldWorks);
}
catch
{
// Ignore errors during cleanup
}
_sldWorks = null;
}
_disposed = true;
}
public SldWorks GetSldWorks()
{
if (_sldWorks == null)
{
_sldWorks = GetRunningInstance();
return _sldWorks;
}
else
{
_sldWorks = CreateNewInstance();
return _sldWorks;
}
}
/// <summary>
/// Finalizer to ensure COM objects are released.
/// </summary>
~SolidWorksService()
{
Dispose(false);
}
#endregion
}
}
Binary file not shown.
-22
View File
@@ -1,22 +0,0 @@
using System;
namespace ExportDXF
{
public static class Units
{
/// <summary>
/// Multiply factor needed to convert the desired units to meters.
/// </summary>
public static double ScaleFactor = 0.0254; // inches to meters
public static double ToSldWorks(this double d)
{
return Math.Round(d * ScaleFactor, 8);
}
public static double FromSldWorks(this double d)
{
return Math.Round(d / ScaleFactor, 8);
}
}
}
+202
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)}";
}
}
}
@@ -0,0 +1,43 @@
namespace ExportDXF.Utilities
{
/// <summary>
/// Contains sheet metal properties extracted from a SolidWorks part.
/// </summary>
public class SheetMetalProperties
{
/// <summary>
/// Material thickness
/// </summary>
public double Thickness { get; set; }
/// <summary>
/// K-factor for bend calculations.
/// </summary>
public double KFactor { get; set; }
/// <summary>
/// Inside bend radius
/// </summary>
public double BendRadius { get; set; }
/// <summary>
/// Bend allowance
/// </summary>
public double BendAllowance { get; set; }
/// <summary>
/// Whether auto relief is enabled.
/// </summary>
public bool AutoRelief { get; set; }
/// <summary>
/// Relief ratio for auto relief.
/// </summary>
public double ReliefRatio { get; set; }
public override string ToString()
{
return $"Thickness: {Thickness:F2}\", K-Factor: {KFactor:F3}, Bend Radius: {BendRadius:F2}\"";
}
}
}
+513
View File
@@ -0,0 +1,513 @@
using ExportDXF.Extensions;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst;
using System;
using System.Collections.Generic;
using System.Linq;
namespace ExportDXF.Utilities
{
/// <summary>
/// Utility class for SolidWorks-specific operations and helper functions.
/// </summary>
public static class SolidWorksHelper
{
#region Feature Names Constants
private const string FLAT_PATTERN_FEATURE = "FlatPattern";
private const string SHEET_METAL_FEATURE = "SheetMetal";
private const string BASE_FLANGE_FEATURE = "BaseFlange";
private const string FLAT_PATTERN_FOLDER = "Flat-Pattern";
#endregion
#region Sheet Metal Operations
/// <summary>
/// Gets sheet metal properties from the model.
/// </summary>
/// <param name="model">The model to extract properties from.</param>
/// <returns>Sheet metal properties, or null if not a sheet metal part.</returns>
public static SheetMetalProperties GetSheetMetalProperties(ModelDoc2 model)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var sheetMetalFeature = model.GetFeatureByTypeName(SHEET_METAL_FEATURE);
if (sheetMetalFeature == null)
return null;
var sheetMetalData = sheetMetalFeature.GetDefinition() as SheetMetalFeatureData;
if (sheetMetalData == null)
return null;
return new SheetMetalProperties
{
Thickness = sheetMetalData.Thickness.FromSolidWorksToInches(),
KFactor = sheetMetalData.KFactor,
BendRadius = sheetMetalData.BendRadius.FromSolidWorksToInches(),
BendAllowance = sheetMetalData.BendAllowance.FromSolidWorksToInches(),
AutoRelief = sheetMetalData.UseAutoRelief,
ReliefRatio = sheetMetalData.ReliefRatio
};
}
/// <summary>
/// Configures flat pattern settings for optimal DXF export.
/// Unchecks corner treatment and enables simplify bends.
/// </summary>
/// <param name="model">The model containing the flat pattern.</param>
/// <returns>True if settings were successfully modified.</returns>
public static bool ConfigureFlatPatternSettings(ModelDoc2 model)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var flatPattern = model.GetFeatureByTypeName(FLAT_PATTERN_FEATURE);
if (flatPattern == null)
return false;
var featureData = flatPattern.GetDefinition() as FlatPatternFeatureData;
if (featureData == null)
return false;
try
{
// Configure for cleaner DXF output
featureData.CornerTreatment = false; // Remove corner treatments
featureData.SimplifyBends = true; // Simplify bend representations
return flatPattern.ModifyDefinition(featureData, model, null);
}
catch
{
return false;
}
}
/// <summary>
/// Sets the suppression state of the flat pattern feature.
/// </summary>
/// <param name="model">The model containing the flat pattern.</param>
/// <param name="suppressionState">The desired suppression state.</param>
/// <returns>True if the flat pattern is suppressed after the operation.</returns>
public static bool SetFlatPatternSuppressionState(
ModelDoc2 model,
swComponentSuppressionState_e suppressionState)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
var flatPattern = model.GetFeatureByTypeName(FLAT_PATTERN_FEATURE);
if (flatPattern == null)
return false;
try
{
flatPattern.SetSuppression((int)suppressionState);
return flatPattern.IsSuppressed();
}
catch
{
return false;
}
}
/// <summary>
/// Gets the flat pattern feature from a model.
/// </summary>
/// <param name="model">The model to search.</param>
/// <returns>The flat pattern feature, or null if not found.</returns>
public static Feature GetFlatPatternFeature(ModelDoc2 model)
{
return model?.GetFeatureByTypeName(FLAT_PATTERN_FEATURE);
}
/// <summary>
/// Checks if a model has a flat pattern feature.
/// </summary>
/// <param name="model">The model to check.</param>
/// <returns>True if the model has a flat pattern feature.</returns>
public static bool HasFlatPattern(ModelDoc2 model)
{
return GetFlatPatternFeature(model) != null;
}
#endregion
#region Feature Operations
/// <summary>
/// Gets all features of a specific type from a model.
/// </summary>
/// <param name="model">The model to search.</param>
/// <param name="featureTypeName">The name of the feature type to find.</param>
/// <returns>A list of features of the specified type.</returns>
public static List<Feature> GetFeaturesByTypeName(ModelDoc2 model, string featureTypeName)
{
if (model == null || string.IsNullOrEmpty(featureTypeName))
return new List<Feature>();
var features = new List<Feature>();
var feature = model.FirstFeature() as Feature;
while (feature != null)
{
if (string.Equals(feature.GetTypeName2(), featureTypeName, StringComparison.OrdinalIgnoreCase))
{
features.Add(feature);
}
// Check sub-features
var subFeature = feature.GetFirstSubFeature() as Feature;
while (subFeature != null)
{
if (string.Equals(subFeature.GetTypeName2(), featureTypeName, StringComparison.OrdinalIgnoreCase))
{
features.Add(subFeature);
}
subFeature = subFeature.GetNextSubFeature() as Feature;
}
feature = feature.GetNextFeature() as Feature;
}
return features;
}
/// <summary>
/// Suppresses or unsuppresses a feature by name.
/// </summary>
/// <param name="model">The model containing the feature.</param>
/// <param name="featureName">The name of the feature to suppress/unsuppress.</param>
/// <param name="suppress">True to suppress, false to unsuppress.</param>
/// <returns>True if the operation was successful.</returns>
public static bool SetFeatureSuppressionByName(ModelDoc2 model, string featureName, bool suppress)
{
if (model == null || string.IsNullOrEmpty(featureName))
return false;
try
{
// Use Extension.SelectByID2 to find and select the feature
var modelExtension = model.Extension;
bool selected = modelExtension.SelectByID2(featureName, "BODYFEATURE", 0, 0, 0, false, 0, null, 0);
if (!selected)
return false;
var selectionManager = model.SelectionManager as SelectionMgr;
var feature = selectionManager.GetSelectedObject6(1, -1) as Feature;
if (feature == null)
return false;
var suppressionState = suppress
? swFeatureSuppressionAction_e.swSuppressFeature
: swFeatureSuppressionAction_e.swUnSuppressFeature;
feature.SetSuppression((int)suppressionState);
// Clear the selection
model.ClearSelection2(true);
return true;
}
catch
{
return false;
}
}
#endregion
#region Configuration Operations
/// <summary>
/// Gets all configuration names from a model.
/// </summary>
/// <param name="model">The model to query.</param>
/// <returns>A list of configuration names.</returns>
public static List<string> GetConfigurationNames(ModelDoc2 model)
{
if (model == null)
return new List<string>();
var configNames = (string[])model.GetConfigurationNames();
return configNames?.ToList() ?? new List<string>();
}
/// <summary>
/// Activates a specific configuration.
/// </summary>
/// <param name="model">The model to modify.</param>
/// <param name="configurationName">The name of the configuration to activate.</param>
/// <returns>True if the configuration was successfully activated.</returns>
public static bool ActivateConfiguration(ModelDoc2 model, string configurationName)
{
if (model == null || string.IsNullOrEmpty(configurationName))
return false;
try
{
return model.ShowConfiguration2(configurationName);
}
catch
{
return false;
}
}
/// <summary>
/// Gets the active configuration name.
/// </summary>
/// <param name="model">The model to query.</param>
/// <returns>The name of the active configuration, or null if unavailable.</returns>
public static string GetActiveConfigurationName(ModelDoc2 model)
{
if (model == null)
return null;
try
{
var config = model.GetActiveConfiguration() as Configuration;
return config?.Name;
}
catch
{
return null;
}
}
#endregion
#region Custom Properties
/// <summary>
/// Gets a custom property value from a model.
/// </summary>
/// <param name="model">The model to query.</param>
/// <param name="propertyName">The name of the property.</param>
/// <param name="configurationName">The configuration name (empty string for file-level properties).</param>
/// <returns>The property value, or null if not found.</returns>
public static string GetCustomProperty(ModelDoc2 model, string propertyName, string configurationName = "")
{
if (model == null || string.IsNullOrEmpty(propertyName))
return null;
try
{
var customPropertyManager = model.Extension.CustomPropertyManager[configurationName ?? ""];
return customPropertyManager.Get(propertyName);
}
catch
{
return null;
}
}
/// <summary>
/// Sets a custom property value on a model.
/// </summary>
/// <param name="model">The model to modify.</param>
/// <param name="propertyName">The name of the property.</param>
/// <param name="propertyValue">The value to set.</param>
/// <param name="configurationName">The configuration name (empty string for file-level properties).</param>
/// <returns>True if the property was successfully set.</returns>
public static bool SetCustomProperty(ModelDoc2 model, string propertyName, string propertyValue, string configurationName = "")
{
if (model == null || string.IsNullOrEmpty(propertyName))
return false;
try
{
var customPropertyManager = model.Extension.CustomPropertyManager[configurationName ?? ""];
var result = customPropertyManager.Add3(propertyName, (int)swCustomInfoType_e.swCustomInfoText, propertyValue, (int)swCustomPropertyAddOption_e.swCustomPropertyReplaceValue);
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Gets all custom property names from a model.
/// </summary>
/// <param name="model">The model to query.</param>
/// <param name="configurationName">The configuration name (empty string for file-level properties).</param>
/// <returns>A list of custom property names.</returns>
public static List<string> GetCustomPropertyNames(ModelDoc2 model, string configurationName = "")
{
if (model == null)
return new List<string>();
try
{
var customPropertyManager = model.Extension.CustomPropertyManager[configurationName ?? ""];
var propertyNames = (string[])customPropertyManager.GetNames();
return propertyNames?.ToList() ?? new List<string>();
}
catch
{
return new List<string>();
}
}
#endregion
#region Material Operations
/// <summary>
/// Gets the material name for a part document.
/// </summary>
/// <param name="part">The part document.</param>
/// <param name="configurationName">The configuration name.</param>
/// <returns>The material name, or null if not assigned.</returns>
public static string GetMaterial(PartDoc part, string configurationName)
{
if (part == null)
return null;
try
{
return part.GetMaterialPropertyName2(configurationName, out _);
}
catch
{
return null;
}
}
/// <summary>
/// Sets the material for a part document.
/// </summary>
/// <param name="part">The part document.</param>
/// <param name="materialName">The name of the material to assign.</param>
/// <param name="databasePath">The path to the material database.</param>
/// <returns>True if the material was successfully assigned.</returns>
public static bool SetMaterial(PartDoc part, string materialName, string databasePath)
{
if (part == null || string.IsNullOrEmpty(materialName))
return false;
try
{
part.SetMaterialPropertyName2("", databasePath, materialName);
return true;
}
catch
{
return false;
}
}
#endregion
#region Document Operations
/// <summary>
/// Gets the file path of a model document.
/// </summary>
/// <param name="model">The model document.</param>
/// <returns>The full file path, or empty string if not saved.</returns>
public static string GetFilePath(ModelDoc2 model)
{
return model?.GetPathName() ?? string.Empty;
}
/// <summary>
/// Gets the document title (filename without path).
/// </summary>
/// <param name="model">The model document.</param>
/// <returns>The document title.</returns>
public static string GetTitle(ModelDoc2 model)
{
return model?.GetTitle() ?? string.Empty;
}
/// <summary>
/// Checks if a document has been saved.
/// </summary>
/// <param name="model">The model document.</param>
/// <returns>True if the document has been saved to disk.</returns>
public static bool IsSaved(ModelDoc2 model)
{
return !string.IsNullOrEmpty(GetFilePath(model));
}
/// <summary>
/// Saves a document.
/// </summary>
/// <param name="model">The model document to save.</param>
/// <returns>True if the save operation was successful.</returns>
public static bool Save(ModelDoc2 model)
{
if (model == null)
return false;
try
{
int errors = 0;
int warnings = 0;
return model.Save3((int)swSaveAsOptions_e.swSaveAsOptions_Silent, ref errors, ref warnings);
}
catch
{
return false;
}
}
#endregion
#region Component Operations
/// <summary>
/// Resolves a lightweight component to fully loaded.
/// </summary>
/// <param name="component">The component to resolve.</param>
/// <returns>True if the component was successfully resolved.</returns>
public static bool ResolveComponent(Component2 component)
{
if (component == null)
return false;
try
{
component.SetLightweightToResolved();
return true;
}
catch
{
return false;
}
}
/// <summary>
/// Checks if a component is suppressed.
/// </summary>
/// <param name="component">The component to check.</param>
/// <returns>True if the component is suppressed.</returns>
public static bool IsComponentSuppressed(Component2 component)
{
return component?.IsSuppressed() ?? false;
}
/// <summary>
/// Gets the model document from a component.
/// </summary>
/// <param name="component">The component.</param>
/// <returns>The model document, or null if unavailable.</returns>
public static ModelDoc2 GetModelFromComponent(Component2 component)
{
return component?.GetModelDoc2() as ModelDoc2;
}
#endregion
}
}
+274
View File
@@ -0,0 +1,274 @@
using System;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace ExportDXF.Utilities
{
/// <summary>
/// Utility class for text processing and string manipulation operations.
/// </summary>
public static class TextHelper
{
private static readonly Regex XmlTagRegex = new Regex(@"<[^>]+>", RegexOptions.Compiled);
private static readonly Regex WhitespaceRegex = new Regex(@"\s+", RegexOptions.Compiled);
private static readonly Regex FontTagRegex = new Regex(@"<FONT.*?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// Removes all XML tags from the input string.
/// </summary>
/// <param name="input">The string containing XML tags to remove.</param>
/// <returns>The string with all XML tags removed, or the original input if null/empty.</returns>
public static string RemoveXmlTags(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return XmlTagRegex.Replace(input, string.Empty);
}
/// <summary>
/// Removes specific SolidWorks font XML tags from the input string.
/// This is more targeted than RemoveXmlTags and handles SolidWorks-specific formatting.
/// </summary>
/// <param name="input">The string containing font tags to remove.</param>
/// <returns>The string with font tags removed.</returns>
public static string RemoveFontXmlTags(string input)
{
if (string.IsNullOrEmpty(input))
return input;
var result = input;
var matches = FontTagRegex.Matches(result);
// Process matches in reverse order to maintain indices
for (int i = matches.Count - 1; i >= 0; i--)
{
var match = matches[i];
result = result.Remove(match.Index, match.Length);
}
return result;
}
/// <summary>
/// Normalizes whitespace in a string by replacing multiple consecutive whitespace characters with a single space.
/// </summary>
/// <param name="input">The string to normalize.</param>
/// <returns>The string with normalized whitespace.</returns>
public static string NormalizeWhitespace(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return WhitespaceRegex.Replace(input.Trim(), " ");
}
/// <summary>
/// Cleans text by removing XML tags and normalizing whitespace.
/// This is a common operation for processing text from SolidWorks.
/// </summary>
/// <param name="input">The text to clean.</param>
/// <returns>Cleaned text with XML tags removed and whitespace normalized.</returns>
public static string CleanText(string input)
{
if (string.IsNullOrEmpty(input))
return input;
var cleaned = RemoveXmlTags(input);
return NormalizeWhitespace(cleaned);
}
/// <summary>
/// Returns a number with its ordinal suffix (1st, 2nd, 3rd, 4th, etc.).
/// </summary>
/// <param name="number">The number to format.</param>
/// <returns>The number with appropriate ordinal suffix.</returns>
public static string GetOrdinalSuffix(int number)
{
if (number <= 0)
return number.ToString();
// Special cases for 11th, 12th, 13th
if (number >= 11 && number <= 13)
return number + "th";
return number + GetSuffix(number % 10);
}
/// <summary>
/// Converts a string to title case (first letter of each word capitalized).
/// </summary>
/// <param name="input">The string to convert.</param>
/// <returns>The string in title case.</returns>
public static string ToTitleCase(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(input.ToLowerInvariant());
}
/// <summary>
/// Truncates a string to the specified maximum length, optionally adding an ellipsis.
/// </summary>
/// <param name="input">The string to truncate.</param>
/// <param name="maxLength">The maximum length of the result.</param>
/// <param name="useEllipsis">Whether to add "..." when truncating.</param>
/// <returns>The truncated string.</returns>
public static string Truncate(string input, int maxLength, bool useEllipsis = true)
{
if (string.IsNullOrEmpty(input))
return input;
if (input.Length <= maxLength)
return input;
if (useEllipsis && maxLength > 3)
{
return input.Substring(0, maxLength - 3) + "...";
}
return input.Substring(0, maxLength);
}
/// <summary>
/// Removes invalid filename characters from a string, replacing them with underscores.
/// </summary>
/// <param name="filename">The filename to sanitize.</param>
/// <returns>A safe filename with invalid characters replaced.</returns>
public static string SanitizeFileName(string filename)
{
if (string.IsNullOrEmpty(filename))
return filename;
var invalidChars = System.IO.Path.GetInvalidFileNameChars();
var sb = new StringBuilder(filename);
foreach (var invalidChar in invalidChars)
{
sb.Replace(invalidChar, '_');
}
// Also replace some additional problematic characters
sb.Replace(' ', '_'); // Spaces can be problematic
sb.Replace('"', '\''); // Double quotes to single quotes
return sb.ToString();
}
/// <summary>
/// Checks if a string is null, empty, or contains only whitespace.
/// </summary>
/// <param name="input">The string to check.</param>
/// <returns>True if the string is null, empty, or whitespace only.</returns>
public static bool IsNullOrWhiteSpace(string input)
{
return string.IsNullOrWhiteSpace(input);
}
/// <summary>
/// Safely gets a substring without throwing exceptions for invalid indices.
/// </summary>
/// <param name="input">The source string.</param>
/// <param name="startIndex">The starting index.</param>
/// <param name="length">The length of the substring.</param>
/// <returns>The substring, or empty string if indices are invalid.</returns>
public static string SafeSubstring(string input, int startIndex, int length)
{
if (string.IsNullOrEmpty(input) || startIndex < 0 || startIndex >= input.Length)
return string.Empty;
var actualLength = Math.Min(length, input.Length - startIndex);
return actualLength <= 0 ? string.Empty : input.Substring(startIndex, actualLength);
}
/// <summary>
/// Safely gets a substring from the start index to the end of the string.
/// </summary>
/// <param name="input">The source string.</param>
/// <param name="startIndex">The starting index.</param>
/// <returns>The substring from start index to end, or empty if invalid.</returns>
public static string SafeSubstring(string input, int startIndex)
{
if (string.IsNullOrEmpty(input) || startIndex < 0 || startIndex >= input.Length)
return string.Empty;
return input.Substring(startIndex);
}
/// <summary>
/// Pads a string to a specific length, truncating if too long.
/// </summary>
/// <param name="input">The string to pad or truncate.</param>
/// <param name="totalLength">The desired total length.</param>
/// <param name="paddingChar">The character to use for padding.</param>
/// <param name="padLeft">True to pad on the left, false to pad on the right.</param>
/// <returns>A string of exactly the specified length.</returns>
public static string PadOrTruncate(string input, int totalLength, char paddingChar = ' ', bool padLeft = false)
{
if (string.IsNullOrEmpty(input))
input = string.Empty;
if (input.Length == totalLength)
return input;
if (input.Length > totalLength)
return input.Substring(0, totalLength);
return padLeft
? input.PadLeft(totalLength, paddingChar)
: input.PadRight(totalLength, paddingChar);
}
/// <summary>
/// Converts a string to a safe identifier (letters, numbers, underscores only).
/// </summary>
/// <param name="input">The input string.</param>
/// <returns>A safe identifier string.</returns>
public static string ToSafeIdentifier(string input)
{
if (string.IsNullOrEmpty(input))
return "Identifier";
var sb = new StringBuilder();
foreach (char c in input)
{
if (char.IsLetterOrDigit(c))
{
sb.Append(c);
}
else if (c == ' ' || c == '-' || c == '.')
{
sb.Append('_');
}
}
var result = sb.ToString();
// Ensure it starts with a letter or underscore
if (result.Length > 0 && char.IsDigit(result[0]))
{
result = "_" + result;
}
return string.IsNullOrEmpty(result) ? "Identifier" : result;
}
#region Private Helper Methods
private static string GetSuffix(int lastDigit)
{
switch (lastDigit)
{
case 1: return "st";
case 2: return "nd";
case 3: return "rd";
default: return "th";
}
}
#endregion
}
}
@@ -1,11 +1,12 @@
using SolidWorks.Interop.sldworks; using ExportDXF.Extensions;
using SolidWorks.Interop.sldworks;
using SolidWorks.Interop.swconst; using SolidWorks.Interop.swconst;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace ExportDXF namespace ExportDXF.Utilities
{ {
internal static class ViewHelper internal static class ViewHelper
{ {
@@ -1,4 +1,5 @@
using System.Windows.Forms; using ExportDXF.Utilities;
using System.Windows.Forms;
namespace ExportDXF.ViewFlipDeciders namespace ExportDXF.ViewFlipDeciders
{ {
@@ -1,4 +1,5 @@
using System.Linq; using ExportDXF.Utilities;
using System.Linq;
namespace ExportDXF.ViewFlipDeciders namespace ExportDXF.ViewFlipDeciders
{ {
@@ -1,4 +1,5 @@
using System.Linq; using ExportDXF.Utilities;
using System.Linq;
using System.Windows.Forms; using System.Windows.Forms;
namespace ExportDXF.ViewFlipDeciders namespace ExportDXF.ViewFlipDeciders
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace ExportDXF.ViewFlipDeciders
{
/// <summary>
/// Factory for discovering and creating IViewFlipDecider implementations.
/// </summary>
public static class ViewFlipDeciderFactory
{
private static readonly object _lock = new object();
private static List<IViewFlipDecider> _cachedDeciders;
/// <summary>
/// Gets all available IViewFlipDecider implementations from loaded assemblies.
/// Results are cached for performance.
/// </summary>
/// <returns>An enumerable collection of IViewFlipDecider instances.</returns>
public static IEnumerable<IViewFlipDecider> GetAvailableDeciders()
{
if (_cachedDeciders != null)
return _cachedDeciders;
lock (_lock)
{
if (_cachedDeciders != null)
return _cachedDeciders;
_cachedDeciders = DiscoverDeciders().ToList();
return _cachedDeciders;
}
}
/// <summary>
/// Gets a specific view flip decider by name.
/// </summary>
/// <param name="name">The name of the decider to retrieve.</param>
/// <returns>The matching decider, or null if not found.</returns>
public static IViewFlipDecider GetDeciderByName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return null;
return GetAvailableDeciders()
.FirstOrDefault(d => string.Equals(d.Name, name, StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// Gets the default (Automatic) view flip decider, or the first available if Automatic doesn't exist.
/// </summary>
/// <returns>The default decider.</returns>
public static IViewFlipDecider GetDefaultDecider()
{
var automatic = GetDeciderByName("Automatic");
if (automatic != null)
return automatic;
return GetAvailableDeciders().FirstOrDefault();
}
/// <summary>
/// Clears the cached deciders, forcing rediscovery on next call.
/// Useful for testing or if assemblies are loaded dynamically.
/// </summary>
public static void ClearCache()
{
lock (_lock)
{
_cachedDeciders = null;
}
}
/// <summary>
/// Discovers all IViewFlipDecider implementations in loaded assemblies.
/// </summary>
private static IEnumerable<IViewFlipDecider> DiscoverDeciders()
{
var deciderType = typeof(IViewFlipDecider);
var discoveredTypes = new List<Type>();
try
{
// Search all loaded assemblies
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
try
{
var types = assembly.GetTypes()
.Where(t => deciderType.IsAssignableFrom(t) &&
t.IsClass &&
!t.IsAbstract &&
HasParameterlessConstructor(t));
discoveredTypes.AddRange(types);
}
catch (ReflectionTypeLoadException)
{
// Skip assemblies that can't be loaded
continue;
}
}
}
catch (Exception)
{
// If discovery fails entirely, return empty collection
yield break;
}
// Create instances of discovered types
foreach (var type in discoveredTypes)
{
IViewFlipDecider instance = null;
try
{
instance = (IViewFlipDecider)Activator.CreateInstance(type);
}
catch (Exception)
{
// Skip types that can't be instantiated
continue;
}
if (instance != null)
{
yield return instance;
}
}
}
/// <summary>
/// Checks if a type has a public parameterless constructor.
/// </summary>
private static bool HasParameterlessConstructor(Type type)
{
return type.GetConstructor(
BindingFlags.Public | BindingFlags.Instance,
null,
Type.EmptyTypes,
null) != null;
}
}
}
+1
View File
@@ -5,5 +5,6 @@
</startup> </startup>
<appSettings> <appSettings>
<add key="MaxBendRadius" value="2.0"/> <add key="MaxBendRadius" value="2.0"/>
<add key="FabWorksApiUrl" value="http://localhost:5206"/>
</appSettings> </appSettings>
</configuration> </configuration>
@@ -0,0 +1,9 @@
namespace FabWorks.Api.Configuration
{
public class FileStorageOptions
{
public const string SectionName = "FileStorage";
public string OutputFolder { get; set; } = @"C:\ExportDXF\Output";
}
}
@@ -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
}
};
}
}
@@ -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()
};
}
}
@@ -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; }
}
}
@@ -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);
}
}
}
@@ -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
};
}
}
}
+17
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; }
}
}
+62
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; }
}
}
+11
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; }
}
}
+20
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
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();
@@ -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"
}
}
}
}
+167
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";
}
}
}
@@ -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();
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+15
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"
}
}
+824
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; }
}
+56
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>
+155
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);
}
+50
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);
}
}
+23
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;
}
+393
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>`;
}
}
};
+36
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);
}
}
};
+94
View File
@@ -0,0 +1,94 @@
using FabWorks.Core.Models;
using Microsoft.EntityFrameworkCore;
namespace FabWorks.Core.Data
{
public class FabWorksDbContext : DbContext
{
public DbSet<ExportRecord> ExportRecords { get; set; }
public DbSet<BomItem> BomItems { get; set; }
public DbSet<CutTemplate> CutTemplates { get; set; }
public DbSet<FormProgram> FormPrograms { get; set; }
public DbSet<Drawing> Drawings { get; set; }
public FabWorksDbContext(DbContextOptions<FabWorksDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ExportRecord>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.DrawingNumber).HasMaxLength(100);
entity.Property(e => e.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.OutputFolder).HasMaxLength(500);
entity.Property(e => e.ExportedBy).HasMaxLength(100);
entity.Property(e => e.PdfContentHash).HasMaxLength(64);
entity.HasMany(e => e.BomItems)
.WithOne(b => b.ExportRecord)
.HasForeignKey(b => b.ExportRecordId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Drawing)
.WithMany(d => d.ExportRecords)
.HasForeignKey(e => e.DrawingId)
.OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity<BomItem>(entity =>
{
entity.HasKey(e => e.ID);
entity.Property(e => e.ItemNo).HasMaxLength(50);
entity.Property(e => e.PartNo).HasMaxLength(100);
entity.Property(e => e.Description).HasMaxLength(500);
entity.Property(e => e.PartName).HasMaxLength(200);
entity.Property(e => e.ConfigurationName).HasMaxLength(100);
entity.Property(e => e.Material).HasMaxLength(100);
entity.HasOne(e => e.CutTemplate)
.WithOne(ct => ct.BomItem)
.HasForeignKey<CutTemplate>(ct => ct.BomItemId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.FormProgram)
.WithOne(fp => fp.BomItem)
.HasForeignKey<FormProgram>(fp => fp.BomItemId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<CutTemplate>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.DxfFilePath).HasMaxLength(500);
entity.Property(e => e.CutTemplateName).HasMaxLength(100);
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();
});
}
}
}
+17
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>
@@ -0,0 +1,270 @@
// <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("20260218171742_InitialCreate")]
partial class InitialCreate
{
/// <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<double?>("Thickness")
.HasColumnType("float");
b.HasKey("Id");
b.HasIndex("BomItemId")
.IsUnique();
b.ToTable("CutTemplates");
});
modelBuilder.Entity("FabWorks.Core.Models.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
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.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.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.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}
@@ -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");
}
}
}
@@ -0,0 +1,273 @@
// <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("20260219134027_AddCutTemplateRevision")]
partial class AddCutTemplateRevision
{
/// <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.ExportRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
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.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.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.ExportRecord", b =>
{
b.Navigation("BomItems");
});
#pragma warning restore 612, 618
}
}
}
@@ -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");
}
}
}
@@ -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
}
}
}
@@ -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");
}
}
}
@@ -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
}
}
}
@@ -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;");
}
}
}
@@ -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
}
}
}
@@ -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);
}
}
}
@@ -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
}
}
}
+22
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; }
}
}
+33
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; }
}
}
+14
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>();
}
}
+25
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>();
}
}
+20
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; }
}
}
+171
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);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace FabWorks.Core.PressBrake
{
public enum MatType
{
MildSteel,
HighStrengthSteel,
Stainless,
SoftAluminum,
HardAluminum
}
}
+59
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;
}
}
}
+149
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;
}
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace FabWorks.Core.PressBrake
{
public class SegEntry
{
public double SegValue { get; set; }
}
}
+36
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; }
}
}
+24
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; }
}
}
+32
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>
+53
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
}
}
}

Some files were not shown because too many files have changed in this diff Show More