feat(falukant): add interactive 3d controls
All checks were successful
Deploy to production / deploy (push) Successful in 1m57s

This commit is contained in:
Torsten Schulz (local)
2026-09-30 14:32:38 +02:00
parent c3f51f043e
commit ab601933b7
3 changed files with 67 additions and 4 deletions

View File

@@ -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%;

View File

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

View File

@@ -0,0 +1,3 @@
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
export { OrbitControls };