Compare commits

..

10 Commits

Author SHA1 Message Date
brentperteet
4281fa2b97 Add library git change detection and management system
Features:
- Yellow banner alert on Library tab when changes detected
- Custom diff viewer showing changed symbols/footprints by name
- Categorizes changes: symbols, footprints, and other files
- Color-coded status indicators (added/modified/deleted/untracked)
- Commit and push functionality from UI
- Smart authentication error handling with helpful messages
- Git configuration info display (remote URL, auth type, credential helper)
- Terminal opener for manual git credential setup
- Auto-refresh after commits to update status

Backend:
- check_library_git_status(): Checks for uncommitted/unpushed changes
- get_library_changed_parts(): Parses git status and categorizes by part type
- Socket handlers for git status, changes, commit, and push operations
- Authentication type detection (SSH vs HTTPS)
- Context-specific error messages for auth failures

Frontend:
- Git status banner with view changes button
- Modal with tabular display of changed parts
- Commit message input with commit/push buttons
- Real-time status updates during git operations
- Git config info section with terminal helper button

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-28 12:14:03 -06:00
brentperteet
7a7a9dfdcc Fix switchTab function scope for ES6 module compatibility
- Expose switchTab to window object so onclick handlers can access it
- Fixes "switchTab is not defined" error when using type="module"
- Module-scoped functions are not accessible from inline HTML onclick handlers

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 14:01:35 -06:00
brentperteet
aa7f720194 Fix Three.js module loading for OrbitControls and STLLoader
- Switch from legacy script includes to ES6 modules with import map
- Import OrbitControls and STLLoader from three/addons/
- Update constructor calls to use imported classes directly
- Remove THREE namespace prefix for OrbitControls and STLLoader
- Fixes "THREE.OrbitControls is not a constructor" error

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 13:58:14 -06:00
brentperteet
5966f2b4b8 Add interactive 3D model viewer with STEP to STL conversion
- Install and integrate CadQuery for STEP to STL conversion on server
- Add convert_step_to_stl() function using CadQuery OCP bindings
- Implement Three.js-based interactive 3D viewer with OrbitControls
- Add STLLoader for loading converted models
- Auto-center and scale models to fit viewer
- Add lighting (ambient + 2 directional lights) for better visualization
- Enable mouse controls: rotate (left-click drag), zoom (scroll), pan (right-click drag)
- Add debug logging throughout conversion pipeline
- Display converted STL models in real-time when footprint is selected

Dependencies added: cadquery, cadquery-ocp, zstandard

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 13:54:24 -06:00
brentperteet
fdfaa7e059 Fix 3D model viewer to show when editing existing parts
- Add 3D model request when loading part from database
- Add console logging for debugging model extraction
- Request model for both manual footprint selection and auto-loaded parts

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 13:30:02 -06:00
brentperteet
adffdd211c Add 3D model extraction and download from embedded footprint models
- Parse embedded_files sections in KiCad footprint files
- Extract and decompress (zstd) embedded STEP models
- Add backend endpoint to serve 3D models
- Add UI section to display and download 3D models
- Include Three.js library for future interactive viewing
- Provide download link for extracted STEP files
- Note: Interactive 3D viewing requires STEP to STL/OBJ conversion (future enhancement)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 13:18:34 -06:00
brentperteet
5869d2c693 Enhance library browser with symbol/footprint resolution and datasheet links
- Add library path resolution for symbols and footprints using KiCad library tables
- Parse sym-lib-table and fp-lib-table with multi-line format support
- Resolve environment variables (KICAD9_SYMBOL_DIR, UM_KICAD, etc.)
- Auto-select symbol and footprint when loading part from database
- Add Datasheet column to parts search results with clickable links
- Make IPN, Description, Class, Manufacturer, and MPN fields read-only
- Remove file extensions (.kicad_sym, .pretty) from library dropdown display
- Fix column access in search results using index-based retrieval

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-27 12:44:10 -06:00
brentperteet
0de59dabfc Add complete manufacturing build pipeline with enhanced features
Build Pipeline:
- Generate schematic PDF with all sheets
- Generate 11 individual board layer PDFs (copper, silkscreen, soldermask, paste, fabrication, outline)
- Add "Layer: <name>" text overlay to each board layer PDF
- Merge all board PDFs into single document
- Generate Gerbers with all layers
- Generate drill files in Excellon format
- Generate ODB++ package (optional)
- Export STEP 3D model
- Generate BOMs for active variant
- Package all outputs into timestamped ZIP file
- Real-time progress bar (0-100%) with status updates
- Detailed build log with timestamps
- Auto-download ZIP on completion

