feat(falukant): add interactive 3d controls
All checks were successful
Deploy to production / deploy (push) Successful in 1m57s
All checks were successful
Deploy to production / deploy (push) Successful in 1m57s
This commit is contained in:
@@ -134,6 +134,7 @@ export default {
|
|||||||
threeRuntime: null,
|
threeRuntime: null,
|
||||||
threeLoaders: null,
|
threeLoaders: null,
|
||||||
threeModelRuntime: null
|
threeModelRuntime: null
|
||||||
|
,controls: null
|
||||||
,resizeObserver: null
|
,resizeObserver: null
|
||||||
,intersectionObserver: null
|
,intersectionObserver: null
|
||||||
,isVisible: false
|
,isVisible: false
|
||||||
@@ -320,6 +321,16 @@ export default {
|
|||||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
container.appendChild(this.renderer.domElement);
|
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
|
// Verbesserte Beleuchtung für kräftigere Farben
|
||||||
// HemisphereLight für etwas Umgebungsfarbe und natürliche Ground-Reflektion
|
// HemisphereLight für etwas Umgebungsfarbe und natürliche Ground-Reflektion
|
||||||
const hemi = new runtime.HemisphereLight(0xffffff, 0x888877, 0.6);
|
const hemi = new runtime.HemisphereLight(0xffffff, 0x888877, 0.6);
|
||||||
@@ -556,7 +567,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.camera.position.set(0, camY, camZ);
|
this.camera.position.set(0, camY, camZ);
|
||||||
|
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.camera.lookAt(scaledCenter.x, scaledCenter.y, scaledCenter.z);
|
||||||
|
}
|
||||||
this.onWindowResize();
|
this.onWindowResize();
|
||||||
// debug info
|
// debug info
|
||||||
this.debugInfo.camera = { x: this.camera.position.x, y: this.camera.position.y, z: this.camera.position.z };
|
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.mixer.update(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.controls?.update();
|
||||||
|
|
||||||
// Keine Bewegung - Modelle bleiben statisch
|
// Keine Bewegung - Modelle bleiben statisch
|
||||||
|
|
||||||
if (this.renderer && this.scene && this.camera) {
|
if (this.renderer && this.scene && this.camera) {
|
||||||
@@ -657,6 +677,9 @@ export default {
|
|||||||
this.renderer = null;
|
this.renderer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.controls?.dispose();
|
||||||
|
this.controls = null;
|
||||||
|
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
this.scene.clear();
|
this.scene.clear();
|
||||||
}
|
}
|
||||||
@@ -693,6 +716,15 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.character-3d-container :deep(canvas) {
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-3d-container :deep(canvas):active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.character-fallback {
|
.character-fallback {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export default {
|
|||||||
camera: null,
|
camera: null,
|
||||||
renderer: null,
|
renderer: null,
|
||||||
renderedModel: null,
|
renderedModel: null,
|
||||||
|
controls: null,
|
||||||
|
onControlsChange: null,
|
||||||
resizeObserver: null,
|
resizeObserver: null,
|
||||||
visibilityObserver: null,
|
visibilityObserver: null,
|
||||||
isVisible: false,
|
isVisible: false,
|
||||||
@@ -71,7 +73,10 @@ export default {
|
|||||||
const container = this.$refs.container;
|
const container = this.$refs.container;
|
||||||
if (!container || import.meta.env.VITE_DISABLE_3D === 'true') return;
|
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;
|
if (!this.isVisible || !this.$refs.container || this.renderer) return;
|
||||||
this.scene = markRaw(new runtime.Scene());
|
this.scene = markRaw(new runtime.Scene());
|
||||||
this.camera = markRaw(new runtime.PerspectiveCamera(36, 1, 0.1, 100));
|
this.camera = markRaw(new runtime.PerspectiveCamera(36, 1, 0.1, 100));
|
||||||
@@ -79,6 +84,15 @@ export default {
|
|||||||
this.renderer.setClearColor(0x000000, 0);
|
this.renderer.setClearColor(0x000000, 0);
|
||||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
||||||
container.appendChild(this.renderer.domElement);
|
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));
|
this.scene.add(new runtime.HemisphereLight(0xfff8e8, 0x665b50, 2.2));
|
||||||
const keyLight = new runtime.DirectionalLight(0xffffff, 2.8);
|
const keyLight = new runtime.DirectionalLight(0xffffff, 2.8);
|
||||||
@@ -127,6 +141,7 @@ export default {
|
|||||||
const center = scaledBox.getCenter(new modelRuntime.Vector3());
|
const center = scaledBox.getCenter(new modelRuntime.Vector3());
|
||||||
this.renderedModel.position.set(-center.x, -scaledBox.min.y, -center.z);
|
this.renderedModel.position.set(-center.x, -scaledBox.min.y, -center.z);
|
||||||
this.scene.add(this.renderedModel);
|
this.scene.add(this.renderedModel);
|
||||||
|
this.controls?.update();
|
||||||
this.render();
|
this.render();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Unable to load 3D house model '${this.model}'`, 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 width = Math.max(container.clientWidth, 1);
|
||||||
const height = Math.max(container.clientHeight, 1);
|
const height = Math.max(container.clientHeight, 1);
|
||||||
this.camera.aspect = width / height;
|
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.camera.updateProjectionMatrix();
|
||||||
this.renderer.setSize(width, height, false);
|
this.renderer.setSize(width, height, false);
|
||||||
this.renderer.render(this.scene, this.camera);
|
this.renderer.render(this.scene, this.camera);
|
||||||
@@ -165,6 +178,12 @@ export default {
|
|||||||
this.resizeObserver?.disconnect();
|
this.resizeObserver?.disconnect();
|
||||||
this.resizeObserver = null;
|
this.resizeObserver = null;
|
||||||
this.removeModel();
|
this.removeModel();
|
||||||
|
if (this.controls) {
|
||||||
|
this.controls.removeEventListener('change', this.onControlsChange);
|
||||||
|
this.controls.dispose();
|
||||||
|
this.controls = null;
|
||||||
|
this.onControlsChange = null;
|
||||||
|
}
|
||||||
if (this.renderer) {
|
if (this.renderer) {
|
||||||
this.$refs.container?.removeChild(this.renderer.domElement);
|
this.$refs.container?.removeChild(this.renderer.domElement);
|
||||||
// Release immediately instead of waiting for garbage collection.
|
// Release immediately instead of waiting for garbage collection.
|
||||||
@@ -208,4 +227,13 @@ export default {
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.house-3d :deep(canvas) {
|
||||||
|
cursor: grab;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.house-3d :deep(canvas):active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
3
frontend/src/utils/threeControls.js
Normal file
3
frontend/src/utils/threeControls.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
|
|
||||||
|
export { OrbitControls };
|
||||||
Reference in New Issue
Block a user