From fe9117dd5ee9d2dd199b85aee1a52a9a5a879be0 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Fri, 13 Feb 2026 16:59:54 -0800 Subject: [PATCH] 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. --- src/core/rendering/ActorRenderer.js | 11 +++--- src/core/rendering/BaseRenderer.js | 46 +++++++++++++++++++++-- src/core/rendering/VehiclePartRenderer.js | 3 ++ src/lib/save-editor/ActorEditor.svelte | 7 +++- src/lib/save-editor/VehicleEditor.svelte | 7 +++- 5 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/core/rendering/ActorRenderer.js b/src/core/rendering/ActorRenderer.js index a4d29e1..90542d7 100644 --- a/src/core/rendering/ActorRenderer.js +++ b/src/core/rendering/ActorRenderer.js @@ -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(); } } diff --git a/src/core/rendering/BaseRenderer.js b/src/core/rendering/BaseRenderer.js index 3c6f6d5..9ce7f84 100644 --- a/src/core/rendering/BaseRenderer.js +++ b/src/core/rendering/BaseRenderer.js @@ -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(); } diff --git a/src/core/rendering/VehiclePartRenderer.js b/src/core/rendering/VehiclePartRenderer.js index 26c7080..36f4293 100644 --- a/src/core/rendering/VehiclePartRenderer.js +++ b/src/core/rendering/VehiclePartRenderer.js @@ -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); } diff --git a/src/lib/save-editor/ActorEditor.svelte b/src/lib/save-editor/ActorEditor.svelte index 1168438..2909d97 100644 --- a/src/lib/save-editor/ActorEditor.svelte +++ b/src/lib/save-editor/ActorEditor.svelte @@ -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; } diff --git a/src/lib/save-editor/VehicleEditor.svelte b/src/lib/save-editor/VehicleEditor.svelte index ca72877..2631776 100644 --- a/src/lib/save-editor/VehicleEditor.svelte +++ b/src/lib/save-editor/VehicleEditor.svelte @@ -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; }