// Debug UI Controller class DebugUI { constructor(scene, camera, controls, gridHelper, loader) { this.scene = scene; this.camera = camera; this.controls = controls; this.gridHelper = gridHelper; this.loader = loader; this.loadedModel = null; this.skeletonHelper = null; this.animationMixer = null; this.animations = []; this.currentAction = null; this.isMinimized = false; this.init(); } init() { this.setupMinimize(); this.setupOrbitControls(); this.setupGridToggle(); this.setupSkeletonToggle(); this.setupAnimationControls(); this.setupAssetSelector(); } setupMinimize() { const debugUI = document.getElementById('debug-ui'); const minimizeBtn = document.getElementById('minimize-btn'); minimizeBtn.addEventListener('click', () => { this.isMinimized = !this.isMinimized; debugUI.classList.toggle('minimized', this.isMinimized); minimizeBtn.textContent = this.isMinimized ? '+' : '−'; }); } setupOrbitControls() { document.getElementById('orbit-controls-toggle').addEventListener('change', (event) => { this.controls.enabled = event.target.checked; }); } setupGridToggle() { document.getElementById('show-grid-toggle').addEventListener('change', (event) => { this.gridHelper.visible = event.target.checked; }); } setupSkeletonToggle() { document.getElementById('show-skeleton-toggle').addEventListener('change', (event) => { if (event.target.checked) { this.showSkeleton(); } else { this.hideSkeleton(); } }); } showSkeleton() { if (!this.loadedModel) return; this.loadedModel.traverse((child) => { if (child.isSkinnedMesh && child.skeleton) { if (!this.skeletonHelper) { this.skeletonHelper = new THREE.SkeletonHelper(this.loadedModel); this.skeletonHelper.material.linewidth = 2; this.scene.add(this.skeletonHelper); } } }); } hideSkeleton() { if (this.skeletonHelper) { this.scene.remove(this.skeletonHelper); this.skeletonHelper = null; } } setupAnimationControls() { // Animation selector document.getElementById('animation-select').addEventListener('change', (event) => { this.selectAnimation(event.target.value); }); // Play animation toggle document.getElementById('play-animation-toggle').addEventListener('change', (event) => { if (event.target.checked) { this.playAnimation(); } else { this.pauseAnimation(); } }); } selectAnimation(animationName) { if (!this.animationMixer || !animationName) { this.pauseAnimation(); return; } // Stop current action if (this.currentAction) { this.currentAction.stop(); } // Find and play selected animation const clip = this.animations.find(anim => anim.name === animationName); if (clip) { this.currentAction = this.animationMixer.clipAction(clip); // Auto-play if checkbox is checked if (document.getElementById('play-animation-toggle').checked) { this.currentAction.play(); } } } playAnimation() { if (this.currentAction) { this.currentAction.play(); } } pauseAnimation() { if (this.currentAction) { this.currentAction.paused = true; } } updateAnimationMixer(delta) { if (this.animationMixer) { this.animationMixer.update(delta); } } // Program custom animations here createCustomAnimations() { // Example: Create a custom rotation animation const times = [0, 2]; const values = [0, Math.PI*2]; const track = new THREE.NumberKeyframeTrack('.rotation[y]', times, values); const clip = new THREE.AnimationClip('RotateY', 2, [track]); return [clip]; return []; // Return array of custom AnimationClip objects } setupAssetSelector() { // Available assets in the assets folder const assets = [ 'assets/test_character.glb' // Add more assets here as needed ]; // Populate asset dropdown const assetSelect = document.getElementById('asset-select'); assets.forEach(assetPath => { const option = document.createElement('option'); option.value = assetPath; option.textContent = assetPath.split('/').pop(); // Show only filename assetSelect.appendChild(option); }); // Asset selection handler assetSelect.addEventListener('change', (event) => { this.loadAsset(event.target.value); }); } loadAsset(assetPath) { if (!assetPath) { // Remove model if "No Model" is selected if (this.loadedModel) { this.scene.remove(this.loadedModel); this.loadedModel = null; } this.updateModelStatus('No model loaded'); return; } // Load selected asset this.updateModelStatus('Loading...'); this.loader.load( assetPath, (gltf) => { // Remove previous model if exists if (this.loadedModel) { this.scene.remove(this.loadedModel); } // Remove previous skeleton helper if exists this.hideSkeleton(); // Uncheck skeleton toggle document.getElementById('show-skeleton-toggle').checked = false; this.loadedModel = gltf.scene; // Get bounding box const box = new THREE.Box3().setFromObject(this.loadedModel); const size = box.getSize(new THREE.Vector3()); const center = box.getCenter(new THREE.Vector3()); // Calculate max dimension const maxDim = Math.max(size.x, size.y, size.z); // Scale model to fit in a 4 unit cube (fits nicely with the grid) const targetSize = 4; const scale = targetSize / maxDim; this.loadedModel.scale.setScalar(scale); // Recalculate box after scaling box.setFromObject(this.loadedModel); box.getCenter(center); // Center the model at origin on X and Z, but place bottom at y=0 this.loadedModel.position.x = -center.x; this.loadedModel.position.y = -box.min.y; this.loadedModel.position.z = -center.z; this.scene.add(this.loadedModel); // Setup animations this.animations = gltf.animations || []; // Add custom animations const customAnimations = this.createCustomAnimations(); this.animations = [...this.animations, ...customAnimations]; if (this.animations.length > 0) { this.animationMixer = new THREE.AnimationMixer(this.loadedModel); // Populate animation dropdown const animSelect = document.getElementById('animation-select'); animSelect.innerHTML = ''; this.animations.forEach(clip => { const option = document.createElement('option'); option.value = clip.name; option.textContent = clip.name; animSelect.appendChild(option); }); } else { this.animationMixer = null; document.getElementById('animation-select').innerHTML = ''; } // Reset animation controls document.getElementById('animation-select').value = ''; document.getElementById('play-animation-toggle').checked = false; // Check for skeleton let hasSkeleton = false; this.loadedModel.traverse((child) => { if (child.isSkinnedMesh) { hasSkeleton = true; console.log('Skeleton found:', child.skeleton); } }); const statusText = hasSkeleton ? 'Model loaded & fitted (with skeleton)' : 'Model loaded & fitted'; this.updateModelStatus(statusText); }, undefined, (error) => { console.error('Error loading model:', error); this.updateModelStatus('Error loading model'); } ); } updateModelStatus(status) { document.getElementById('model-status').textContent = status; } }