Symbol & Footprint Library Integration:
- Browse KiCad symbol libraries from UM_KICAD environment variable
- Live SVG preview of selected symbols with pins, graphics, and labels
- Browse KiCad footprint libraries (.pretty directories)
- Live SVG preview of selected footprints with pads and silkscreen
- Associate symbols and footprints with parts in database
- Store as LibraryName:ComponentName format

WebSocket Connection Improvements:
- Increase ping timeout to 120 seconds (from 60s default)
- Add 25-second ping interval to keep connections alive
- Wait 10 seconds for reconnection before shutdown (handles page refresh)
- Cancel shutdown timer when client reconnects
- Use hidden link download to preserve WebSocket connection (not window.location)

PDF Text Overlay:
- Add reportlab and PyPDF2 imports for PDF manipulation
- Add add_text_overlay_to_pdf() helper function
- Overlay layer names in upper left corner of board PDFs
- Use Helvetica-Bold 14pt font at position (50, 750)

Bug Fixes:
- Fix BOM generator argument order (schematic, project, variant, dnp_uuids, pcb_file)
- Pass empty JSON array '[]' for dnp_uuids instead of output directory
- Move generated BOM files from project dir to output dir for packaging
- Fix datetime import (was missing)
- Use app_args instead of config for getting schematic/board file paths

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 13:01:35 -06:00
brentperteet
bcb2c70e93 Add create new part functionality to library browser
- Add UI to create parts from missing IPNs in spreadsheet
- "+ Add New Part" button switches to create part view
- Auto-populate fields from spreadsheet (description, class, manufacturer, MPN)
- Normalize class field (uppercase, replace special chars with underscores)
- Add optional datasheet URL field
- Exclude section header rows (no manufacturer/MPN)
- Cancel button returns to browser view
- Align logo banner and h1 header to left

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 09:30:19 -06:00
brentperteet
55235f222b Add library browser, BOM generation, and UI improvements
- Add parts library browser with ODBC database search
- Implement hierarchical BOM generation with PCB side detection
- Add STEP export and PCB rendering functionality
- Adjust page width to 80% up to 1536px max
- Add library-only mode for standalone library browsing

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-22 19:26:36 -06:00
3 changed files with 3984 additions and 31 deletions

2206
app.py

File diff suppressed because it is too large Load Diff

445
bom_generator.py Normal file
View File

