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>
This commit is contained in:
99
app.py
99
app.py
@@ -173,6 +173,64 @@ def get_footprints_in_library(library_path):
|
||||
|
||||
return sorted(footprints)
|
||||
|
||||
def convert_step_to_stl(step_data_bytes):
|
||||
"""Convert STEP file data to STL format using CadQuery.
|
||||
|
||||
Args:
|
||||
step_data_bytes: Raw STEP file data as bytes
|
||||
|
||||
Returns:
|
||||
STL file data as bytes, or None if conversion fails
|
||||
"""
|
||||
try:
|
||||
import cadquery as cq
|
||||
import tempfile
|
||||
|
||||
# Write STEP data to a temporary file
|
||||
with tempfile.NamedTemporaryFile(suffix='.step', delete=False, mode='wb') as temp_step:
|
||||
temp_step.write(step_data_bytes)
|
||||
temp_step_path = temp_step.name
|
||||
|
||||
try:
|
||||
# Import the STEP file
|
||||
print(f"[3D Model] Importing STEP file...")
|
||||
result = cq.importers.importStep(temp_step_path)
|
||||
|
||||
# Export to STL
|
||||
print(f"[3D Model] Converting to STL...")
|
||||
with tempfile.NamedTemporaryFile(suffix='.stl', delete=False, mode='wb') as temp_stl:
|
||||
temp_stl_path = temp_stl.name
|
||||
|
||||
# Export the shape to STL
|
||||
cq.exporters.export(result, temp_stl_path, exportType='STL')
|
||||
|
||||
# Read the STL file
|
||||
with open(temp_stl_path, 'rb') as f:
|
||||
stl_data = f.read()
|
||||
|
||||
print(f"[3D Model] Conversion successful, STL size: {len(stl_data)} bytes")
|
||||
|
||||
# Cleanup
|
||||
os.unlink(temp_step_path)
|
||||
os.unlink(temp_stl_path)
|
||||
|
||||
return stl_data
|
||||
|
||||
except Exception as e:
|
||||
print(f"[3D Model] Error during conversion: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# Cleanup on error
|
||||
if os.path.exists(temp_step_path):
|
||||
os.unlink(temp_step_path)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"[3D Model] Error setting up conversion: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def extract_embedded_model(footprint_path, footprint_name):
|
||||
"""Extract embedded 3D model from a KiCad footprint file.
|
||||
|
||||
@@ -2140,18 +2198,25 @@ def handle_get_footprint_model(data):
|
||||
library_path = data.get('library_path', '')
|
||||
footprint_name = data.get('footprint_name', '')
|
||||
|
||||
print(f"[3D Model] Request for footprint: {footprint_name}")
|
||||
print(f"[3D Model] Library path: {library_path}")
|
||||
|
||||
if not library_path or not footprint_name:
|
||||
print(f"[3D Model] Missing library_path or footprint_name")
|
||||
emit('model_result', {'has_model': False})
|
||||
return
|
||||
|
||||
if not os.path.exists(library_path):
|
||||
print(f"[3D Model] Library path does not exist: {library_path}")
|
||||
emit('model_result', {'has_model': False})
|
||||
return
|
||||
|
||||
# Extract embedded model
|
||||
print(f"[3D Model] Extracting model from {library_path}/{footprint_name}")
|
||||
model_data = extract_embedded_model(library_path, footprint_name)
|
||||
|
||||
if model_data:
|
||||
print(f"[3D Model] Found model: {model_data['name']}")
|
||||
# Decode base64 to get the actual file data
|
||||
import base64
|
||||
import zstandard as zstd
|
||||
@@ -2159,25 +2224,37 @@ def handle_get_footprint_model(data):
|
||||
try:
|
||||
# Decode base64
|
||||
compressed_data = base64.b64decode(model_data['data'])
|
||||
print(f"[3D Model] Decoded base64, compressed size: {len(compressed_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')
|
||||
step_data = dctx.decompress(compressed_data)
|
||||
print(f"[3D Model] Decompressed STEP, size: {len(step_data)}")
|
||||
|
||||
emit('model_result', {
|
||||
'has_model': True,
|
||||
'name': model_data['name'],
|
||||
'type': model_data['type'],
|
||||
'data': model_base64,
|
||||
'format': 'step' # Assuming STEP format
|
||||
})
|
||||
# Convert STEP to STL
|
||||
stl_data = convert_step_to_stl(step_data)
|
||||
|
||||
if stl_data:
|
||||
# Encode STL as base64 for transmission
|
||||
stl_base64 = base64.b64encode(stl_data).decode('ascii')
|
||||
print(f"[3D Model] Success! Sending STL model to client")
|
||||
|
||||
emit('model_result', {
|
||||
'has_model': True,
|
||||
'name': model_data['name'].replace('.stp', '.stl').replace('.step', '.stl'),
|
||||
'type': model_data['type'],
|
||||
'data': stl_base64,
|
||||
'format': 'stl'
|
||||
})
|
||||
else:
|
||||
print(f"[3D Model] Failed to convert STEP to STL")
|
||||
emit('model_result', {'has_model': False})
|
||||
except Exception as e:
|
||||
print(f"Error decoding model data: {e}")
|
||||
print(f"[3D Model] Error processing model data: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
emit('model_result', {'has_model': False})
|
||||
else:
|
||||
print(f"[3D Model] No embedded model found")
|
||||
emit('model_result', {'has_model': False})
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user