diff --git a/frontend/src/components/Character3D.vue b/frontend/src/components/Character3D.vue index ddb5781..fd8e2c2 100755 --- a/frontend/src/components/Character3D.vue +++ b/frontend/src/components/Character3D.vue @@ -134,6 +134,7 @@ export default { threeRuntime: null, threeLoaders: null, threeModelRuntime: null + ,controls: null ,resizeObserver: null ,intersectionObserver: null ,isVisible: false @@ -320,6 +321,16 @@ export default { this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); container.appendChild(this.renderer.domElement); + const { OrbitControls } = await import('@/utils/threeControls.js'); + this.controls = markRaw(new OrbitControls(this.camera, this.renderer.domElement)); + this.controls.target.set(0, 1, 0); + this.controls.enablePan = false; + this.controls.enableDamping = true; + this.controls.dampingFactor = 0.08; + this.controls.minDistance = 1; + this.controls.maxDistance = 8; + this.controls.update(); + // Verbesserte Beleuchtung für kräftigere Farben // HemisphereLight für etwas Umgebungsfarbe und natürliche Ground-Reflektion const hemi = new runtime.HemisphereLight(0xffffff, 0x888877, 0.6); @@ -556,7 +567,14 @@ export default { } this.camera.position.set(0, camY, camZ); - this.camera.lookAt(scaledCenter.x, scaledCenter.y, scaledCenter.z); + if (this.controls) { + this.controls.target.set(scaledCenter.x, scaledCenter.y, scaledCenter.z); + this.controls.minDistance = Math.max(0.8, distance * 0.55); + this.controls.maxDistance = Math.max(this.controls.minDistance + 1, distance * 3.2); + this.controls.update(); + } else { + this.camera.lookAt(scaledCenter.x, scaledCenter.y, scaledCenter.z); + } this.onWindowResize(); // debug info this.debugInfo.camera = { x: this.camera.position.x, y: this.camera.position.y, z: this.camera.position.z }; @@ -600,6 +618,8 @@ export default { this.mixer.update(delta); } + this.controls?.update(); + // Keine Bewegung - Modelle bleiben statisch if (this.renderer && this.scene && this.camera) { @@ -657,6 +677,9 @@ export default { this.renderer = null; } + this.controls?.dispose(); + this.controls = null; + if (this.scene) { this.scene.clear(); } @@ -693,6 +716,15 @@ export default { overflow: hidden; } +.character-3d-container :deep(canvas) { + cursor: grab; + touch-action: none; +} + +.character-3d-container :deep(canvas):active { + cursor: grabbing; +} + .character-fallback { width: 100%; height: 100%; diff --git a/frontend/src/components/falukant/House3D.vue b/frontend/src/components/falukant/House3D.vue index 605edfe..2517771 100644 --- a/frontend/src/components/falukant/House3D.vue +++ b/frontend/src/components/falukant/House3D.vue @@ -25,6 +25,8 @@ export default { camera: null, renderer: null, renderedModel: null, + controls: null, + onControlsChange: null, resizeObserver: null, visibilityObserver: null, isVisible: false, @@ -71,7 +73,10 @@ export default { const container = this.$refs.container; if (!container || import.meta.env.VITE_DISABLE_3D === 'true') return; - const runtime = await import('@/utils/threeRuntime.js'); + const [runtime, controlsRuntime] = await Promise.all([ + import('@/utils/threeRuntime.js'), + import('@/utils/threeControls.js'), + ]); if (!this.isVisible || !this.$refs.container || this.renderer) return; this.scene = markRaw(new runtime.Scene()); this.camera = markRaw(new runtime.PerspectiveCamera(36, 1, 0.1, 100)); @@ -79,6 +84,15 @@ export default { this.renderer.setClearColor(0x000000, 0); this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); container.appendChild(this.renderer.domElement); + this.camera.position.set(0, 1.5, 4.1); + this.controls = markRaw(new controlsRuntime.OrbitControls(this.camera, this.renderer.domElement)); + this.controls.target.set(0, 1.05, 0); + this.controls.enablePan = false; + this.controls.minDistance = 2.2; + this.controls.maxDistance = 8; + this.onControlsChange = () => this.render(); + this.controls.addEventListener('change', this.onControlsChange); + this.controls.update(); this.scene.add(new runtime.HemisphereLight(0xfff8e8, 0x665b50, 2.2)); const keyLight = new runtime.DirectionalLight(0xffffff, 2.8); @@ -127,6 +141,7 @@ export default { const center = scaledBox.getCenter(new modelRuntime.Vector3()); this.renderedModel.position.set(-center.x, -scaledBox.min.y, -center.z); this.scene.add(this.renderedModel); + this.controls?.update(); this.render(); } catch (error) { console.error(`Unable to load 3D house model '${this.model}'`, error); @@ -139,8 +154,6 @@ export default { const width = Math.max(container.clientWidth, 1); const height = Math.max(container.clientHeight, 1); this.camera.aspect = width / height; - this.camera.position.set(0, 1.5, 4.1); - this.camera.lookAt(0, 1.05, 0); this.camera.updateProjectionMatrix(); this.renderer.setSize(width, height, false); this.renderer.render(this.scene, this.camera); @@ -165,6 +178,12 @@ export default { this.resizeObserver?.disconnect(); this.resizeObserver = null; this.removeModel(); + if (this.controls) { + this.controls.removeEventListener('change', this.onControlsChange); + this.controls.dispose(); + this.controls = null; + this.onControlsChange = null; + } if (this.renderer) { this.$refs.container?.removeChild(this.renderer.domElement); // Release immediately instead of waiting for garbage collection. @@ -208,4 +227,13 @@ export default { font-size: 0.82rem; text-align: center; } + +.house-3d :deep(canvas) { + cursor: grab; + touch-action: none; +} + +.house-3d :deep(canvas):active { + cursor: grabbing; +} diff --git a/frontend/src/utils/threeControls.js b/frontend/src/utils/threeControls.js new file mode 100644 index 0000000..b0bf279 --- /dev/null +++ b/frontend/src/utils/threeControls.js @@ -0,0 +1,3 @@ +import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; + +export { OrbitControls };