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:
103
app.py
103
app.py
@@ -173,6 +173,57 @@ def get_footprints_in_library(library_path):
|
|||||||
|
|
||||||
return sorted(footprints)
|
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):
|
def extract_symbol_graphics(file_path, symbol_name):
|
||||||
"""Extract graphical elements from a symbol for rendering."""
|
"""Extract graphical elements from a symbol for rendering."""
|
||||||
try:
|
try:
|
||||||
@@ -2082,6 +2133,58 @@ def handle_render_footprint(data):
|
|||||||
import traceback
|
import traceback
|
||||||
emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'})
|
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
|
# BOM Generation
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>UM KiCad Manager</title>
|
<title>UM KiCad Manager</title>
|
||||||
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/three@0.159.0/build/three.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/three@0.159.0/examples/js/controls/OrbitControls.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: Arial, sans-serif;
|
font-family: Arial, sans-serif;
|
||||||
@@ -486,6 +488,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 3D Model Viewer -->
|
||||||
|
<div id="modelViewerContainer" style="margin-bottom: 15px; display: none;">
|
||||||
|
<label style="display: block; font-weight: bold; margin-bottom: 5px;">3D Model:</label>
|
||||||
|
<div id="modelViewer" style="border: 1px solid #dee2e6; border-radius: 5px; background: #1a1a1a; width: 100%; height: 300px; position: relative;">
|
||||||
|
<div id="modelViewerCanvas"></div>
|
||||||
|
<div id="modelViewerMessage" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; text-align: center;">
|
||||||
|
Loading 3D model...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 5px; font-size: 12px; color: #6c757d;">
|
||||||
|
Use mouse to rotate (left-click drag), zoom (scroll), and pan (right-click drag)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="createPartBtn" class="btn" disabled>Create Part</button>
|
<button id="createPartBtn" class="btn" disabled>Create Part</button>
|
||||||
<button id="cancelCreatePartBtn" class="btn secondary" style="margin-left: 10px;">Cancel</button>
|
<button id="cancelCreatePartBtn" class="btn secondary" style="margin-left: 10px;">Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1261,6 +1277,7 @@
|
|||||||
if (!footprintName) {
|
if (!footprintName) {
|
||||||
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
|
footprintPreview.innerHTML = '<span style="color: #999;">No footprint selected</span>';
|
||||||
selectedFootprint = '';
|
selectedFootprint = '';
|
||||||
|
document.getElementById('modelViewerContainer').style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,6 +1286,12 @@
|
|||||||
library_path: selectedFootprintLibraryPath,
|
library_path: selectedFootprintLibraryPath,
|
||||||
footprint_name: footprintName
|
footprint_name: footprintName
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Request 3D model
|
||||||
|
socket.emit('get_footprint_model', {
|
||||||
|
library_path: selectedFootprintLibraryPath,
|
||||||
|
footprint_name: footprintName
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('footprint_render_result', (data) => {
|
socket.on('footprint_render_result', (data) => {
|
||||||
@@ -1278,6 +1301,22 @@
|
|||||||
selectedFootprint = `${libName}:${data.footprint_name}`;
|
selectedFootprint = `${libName}:${data.footprint_name}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on('model_result', (data) => {
|
||||||
|
const container = document.getElementById('modelViewerContainer');
|
||||||
|
const messageEl = document.getElementById('modelViewerMessage');
|
||||||
|
|
||||||
|
if (data.has_model) {
|
||||||
|
container.style.display = 'block';
|
||||||
|
messageEl.innerHTML = `<strong>3D Model Found:</strong> ${data.name}<br><br>` +
|
||||||
|
`<em>Note: STEP file viewing in browser requires conversion to STL/OBJ format.<br>` +
|
||||||
|
`This feature is under development.</em><br><br>` +
|
||||||
|
`<a href="data:application/stp;base64,${data.data}" download="${data.name}" ` +
|
||||||
|
`style="color: #007bff; text-decoration: underline; cursor: pointer;">Download ${data.name}</a>`;
|
||||||
|
} else {
|
||||||
|
container.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('missing_ipns_result', (data) => {
|
socket.on('missing_ipns_result', (data) => {
|
||||||
createPartFormLoading.style.display = 'none';
|
createPartFormLoading.style.display = 'none';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user