From 6b2b0ec7382693ceb772f0fa536e72c652272090 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Wed, 28 May 2025 05:00:29 +0200 Subject: [PATCH] Add spirit model preview functionality with Three.js --- node/server/public/app.js | 149 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/node/server/public/app.js b/node/server/public/app.js index e8deaa8..681cb3f 100644 --- a/node/server/public/app.js +++ b/node/server/public/app.js @@ -479,6 +479,155 @@ function showSpiritOverlay(spirit) { } +let spiritPreviewRenderer = null, spiritPreviewScene = null, spiritPreviewCamera = null, spiritPreviewObj = null; +let spiritPreviewDragging = false, spiritPreviewLastX = 0, spiritPreviewRotationY = 0, spiritPreviewRotationX = 0; + +function destroySpiritModelPreview() { + // Cleanup: Renderer und Scene entsorgen + if (spiritPreviewRenderer) { + spiritPreviewRenderer.dispose(); + spiritPreviewRenderer.domElement.remove(); + spiritPreviewRenderer = null; + } + spiritPreviewScene = null; + spiritPreviewCamera = null; + spiritPreviewObj = null; + spiritPreviewDragging = false; + spiritPreviewLastX = 0; + spiritPreviewRotationY = 0; + spiritPreviewRotationX = 0; +} + +async function setupSpiritModelPreview(spirit) { + destroySpiritModelPreview(); + + // Canvas erzeugen + const container = document.getElementById('spirit-model-preview'); + if (!container) return; + const width = container.clientWidth; + const height = container.clientHeight; + + // Three.js Preview Renderer + spiritPreviewRenderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); + spiritPreviewRenderer.setClearColor(0x000000, 0); // transparent + spiritPreviewRenderer.setSize(width, height, false); + container.appendChild(spiritPreviewRenderer.domElement); + + // Preview Szene und Kamera + spiritPreviewScene = new THREE.Scene(); + spiritPreviewCamera = new THREE.PerspectiveCamera(27, width / height, 0.1, 100); + spiritPreviewCamera.position.set(0, 1, 5); + + // Licht + const keyLight = new THREE.DirectionalLight(0xffffff, 1.2); + keyLight.position.set(3, 4, 8); + spiritPreviewScene.add(keyLight); + const fillLight = new THREE.AmbientLight(0xffffff, 0.85); + spiritPreviewScene.add(fillLight); + + // Model laden + const modelUrl = spirit['Model URL'] || spirit.modelUrl; + try { + const { scene: modelObj } = await gltfLoader.loadAsync(modelUrl); + // Färbe/Resette ggf. Materialien (optional, nicht zwingend nötig) + modelObj.traverse(mesh => { + if (mesh.isMesh) { + mesh.castShadow = false; + mesh.receiveShadow = false; + mesh.material = mesh.material.clone(); + mesh.material.opacity = 1.0; + mesh.material.transparent = false; + mesh.material.emissiveIntensity = 1.0; + } + }); + // Zentriere das Modell (Bounding Box) + const bbox = new THREE.Box3().setFromObject(modelObj); + const center = bbox.getCenter(new THREE.Vector3()); + modelObj.position.sub(center); + // Größe skalieren + const size = bbox.getSize(new THREE.Vector3()); + const maxDim = Math.max(size.x, size.y, size.z); + const scale = 2.1 / maxDim; + modelObj.scale.setScalar(scale); + + // Initiale Drehung: leicht nach rechts & von oben + spiritPreviewRotationY = Math.PI / 8; + spiritPreviewRotationX = -Math.PI / 18; + modelObj.rotation.y = spiritPreviewRotationY; + modelObj.rotation.x = spiritPreviewRotationX; + + spiritPreviewObj = modelObj; + spiritPreviewScene.add(modelObj); + } catch (err) { + container.innerHTML = `
Modell konnte nicht geladen werden :(
`; + return; + } + + // Maus-Events für Drehung + const canvas = spiritPreviewRenderer.domElement; + canvas.style.cursor = "grab"; + canvas.onpointerdown = (e) => { + spiritPreviewDragging = true; + spiritPreviewLastX = e.clientX; + canvas.setPointerCapture(e.pointerId); + canvas.style.cursor = "grabbing"; + }; + canvas.onpointermove = (e) => { + if (spiritPreviewDragging && spiritPreviewObj) { + let delta = (e.clientX - spiritPreviewLastX) / width * Math.PI; + spiritPreviewRotationY += delta; + spiritPreviewObj.rotation.y = spiritPreviewRotationY; + spiritPreviewLastX = e.clientX; + } + }; + canvas.onpointerup = (e) => { + spiritPreviewDragging = false; + canvas.releasePointerCapture(e.pointerId); + canvas.style.cursor = "grab"; + }; + + // Vertikales Drag: Optional für X-Rotation + let lastY = 0; + canvas.onpointerdown = (e) => { + spiritPreviewDragging = true; + spiritPreviewLastX = e.clientX; + lastY = e.clientY; + canvas.setPointerCapture(e.pointerId); + canvas.style.cursor = "grabbing"; + }; + canvas.onpointermove = (e) => { + if (spiritPreviewDragging && spiritPreviewObj) { + let deltaX = (e.clientX - spiritPreviewLastX) / width * Math.PI; + let deltaY = (e.clientY - lastY) / height * Math.PI; + spiritPreviewRotationY += deltaX; + spiritPreviewRotationX += deltaY; + spiritPreviewRotationX = Math.max(-Math.PI / 2.3, Math.min(Math.PI / 2.3, spiritPreviewRotationX)); // Limitiere X + spiritPreviewObj.rotation.y = spiritPreviewRotationY; + spiritPreviewObj.rotation.x = spiritPreviewRotationX; + spiritPreviewLastX = e.clientX; + lastY = e.clientY; + } + }; + canvas.onpointerup = (e) => { + spiritPreviewDragging = false; + canvas.releasePointerCapture(e.pointerId); + canvas.style.cursor = "grab"; + }; + + // Animation/Rendering starten + previewRenderLoop(); +} + +function previewRenderLoop() { + if (!spiritPreviewRenderer || !spiritPreviewScene || !spiritPreviewCamera) return; + spiritPreviewRenderer.render(spiritPreviewScene, spiritPreviewCamera); + // Nur weiter machen, wenn das Overlay sichtbar ist! + let el = document.getElementById('spirit-info'); + if (el && el.style.display !== 'none') { + requestAnimationFrame(previewRenderLoop); + } +} + // Mouse-Picking (zentral) const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2();