From 5966f2b4b8c0781f99f6876b397c19e2ad0e44bb Mon Sep 17 00:00:00 2001 From: brentperteet Date: Fri, 27 Feb 2026 13:54:24 -0600 Subject: [PATCH] Add interactive 3D model viewer with STEP to STL conversion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app.py | 99 +++++++++++++++++++++++++++++++++----- templates/index.html | 111 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 194 insertions(+), 16 deletions(-) diff --git a/app.py b/app.py index 414169f..201301a 100644 --- a/app.py +++ b/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: diff --git a/templates/index.html b/templates/index.html index 12cda52..8e60224 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,6 +7,7 @@ +