@@ -0,0 +1,445 @@
"""
BOM Generator for KiCad projects
Generates fitted, unfitted, top-side, and bottom-side BOMs
"""
import os
from collections import defaultdict
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill
import csv
def find_hierarchical_sheets(schematic_file):
"""
Find all hierarchical sheet files referenced in the schematic
Returns list of sheet file paths
"""
sheet_files = []
base_dir = os.path.dirname(schematic_file)
with open(schematic_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
in_sheet = False
for line in lines:
stripped = line.strip()
if stripped.startswith('(sheet'):
in_sheet = True
elif in_sheet and '(property "Sheetfile"' in line:
parts = line.split('"')
if len(parts) >= 4:
sheet_filename = parts[3]
sheet_path = os.path.join(base_dir, sheet_filename)
if os.path.exists(sheet_path):
sheet_files.append(sheet_path)
# Recursively find sheets in the child schematic
child_sheets = find_hierarchical_sheets(sheet_path)
sheet_files.extend(child_sheets)
in_sheet = False
return sheet_files
def parse_pcb_for_component_sides(pcb_file):
"""
Parse PCB file to determine which side each component is on
Returns dict mapping reference -> 'Top' or 'Bottom'
"""
component_sides = {}
if not os.path.exists(pcb_file):
return component_sides
with open(pcb_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
in_footprint = False
current_ref = None
current_layer = None
footprint_indent = None
for i, line in enumerate(lines):
stripped = line.strip()
indent_level = len(line) - len(line.lstrip())
# Detect start of footprint
if stripped.startswith('(footprint'):
in_footprint = True
current_ref = None
current_layer = None
footprint_indent = indent_level
# Detect end of footprint
elif in_footprint and stripped == ')' and indent_level == footprint_indent:
# Store the side mapping
if current_ref and current_layer:
side = 'Top' if current_layer == 'F.Cu' else 'Bottom' if current_layer == 'B.Cu' else '-'
component_sides[current_ref] = side
in_footprint = False
current_ref = None
current_layer = None
# Extract reference
elif in_footprint and '(property "Reference"' in line:
parts = line.split('"')
if len(parts) >= 4:
current_ref = parts[3]
# Extract layer
elif in_footprint and '(layer' in stripped:
parts = line.split('"')
if len(parts) >= 2:
current_layer = parts[1]
return component_sides
def parse_single_schematic(schematic_file, dnp_uuids=None):
"""
Parse a single schematic file for components
"""
if dnp_uuids is None:
dnp_uuids = []
components = []
with open(schematic_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
in_symbol = False
current_component = {}
symbol_start_indent = None
for i, line in enumerate(lines):
stripped = line.strip()
indent_level = len(line) - len(line.lstrip())
# Detect start of symbol
if stripped.startswith('(symbol'):
in_symbol = True
current_component = {
'reference': '',
'value': '',
'footprint': '',
'ipn': '',
'mpn': '',
'manufacturer': '',
'description': '',
'datasheet': '',
'side': '-',
'is_dnp': False,
'uuid': ''
}
symbol_start_indent = indent_level
# Detect end of symbol
elif in_symbol and stripped == ')' and indent_level == symbol_start_indent:
# Only add if we have a reference (filter out power symbols, etc.)
if current_component['reference'] and not current_component['reference'].startswith('#'):
# Check if lib_id contains "power:"
if 'lib_id' in current_component and 'power:' not in current_component.get('lib_id', ''):
components.append(current_component.copy())
in_symbol = False
current_component = {}
# Extract lib_id
elif in_symbol and '(lib_id' in stripped:
parts = line.split('"')
if len(parts) >= 2:
current_component['lib_id'] = parts[1]
# Extract UUID (comes after DNP line)
elif in_symbol and '(dnp' in stripped:
# Look forward for UUID
for j in range(i + 1, min(len(lines), i + 5)):
if '(uuid' in lines[j]:
if '\t(uuid' in lines[j] or ' (uuid' in lines[j]:
uuid_parts = lines[j].split('"')
if len(uuid_parts) >= 2:
current_component['uuid'] = uuid_parts[1]
# Check if DNP
if current_component['uuid'] in dnp_uuids or '(dnp yes)' in stripped or stripped == '(dnp)':
current_component['is_dnp'] = True
break
# Extract properties
elif in_symbol and '(property' in line:
parts = line.split('"')
if len(parts) >= 4:
prop_name = parts[1]
prop_value = parts[3]
if prop_name == 'Reference':
current_component['reference'] = prop_value
elif prop_name == 'Value':
current_component['value'] = prop_value
elif prop_name == 'Footprint':
current_component['footprint'] = prop_value
elif prop_name == 'IPN':
current_component['ipn'] = prop_value
elif prop_name == 'MPN':
current_component['mpn'] = prop_value
elif prop_name == 'Manufacturer':
current_component['manufacturer'] = prop_value
elif prop_name == 'Description':
current_component['description'] = prop_value
elif prop_name == 'Datasheet':
current_component['datasheet'] = prop_value
return components
def parse_schematic_for_bom(schematic_file, dnp_uuids=None, pcb_file=None):
"""
Parse schematic hierarchy and PCB file to extract component information for BOM generation
Traverses all hierarchical sheets and gets side information from PCB
Returns list of components with properties:
- reference: Component reference designator
- value: Component value
- footprint: Footprint name
- ipn: Internal Part Number
- mpn: Manufacturer Part Number
- manufacturer: Manufacturer name
- description: Component description
- side: Top/Bottom/- (from PCB layer)
- is_dnp: Whether component is DNP (Do Not Place)
- uuid: Component UUID
"""
if dnp_uuids is None:
dnp_uuids = []
# Get component side information from PCB
component_sides = {}
if pcb_file and os.path.exists(pcb_file):
print(f"Parsing PCB for component sides: {pcb_file}")
component_sides = parse_pcb_for_component_sides(pcb_file)
# Parse root schematic
print(f"Parsing root schematic: {schematic_file}")
components = parse_single_schematic(schematic_file, dnp_uuids)
# Find and parse hierarchical sheets
sheet_files = find_hierarchical_sheets(schematic_file)
for sheet_file in sheet_files:
print(f"Parsing hierarchical sheet: {sheet_file}")
sheet_components = parse_single_schematic(sheet_file, dnp_uuids)
components.extend(sheet_components)
# Apply side information from PCB
for comp in components:
if comp['reference'] in component_sides:
comp['side'] = component_sides[comp['reference']]
print(f"Total components found: {len(components)}")
return components
def group_components_for_bom(components):
"""
Group components by IPN and side for BOM generation
Returns list of grouped components with consolidated references
"""
# Group by (ipn, side, value, footprint, manufacturer, mpn)
groups = defaultdict(list)
for comp in components:
# Create grouping key
key = (
comp.get('ipn', ''),
comp.get('side', '-'),
comp.get('value', ''),
comp.get('footprint', ''),
comp.get('manufacturer', ''),
comp.get('mpn', ''),
comp.get('description', '')
)
groups[key].append(comp)
# Create BOM lines
bom_lines = []
line_num = 1
for key, comps in sorted(groups.items()):
ipn, side, value, footprint, manufacturer, mpn, description = key
# Sort references naturally (R1, R2, R10, etc.)
refs = sorted([c['reference'] for c in comps], key=lambda x: (x.rstrip('0123456789'), int(''.join(filter(str.isdigit, x)) or 0)))
refdes_str = ', '.join(refs)
# Determine SMD/TH from footprint
smd_th = 'SMD' if 'SMD' in footprint.upper() or any(x in footprint for x in ['0402', '0603', '0805', '1206']) else 'TH'
bom_line = {
'line_num': line_num,
'refdes': refdes_str,
'ipn': ipn if ipn else 'N/A',
'qty': len(comps),
'description': description,
'footprint': footprint,
'manufacturer': manufacturer,
'mpn': mpn,
'notes': '',
'smd_th': smd_th,
'side': side,
'populated': True # Since we're only including fitted parts
}
bom_lines.append(bom_line)
line_num += 1
return bom_lines
def generate_main_bom_xlsx(bom_lines, output_file, project_name, variant_name):
"""
Generate main BOM in XLSX format matching the reference format
"""
wb = Workbook()
ws = wb.active
ws.title = "BOM"
# Set column widths
ws.column_dimensions['A'].width = 5
ws.column_dimensions['B'].width = 60
ws.column_dimensions['C'].width = 12
ws.column_dimensions['D'].width = 6
ws.column_dimensions['E'].width = 40
ws.column_dimensions['F'].width = 25
ws.column_dimensions['G'].width = 15
ws.column_dimensions['H'].width = 20
ws.column_dimensions['I'].width = 15
ws.column_dimensions['J'].width = 8
ws.column_dimensions['K'].width = 8
ws.column_dimensions['L'].width = 10
# Add header row (row 2)
headers = ['#', 'RefDes', 'GLE P/N', 'Qty', 'Description', 'Footprint',
'Mfg 1', 'Mfg 1 P/N', 'Notes', 'SMD/TH', 'Side', 'Populated',
'Mfg 2', 'Mfg 2 P/N', 'Mfg 3', 'Mfg 3 P/N', 'Mfg 4', 'Mfg 4 P/N',
'Mfg 5', 'Mfg 5 P/N']
for col_idx, header in enumerate(headers, 1):
cell = ws.cell(row=2, column=col_idx)
cell.value = header
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color='D3D3D3', end_color='D3D3D3', fill_type='solid')
cell.alignment = Alignment(horizontal='left', vertical='center')
# Add BOM lines
for idx, bom_line in enumerate(bom_lines, 3):
ws.cell(row=idx, column=1, value=bom_line['line_num'])
ws.cell(row=idx, column=2, value=bom_line['refdes'])
ws.cell(row=idx, column=3, value=bom_line['ipn'])
ws.cell(row=idx, column=4, value=bom_line['qty'])
ws.cell(row=idx, column=5, value=bom_line['description'])
ws.cell(row=idx, column=6, value=bom_line['footprint'])
ws.cell(row=idx, column=7, value=bom_line['manufacturer'])
ws.cell(row=idx, column=8, value=bom_line['mpn'])
ws.cell(row=idx, column=9, value=bom_line['notes'])
ws.cell(row=idx, column=10, value=bom_line['smd_th'])
ws.cell(row=idx, column=11, value=bom_line['side'])
ws.cell(row=idx, column=12, value=bom_line['populated'])
wb.save(output_file)
print(f"Generated main BOM: {output_file}")
def generate_unfitted_bom_csv(components, output_file, project_name, variant_name):
"""
Generate unfitted (DNP) parts BOM in CSV format
"""
# Filter only DNP parts
dnp_parts = [c for c in components if c['is_dnp']]
# Sort by reference
dnp_parts.sort(key=lambda x: (x['reference'].rstrip('0123456789'),
int(''.join(filter(str.isdigit, x['reference'])) or 0)))
with open(output_file, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile, quoting=csv.QUOTE_ALL)
# Header
writer.writerow(['Components Not Populated Report'])
writer.writerow(['Variant', variant_name, project_name])
writer.writerow(['Assembly', '-', ''])
writer.writerow(['----------'])
# DNP parts
for part in dnp_parts:
writer.writerow([part['reference'], 'Not populated'])
writer.writerow(['----------'])
print(f"Generated unfitted BOM: {output_file}")
def generate_top_bom_xlsx(bom_lines, output_file, project_name, variant_name):
"""
Generate top-side only BOM in XLSX format
"""
# Filter only top-side parts
top_lines = [line for line in bom_lines if line['side'] == 'Top']
generate_main_bom_xlsx(top_lines, output_file, project_name, f"{variant_name} (Top)")
def generate_bottom_bom_xlsx(bom_lines, output_file, project_name, variant_name):
"""
Generate bottom-side only BOM in XLSX format
"""
# Filter only bottom-side parts
bottom_lines = [line for line in bom_lines if line['side'] == 'Bottom']
generate_main_bom_xlsx(bottom_lines, output_file, project_name, f"{variant_name} (Bottom)")
if __name__ == '__main__':
import sys
if len(sys.argv) < 4:
print("Usage: python bom_generator.py <schematic_file> <project_name> <variant_name> [dnp_uuids_json] [pcb_file]")
sys.exit(1)
schematic_file = sys.argv[1]
project_name = sys.argv[2]
variant_name = sys.argv[3]
dnp_uuids = []
if len(sys.argv) > 4:
import json
dnp_uuids = json.loads(sys.argv[4])
pcb_file = None
if len(sys.argv) > 5:
pcb_file = sys.argv[5]
# Parse schematic with hierarchical sheets and PCB side info
components = parse_schematic_for_bom(schematic_file, dnp_uuids, pcb_file)
# Filter fitted parts for main BOM
fitted_components = [c for c in components if not c['is_dnp']]
# Group components
bom_lines = group_components_for_bom(fitted_components)
# Generate output files
output_dir = os.path.dirname(schematic_file)
base_name = f"{project_name}_{variant_name}"
# Main BOM
generate_main_bom_xlsx(bom_lines, os.path.join(output_dir, f"{base_name}_BOM.xlsx"),
project_name, variant_name)
# Unfitted BOM
generate_unfitted_bom_csv(components, os.path.join(output_dir, f"{base_name}_Not_Populated.csv"),
project_name, variant_name)
# Top BOM
generate_top_bom_xlsx(bom_lines, os.path.join(output_dir, f"{base_name}_BOM_Top.xlsx"),
project_name, variant_name)
# Bottom BOM
generate_bottom_bom_xlsx(bom_lines, os.path.join(output_dir, f"{base_name}_BOM_Bottom.xlsx"),
project_name, variant_name)
print(f"BOM generation complete for variant: {variant_name}")

File diff suppressed because it is too large Load Diff