Add drag-to-orbit controls to vehicle and actor editors

Use Three.js OrbitControls in BaseRenderer for rotation-only orbiting
with damping. Vehicle editor auto-rotates and resets on part switch.
Actor editor uses orbit without auto-rotate (has skeletal animations).
Drag vs click detection uses pointermove threshold to avoid false
positives from autoRotate damping.
This commit is contained in:
Christian Semmler 2026-02-13 16:59:54 -08:00
parent f796ea4299
commit fe9117dd5e
5 changed files with 63 additions and 11 deletions

View File

@ -94,6 +94,9 @@ export class ActorRenderer extends BaseRenderer {
this.camera.position.set(2, 0.8, 3.5);
this.camera.lookAt(0, 0.2, 0);
this.setupControls(new THREE.Vector3(0, 0.2, 0));
this.controls.autoRotate = false;
this.raycaster = new THREE.Raycaster();
}
@ -887,17 +890,13 @@ export class ActorRenderer extends BaseRenderer {
if (this.mixer) {
this.mixer.update(delta);
} else if (this.modelGroup) {
// Fallback: rotate if no animation loaded
this.modelGroup.rotation.y += 0.01;
}
this.controls?.update();
}
dispose() {
this.animating = false;
this.stopAnimation();
this.clearModel();
this.renderer?.dispose();
super.dispose();
this.animationCache.clear();
}
}

View File

@ -1,4 +1,5 @@
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
/**
* Base renderer providing shared Three.js setup, lighting, texture creation,
@ -24,6 +25,9 @@ export class BaseRenderer {
this.renderer.setClearColor(0x000000, 0);
this.setupLighting();
this.controls = null;
this._didDrag = false;
}
setupLighting() {
@ -35,6 +39,39 @@ export class BaseRenderer {
this.scene.add(sunLight);
}
setupControls(target) {
this.controls = new OrbitControls(this.camera, this.canvas);
this.controls.enableZoom = false;
this.controls.enablePan = false;
this.controls.enableDamping = true;
this.controls.dampingFactor = 0.1;
this.controls.autoRotate = true;
this.controls.autoRotateSpeed = 4.0;
this.controls.target.copy(target);
this.controls.addEventListener('start', () => {
this.controls.autoRotate = false;
});
this._onPointerDown = (e) => {
this._didDrag = false;
this._pointerStart = { x: e.clientX, y: e.clientY };
};
this._onPointerMove = (e) => {
if (!this._pointerStart) return;
const dx = e.clientX - this._pointerStart.x;
const dy = e.clientY - this._pointerStart.y;
if (dx * dx + dy * dy > 9) this._didDrag = true;
};
this.canvas.addEventListener('pointerdown', this._onPointerDown);
this.canvas.addEventListener('pointermove', this._onPointerMove);
}
wasDragged() {
return this._didDrag;
}
/**
* Create a Three.js texture from parsed texture data
*/
@ -190,9 +227,7 @@ export class BaseRenderer {
* Called each frame before rendering.
*/
updateAnimation() {
if (this.modelGroup) {
this.modelGroup.rotation.y += 0.01;
}
this.controls?.update();
}
resize(width, height) {
@ -203,6 +238,11 @@ export class BaseRenderer {
dispose() {
this.animating = false;
if (this.controls) {
this.controls.dispose();
this.canvas.removeEventListener('pointerdown', this._onPointerDown);
this.canvas.removeEventListener('pointermove', this._onPointerMove);
}
this.clearModel();
this.renderer?.dispose();
}

View File

@ -14,6 +14,8 @@ export class VehiclePartRenderer extends BaseRenderer {
this.camera.position.set(0, 0, 3);
this.camera.lookAt(0, 0, 0);
this.setupControls(new THREE.Vector3(0, 0, 0));
}
/**
@ -56,6 +58,7 @@ export class VehiclePartRenderer extends BaseRenderer {
this.centerAndScaleModel(1.5);
this.scene.add(this.modelGroup);
this.controls.autoRotate = true;
this.renderer.render(this.scene, this.camera);
}

View File

@ -203,6 +203,7 @@
function handleCanvasClick(event) {
if (!renderer || !slot?.characters || !charState) return;
if (renderer.wasDragged()) return;
const playerId = slot.header?.actorId;
let acted = false;
@ -393,10 +394,14 @@
canvas {
display: block;
border-radius: 8px;
cursor: pointer;
cursor: grab;
max-width: 100%;
}
canvas:active {
cursor: grabbing;
}
canvas:focus {
outline: none;
}

View File

@ -251,6 +251,7 @@
function cycleColor() {
if (!currentPart || partError) return;
if (renderer?.wasDragged()) return;
// Find current color index and cycle to next
const currentIdx = LegoColorNames.indexOf(currentColorValue);
@ -398,10 +399,14 @@
canvas {
display: block;
border-radius: 8px;
cursor: pointer;
cursor: grab;
max-width: 100%;
}
canvas:active {
cursor: grabbing;
}
canvas:focus {
outline: none;
}