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:
@@ -7,6 +7,7 @@
|
||||
<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>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.159.0/examples/js/loaders/STLLoader.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
@@ -1302,6 +1303,101 @@
|
||||
selectedFootprint = `${libName}:${data.footprint_name}`;
|
||||
});
|
||||
|
||||
let modelScene = null;
|
||||
let modelRenderer = null;
|
||||
let modelCamera = null;
|
||||
let modelControls = null;
|
||||
let modelMesh = null;
|
||||
|
||||
function init3DViewer() {
|
||||
const canvasContainer = document.getElementById('modelViewerCanvas');
|
||||
const width = canvasContainer.clientWidth || 800;
|
||||
const height = 300;
|
||||
|
||||
// Scene
|
||||
modelScene = new THREE.Scene();
|
||||
modelScene.background = new THREE.Color(0x1a1a1a);
|
||||
|
||||
// Camera
|
||||
modelCamera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
||||
modelCamera.position.set(0, 0, 50);
|
||||
|
||||
// Renderer
|
||||
modelRenderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
modelRenderer.setSize(width, height);
|
||||
canvasContainer.innerHTML = '';
|
||||
canvasContainer.appendChild(modelRenderer.domElement);
|
||||
|
||||
// Lights
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
|
||||
modelScene.add(ambientLight);
|
||||
|
||||
const directionalLight1 = new THREE.DirectionalLight(0xffffff, 0.5);
|
||||
directionalLight1.position.set(1, 1, 1);
|
||||
modelScene.add(directionalLight1);
|
||||
|
||||
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.3);
|
||||
directionalLight2.position.set(-1, -1, -1);
|
||||
modelScene.add(directionalLight2);
|
||||
|
||||
// Controls
|
||||
modelControls = new THREE.OrbitControls(modelCamera, modelRenderer.domElement);
|
||||
modelControls.enableDamping = true;
|
||||
modelControls.dampingFactor = 0.05;
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
modelControls.update();
|
||||
modelRenderer.render(modelScene, modelCamera);
|
||||
}
|
||||
animate();
|
||||
}
|
||||
|
||||
function load3DModel(stlData) {
|
||||
if (!modelScene) {
|
||||
init3DViewer();
|
||||
}
|
||||
|
||||
// Remove old mesh if exists
|
||||
if (modelMesh) {
|
||||
modelScene.remove(modelMesh);
|
||||
}
|
||||
|
||||
// Decode base64 STL data
|
||||
const binaryString = atob(stlData);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Load STL
|
||||
const loader = new THREE.STLLoader();
|
||||
const geometry = loader.parse(bytes.buffer);
|
||||
|
||||
// Center and scale the geometry
|
||||
geometry.center();
|
||||
geometry.computeBoundingBox();
|
||||
const size = new THREE.Vector3();
|
||||
geometry.boundingBox.getSize(size);
|
||||
const maxDim = Math.max(size.x, size.y, size.z);
|
||||
const scale = 30 / maxDim;
|
||||
geometry.scale(scale, scale, scale);
|
||||
|
||||
// Create mesh
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: 0x00aa00,
|
||||
specular: 0x111111,
|
||||
shininess: 50
|
||||
});
|
||||
modelMesh = new THREE.Mesh(geometry, material);
|
||||
modelScene.add(modelMesh);
|
||||
|
||||
// Adjust camera
|
||||
modelCamera.position.set(0, 0, 50);
|
||||
modelControls.reset();
|
||||
}
|
||||
|
||||
socket.on('model_result', (data) => {
|
||||
console.log('Received model_result:', data);
|
||||
const container = document.getElementById('modelViewerContainer');
|
||||
@@ -1310,11 +1406,16 @@
|
||||
if (data.has_model) {
|
||||
console.log('Model found, displaying viewer');
|
||||
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>`;
|
||||
messageEl.style.display = 'none';
|
||||
|
||||
// Load and display the 3D model
|
||||
try {
|
||||
load3DModel(data.data);
|
||||
} catch (error) {
|
||||
console.error('Error loading 3D model:', error);
|
||||
messageEl.style.display = 'block';
|
||||
messageEl.innerHTML = `<span style="color: #dc3545;">Error loading 3D model: ${error.message}</span>`;
|
||||
}
|
||||
} else {
|
||||
console.log('No model found');
|
||||
container.style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user