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>
This commit is contained in:
brentperteet
2026-02-27 13:18:34 -06:00
parent 5869d2c693
commit adffdd211c
2 changed files with 142 additions and 0 deletions

103
app.py
View File

@@ -173,6 +173,57 @@ def get_footprints_in_library(library_path):
return sorted(footprints)
def extract_embedded_model(footprint_path, footprint_name):
"""Extract embedded 3D model from a KiCad footprint file.
Returns:
dict with 'name', 'data' (base64), 'type' if found, None otherwise
"""
try:
footprint_file = os.path.join(footprint_path, f"{footprint_name}.kicad_mod")
if not os.path.exists(footprint_file):
return None
with open(footprint_file, 'r', encoding='utf-8') as f:
content = f.read()
# Look for embedded_files section
embedded_pattern = r'\(embedded_files\s+(.*?)\n\t\)\s*\n\t\(model'
match = re.search(embedded_pattern, content, re.DOTALL)
if not match:
return None
embedded_section = match.group(1)
# Extract file name
name_match = re.search(r'\(name\s+"([^"]+)"\)', embedded_section)
if not name_match:
return None
model_name = name_match.group(1)
# Extract type
type_match = re.search(r'\(type\s+(\w+)\)', embedded_section)
model_type = type_match.group(1) if type_match else 'model'
# Extract base64 data (multiline, starts with |)
data_match = re.search(r'\(data\s+\|(.*?)\n\t\t\t\)', embedded_section, re.DOTALL)
if not data_match:
return None
# Clean up the base64 data (remove whitespace and newlines)
base64_data = re.sub(r'\s+', '', data_match.group(1))
return {
'name': model_name,
'type': model_type,
'data': base64_data
}
except Exception as e:
print(f"Error extracting embedded model: {e}")
return None
def extract_symbol_graphics(file_path, symbol_name):
"""Extract graphical elements from a symbol for rendering."""
try:
@@ -2082,6 +2133,58 @@ def handle_render_footprint(data):
import traceback
emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'})
@socketio.on('get_footprint_model')
def handle_get_footprint_model(data):
"""Extract and return embedded 3D model from a footprint."""
try:
library_path = data.get('library_path', '')
footprint_name = data.get('footprint_name', '')
if not library_path or not footprint_name:
emit('model_result', {'has_model': False})
return
if not os.path.exists(library_path):
emit('model_result', {'has_model': False})
return
# Extract embedded model
model_data = extract_embedded_model(library_path, footprint_name)
if model_data:
# Decode base64 to get the actual file data
import base64
import zstandard as zstd
try:
# Decode base64
compressed_data = base64.b64decode(model_data['data'])
# Decompress (KiCad uses zstd compression)
dctx = zstd.ZstdDecompressor()
decompressed_data = dctx.decompress(compressed_data)
# Re-encode as base64 for transmission
model_base64 = base64.b64encode(decompressed_data).decode('ascii')
emit('model_result', {
'has_model': True,
'name': model_data['name'],
'type': model_data['type'],
'data': model_base64,
'format': 'step' # Assuming STEP format
})
except Exception as e:
print(f"Error decoding model data: {e}")
import traceback
traceback.print_exc()
emit('model_result', {'has_model': False})
else:
emit('model_result', {'has_model': False})
except Exception as e:
import traceback
print(f"Error extracting model: {traceback.format_exc()}")
emit('model_result', {'has_model': False})
# ---------------------------------------------------------------------------
# BOM Generation
# ---------------------------------------------------------------------------