// 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;
}
}