An error occurred while loading the file. Please try again.
  • Cantuerk's avatar
    Update public/assets/models/calisthenics_park_model/scene.bin,... · 0d145787
    Cantuerk authored
    Update public/assets/models/calisthenics_park_model/scene.bin, public/assets/models/calisthenics_park_model/scene.gltf, public/assets/models/calisthenics_park_model/license.txt, public/assets/models/calisthenics_park_model/textures/Palette256_baseColor.png, public/index.html, public/previewImages/calisthenics_park.png
    0d145787
index.html 26.05 KiB
<!doctype html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>GeoVis AR Projekt</title>
  <style>
    body {
      margin: 0;
      font-family: Arial, sans-serif;
      background-color: #f0f0f0;
      color: #333;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-image: url('https://www.hft-stuttgart.de/fileadmin/Dateien/Hochschule/-_R_Juergen_Pollak_HFT_18.04.18-0091.jpg');
      background-size: cover;
      background-position: center;
    .container {
      text-align: center;
      background-color: rgba(255, 255, 255, 0.9);
      padding: 50px;
      border-radius: 10px;
      color: #333;
      max-width: 80%;
      box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2);
    h1 {
      font-size: 3em;
      margin-bottom: 20px;
      color: #444;
    p {
      font-size: 1.2em;
      margin-bottom: 30px;
    #add-menu {
      overflow-y: auto;
    #menu-bar,
    .menu-placeholder {
      position: absolute;
      bottom: 0;
      width: 100%;
      height: 80px;
      display: flex;
      justify-content: space-around;
      align-items: center;
      background: #121212;
      padding: 10px;
      box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.8);
      color: white;
      z-index: 10;
      border-top: 1px solid #2a2a2a;
    .menu-item {
      background: #2c2c2c;
      border-radius: 20px;
      padding: 8px;
      transition: background-color 0.3s, transform 0.2s;
} .menu-item:hover { background-color: #3a3a3a; transform: scale(1.1); } .menu-item img { width: 50px; height: 50px; } .menu-placeholder .menu-item img { width: 50px; height: 50px; } button { background-color: #4CAF50; color: white; font-size: 1em; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease-in-out, transform 0.2s; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin: 10px; } button:hover { background-color: #45a049; } button:active { background-color: #387a39; } /* Confirmation Dialog */ #confirmation-dialog, #delete-confirmation-dialog, #info-dialog { position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; z-index: 20; } .dialog-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); } .dialog-box { position: relative; background: #2c2c2c; padding: 20px; border-radius: 10px; text-align: center; color: white; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.8);
} .dialog-box p { margin-bottom: 20px; font-size: 1.2em; color: #f0f0f0; } .dialog-box button { margin: 5px; padding: 10px 20px; font-size: 16px; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease, transform 0.2s; } .dialog-box button:first-child { background-color: #e74c3c; color: white; } .dialog-box button:first-child:hover { background-color: #c0392b; transform: scale(1.05); } .dialog-box button:last-child { background-color: #3498db; color: white; } .dialog-box button:last-child:hover { background-color: #2980b9; transform: scale(1.05); } #dynamic-menu { position: absolute; bottom: 80px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px; background: #1e1e1e; box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.8); color: white; width: 100vw; height: 200px; z-index: 20; overflow-y: auto; border-radius: 8px; } #dynamic-menu input[type="range"] { width: 100%; margin: 10px auto; background: #3a3a3a; border-radius: 5px; } #dynamic-menu input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px;
background: #fff; border: 2px solid #555; border-radius: 50%; cursor: pointer; } </style> <script src="https://unpkg.com/three@0.126.0/build/three.js"></script> <script src="https://unpkg.com/three@0.126.0/examples/js/loaders/GLTFLoader.js"></script> </head> <body> <!-- Debug --> <div id="debug-container" style=" position: fixed; top: 0; left: 0; width: 100%; z-index: 9999; font-family: monospace; font-size: 12px; display: flex; flex-direction: column; align-items: flex-start; "> <div id="debug-console" style=" width: 100%; max-height: 200px; overflow-y: auto; background: rgba(0, 0, 0, 0.7); color: #00ff00; padding: 5px; display: none; "></div> <button id="debug-toggle-btn" style=" background: rgba(0, 0, 0, 0.7); color: #00ff00; font-size: 14px; border: none; padding: 5px 10px; cursor: pointer; display: none; "></button> </div> <!-- Standardmenü --> <div id="menu-bar" style="display: none;"> <div class="menu-item" id="add-section" onclick="showMenu('add-menu')"> <img src="previewImages/add-icon.png" alt="Hinzufügen" /> </div> <div class="menu-item" onclick="showMenu('map-window')"> <img src="previewImages/map-icon.png" alt="Karte" /> </div> <div class="menu-item" id="options-section" onclick="showMenu('options-menu')"> <img src="previewImages/options-icon.png" alt="Optionen" /> </div> </div> <!-- Hinzufügen-Menü --> <div id="add-menu" class="menu-placeholder" style="display: none;"></div> <!-- Bearbeiten-Menü --> <div id="edit-menu" class="menu-placeholder" style="display: none;"> <div class="menu-item" onclick="openRotationMenu()"> <img src="previewImages/rotate-icon.png" alt="Rotation" /> </div> <div class="menu-item" onclick="openScaleMenu()"> <img src="previewImages/scale-icon.png" alt="Skalierung" /> </div> <div class="menu-item" onclick="deleteModel()"> <img src="previewImages/delete-icon.png" alt="Löschen" />
</div> <div class="menu-item" onclick="completeEditing()"> <img src="previewImages/check-icon.png" alt="Fertigstellen" /> </div> </div> <!-- Optionen-Menü --> <div id="options-menu" class="menu-placeholder" style="display: none;"> <div class="menu-item" onclick="exitAR()"> <img src="previewImages/exit-icon.png" alt="Beenden" /> </div> <div class="menu-item" onclick="showMenu('menu-bar')"> <img src="previewImages/back-icon.png" alt="Zurück" /> </div> </div> <!-- Dynamisches Menü --> <div id="dynamic-menu" style="display: none;"></div> <!-- Bestätigungsdialog für Löschen --> <div id="delete-confirmation-dialog" style="display: none;"> <div class="dialog-overlay"></div> <div class="dialog-box"> <p id="delete-confirmation-text">Möchten Sie das Modell wirklich löschen?</p> <button onclick="confirmDelete(true)">Ja</button> <button onclick="confirmDelete(false)">Nein</button> </div> </div> <!-- Bestätigungsdialog für Beenden --> <div id="confirmation-dialog" style="display: none;"> <div class="dialog-overlay"></div> <div class="dialog-box"> <p>Möchten Sie die AR-Session wirklich verlassen?</p> <button onclick="confirmExit(true)">Ja</button> <button onclick="confirmExit(false)">Nein</button> </div> </div> <!-- Informations-Dialog --> <div id="info-dialog" style="display: none;"> <div class="dialog-overlay"></div> <div class="dialog-box"> <p id="info-text">Hier kommt die Nachricht hin</p> <button onclick="closeInfoDialog()">OK</button> </div> </div> <div id="map-window" class="menu-placeholder" style="display: none; flex-direction: column; width: 100vw; height: 100vh;"> <div id="map-container" style="z-index: 1500; flex: 15; width: 100%; height: 100%;"></div> <div class="menu-item" onclick="showMenu('menu-bar')" style="cursor: pointer; flex: 1; align-items: center; justify-content: center;"> <img src="previewImages/back-icon.png" alt="Zurück" style="width: 30px; height: 30px;" /> </div> </div> <script src="ar_debug_console.js"></script> <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" /> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <!-- Leaflet einbinden --> <script src="ar_overviewmap.js"></script> <!-- Audio-Element für Button-Klick --> <audio id="button-sound" src="sounds/button-sound.mp3" preload="auto"></audio> <script> // Variablen let selectedModel = 'robot';
let selectedPlacedModel = null; let currentSession = null; let reticle = null; let scene, camera; let models = { bench: { name: "Bench", image: "previewImages/bench.PNG", file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/bench_model/scene.gltf", scale: { x: 0.1, y: 0.1, z: 0.1 }, minScale: 0.05, // 50% der aktuellen Größe maxScale: 0.5 // 500% der aktuellen Größe }, trashbin: { name: "Trash bin", image: "previewImages/trash_can.PNG", file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/trash_model/scene.gltf", scale: { x: 0.03, y: 0.03, z: 0.03 }, minScale: 0.01, // 50% der aktuellen Größe maxScale: 0.1 // 500% der aktuellen Größe }, telephone_box: { name: "Telephone Box", image: "previewImages/telephone_box.PNG", file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/telephone_box_model/scene.gltf", scale: { x: 0.5, y: 0.5, z: 0.5 }, minScale: 0.05, maxScale: 1 }, fire_hydrant_model: { name: "Fire Hydrant", image: "previewImages/hydrant.PNG", file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/fire_hydrant_model/scene.gltf", scale: { x: 0.3, y: 0.3, z: 0.3 }, minScale: 0.1, maxScale: 1 }, statue: { name: "Statue", image: "previewImages/statue.PNG", file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/statue_model/scene.gltf", scale: { x: 0.5, y: 0.5, z: 0.5 }, minScale: 0.05, maxScale: 2 }, fountain: { name: "Fountain", image: "previewImages/fountain.PNG", file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/fountain_model/scene.gltf", scale: { x: 0.001, y: 0.001, z: 0.001 }, minScale: 0.0005, maxScale: 0.005 }, lantern: { name: "Lantern", image: "previewImages/lantern.jpg", file: "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/refs/heads/main/Models/Lantern/glTF/Lantern.gltf", scale: { x: 0.15, y: 0.15, z: 0.15 }, minScale: 0.05, maxScale: 0.3 } calisthenics_park: { name: "Calisthenics Park", image: "previewImages/calisthenics_park.png", file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/calisthenics_park_model/scene.gltf", scale: { x: 0.15, y: 0.15, z: 0.15 }, minScale: 0.05, maxScale: 0.3 }
}; const menus = ['menu-bar', 'add-menu', 'edit-menu', 'options-menu', 'map-window']; window.onload = () => { initializeAddMenu(); // Fügt Sound zu allen Buttons hinzu const buttons = document.querySelectorAll("button, .menu-item"); buttons.forEach(button => { button.addEventListener("click", playButtonSound); }); }; function initializeAddMenu() { const addMenu = document.getElementById('add-menu'); addMenu.innerHTML = Object.entries(models) .map( ([key, model]) => ` <div class="menu-item" id="${key}-item" onclick="selectModel('${key}')"> <img src="${model.image}" alt="${model.name}" /> </div> ` ) .join('') + ` <div class="menu-item" onclick="showMenu('menu-bar')"> <img src="previewImages/back-icon.png" alt="Zurück" /> </div> `; } function showMenu(menuId) { menus.forEach(id => { document.getElementById(id).style.display = id === menuId ? 'flex' : 'none'; }); closeDynamicMenu(); if (menuId === 'menu-bar') clearSelectedModel(); else if (menuId === 'map-window') init_map(); } function clearSelectedModel() { if (selectedPlacedModel) { selectedPlacedModel.traverse((child) => { if (child.isMesh) { child.material.emissive.setHex(0x000000); // Markierung entfernen } }); selectedPlacedModel = null; // Kein Modell mehr ausgewählt } } function selectModel(modelId) { const model = models[modelId]; if (model && model.file) { loadModel(model.file); showMenu('menu-bar'); } } function loadModel(filePath) { const modelConfig = Object.values(models).find(model => model.file === filePath); const loader = new THREE.GLTFLoader(); loader.load( filePath, (gltf) => { const model = gltf.scene; if (modelConfig && modelConfig.scale) { model.scale.set(modelConfig.scale.x, modelConfig.scale.y, modelConfig.scale.z); }
model.position.copy(reticle.position); scene.add(model); // Speichere das platzierte Modell und die Konfiguration selectedPlacedModel = model; selectedPlacedModel.modelConfig = modelConfig; highlightSelectedModel(); showMenu('edit-menu'); }, undefined, (error) => { console.error("Fehler beim Laden des Modells:", error); } ); } function highlightSelectedModel() { if (selectedPlacedModel) { removeHighlightFromAllModels(); selectedPlacedModel.traverse((child) => { if (child.isMesh) { child.material.emissive.setHex(0xff0000); // Rote Hervorhebung } }); } } function removeHighlightFromAllModels() { scene.traverse((child) => { if (child.isMesh && child.material && child.material.emissive) { child.material.emissive.setHex(0x000000); // Markierung entfernen } }); } function removeHighlightFromSelectedModel() { if (selectedPlacedModel) { selectedPlacedModel.traverse((child) => { if (child.isMesh) child.material.emissive.setHex(0x000000); // Markierung entfernen }); } } function selectModelFromScene(event) { const mouse = new THREE.Vector2( (event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1 ); const raycaster = new THREE.Raycaster(); raycaster.setFromCamera(mouse, camera); // Prüfe Kollisionen mit Objekten in der Szene const intersects = raycaster.intersectObjects(scene.children, true); if (intersects.length > 0) { // Finde das Hauptobjekt (Root-Parent), falls Mesh ausgewählt wurde let selectedObject = intersects[0].object; while (selectedObject.parent && selectedObject.parent !== scene) { selectedObject = selectedObject.parent; } // Markiere das gesamte Modell als ausgewählt selectedPlacedModel = selectedObject; highlightSelectedModel(); showMenu("edit-menu"); } }
function completeEditing() { removeHighlightFromSelectedModel(); closeDynamicMenu(); selectedPlacedModel = null; document.getElementById('edit-menu').style.display = 'none'; document.getElementById('menu-bar').style.display = 'flex'; } function openRotationMenu() { if (!selectedPlacedModel) { showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); return; } // Holen Sie die aktuelle Y-Rotation des Modells (in Grad) const currentRotation = Math.round(THREE.MathUtils.radToDeg(selectedPlacedModel.rotation.y)); const dynamicMenu = document.getElementById("dynamic-menu"); dynamicMenu.style.display = "flex"; dynamicMenu.innerHTML = ` <h3>Rotation anpassen</h3> <label>Y-Achse: <span id="current-rotation">${currentRotation}</span>°<input type="range" min="0" max="360" step="10" onchange="updateRotation('y', this.value)"></label> <button onclick="closeDynamicMenu()">Zurück</button> `; } function updateRotation(axis, value) { if (selectedPlacedModel) { const radians = (value / 180) * Math.PI; selectedPlacedModel.rotation[axis] = radians; // Update der aktuellen Rotation im Menü const currentRotationDisplay = document.getElementById("current-rotation"); if (currentRotationDisplay) { currentRotationDisplay.textContent = value; // Zeige den aktuellen Wert in Grad an } } } function calculateBoundingBox(object) { const box = new THREE.Box3().setFromObject(object); const size = new THREE.Vector3(); box.getSize(size); return size; } function calculateMaxScale(object) { const boundingBox = calculateBoundingBox(object); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Berechne die maximal mögliche Skalierung, um im Viewport zu bleiben const scaleWidth = viewportWidth / boundingBox.x; const scaleHeight = viewportHeight / boundingBox.y; // Wähle den kleineren Wert und reduziere ihn leicht, um sicherzugehen, dass das Objekt nicht über den Rand hinausgeht const safeScaleFactor = 0.95; // Puffer, um sicherzustellen, dass es nicht zu groß wird return Math.min(scaleWidth, scaleHeight) * safeScaleFactor; } /** function openScaleMenu() { if (!selectedPlacedModel) { showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); return; }
// Berechne die maximale Skalierung für das spezifische Objekt const maxScale = calculateMaxScale(selectedPlacedModel); // Aktuelle Skalierung des Modells bestimmen const currentScale = selectedPlacedModel.scale.x; const dynamicMenu = document.getElementById("dynamic-menu"); dynamicMenu.style.display = "flex"; dynamicMenu.innerHTML = ` <h3>Skalierung anpassen</h3> <label>Größe: <span id="scale-value">${currentScale.toFixed(2)}</span><input type="range" min="0.01" max="${maxScale.toFixed(2)}" step="0.0001" value="${currentScale}" onchange="updateScale(this.value)"></label> <button onclick="closeDynamicMenu()">Zurück</button> `; } * */ function openScaleMenu() { if (!selectedPlacedModel) { showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."); return; } // Berechne die maximale Skalierung für das spezifische Objekt // const maxScale = calculateMaxScale(selectedPlacedModel); // Aktuelle Skalierung des Modells bestimmen const currentScale = selectedPlacedModel.scale.x; const minScale = selectedPlacedModel.modelConfig.minScale; const maxScale = selectedPlacedModel.modelConfig.maxScale; const step = (maxScale - minScale) / 100; // Dynamische Schrittgröße basierend auf Grenzen console.log("Slider-Werte:", { minScale, maxScale, currentScale }); const dynamicMenu = document.getElementById("dynamic-menu"); dynamicMenu.style.display = "flex"; dynamicMenu.innerHTML = ` <h3>Skalierung anpassen</h3> <label>Größe: <span id="scale-value">${currentScale.toFixed(2)}</span> <input type="range" min="${minScale}" max="${maxScale}" step="${step}" value="${currentScale}" onchange="updateScale(this.value)"></label> <button onclick="closeDynamicMenu()">Zurück</button> `; } function updateScale(value) { if (selectedPlacedModel) { const scale = parseFloat(value); selectedPlacedModel.scale.set(scale, scale, scale); // Anzeige des aktuellen Wertes aktualisieren const scaleValueDisplay = document.getElementById("scale-value"); if (scaleValueDisplay) { scaleValueDisplay.textContent = `${scale.toFixed(2)}`; } } } function deleteModel() { if (!selectedPlacedModel) { showInfoDialog("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es löschen."); return; } // Dialog anzeigen const deleteDialog = document.getElementById('delete-confirmation-dialog'); deleteDialog.style.display = 'flex'; }
function confirmDelete(shouldDelete) { const deleteDialog = document.getElementById('delete-confirmation-dialog'); deleteDialog.style.display = 'none'; if (shouldDelete && selectedPlacedModel) { scene.remove(selectedPlacedModel); selectedPlacedModel = null; showMenu('menu-bar'); } } function closeDynamicMenu() { const dynamicMenu = document.getElementById("dynamic-menu"); dynamicMenu.style.display = "none"; } function refreshMapDialog() { const mapDialog = document.getElementById('map-dialog'); mapDialog.style.display = 'flex'; } function closeMapDialog() { const mapDialog = document.getElementById('map-dialog'); mapDialog.style.display = 'none'; } async function activateXR() { const canvas = document.createElement('canvas'); document.body.appendChild(canvas); const gl = canvas.getContext('webgl', { xrCompatible: true }); const renderer = new THREE.WebGLRenderer({ alpha: true, canvas, context: gl }); renderer.autoClear = false; scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(); camera.matrixAutoUpdate = false; const light = new THREE.DirectionalLight(0xffffff, 1); light.position.set(10, 10, 10); scene.add(light); const loader = new THREE.GLTFLoader(); loader.load("https://immersive-web.github.io/webxr-samples/media/gltf/reticle/reticle.gltf", (gltf) => { reticle = gltf.scene; reticle.visible = false; scene.add(reticle); }); currentSession = await navigator.xr.requestSession('immersive-ar', { optionalFeatures: ["dom-overlay"], domOverlay: { root: document.body }, requiredFeatures: ['hit-test'] }); currentSession.updateRenderState({ baseLayer: new XRWebGLLayer(currentSession, gl) }); const referenceSpace = await currentSession.requestReferenceSpace('local'); const viewerSpace = await currentSession.requestReferenceSpace('viewer'); const hitTestSource = await currentSession.requestHitTestSource({ space: viewerSpace }); document.getElementById('menu-bar').style.display = 'flex'; currentSession.addEventListener("end", () => { currentSession = null; menus.forEach(id => { document.getElementById(id).style.display = 'none'; }); }); canvas.addEventListener("pointerdown", selectModelFromScene);
currentSession.requestAnimationFrame(function onXRFrame(time, frame) { currentSession.requestAnimationFrame(onXRFrame); gl.bindFramebuffer(gl.FRAMEBUFFER, currentSession.renderState.baseLayer.framebuffer); const pose = frame.getViewerPose(referenceSpace); if (pose) { const view = pose.views[0]; const viewport = currentSession.renderState.baseLayer.getViewport(view); renderer.setSize(viewport.width, viewport.height); camera.matrix.fromArray(view.transform.matrix); camera.projectionMatrix.fromArray(view.projectionMatrix); camera.updateMatrixWorld(true); const hitTestResults = frame.getHitTestResults(hitTestSource); if (hitTestResults.length > 0) { const hitPose = hitTestResults[0].getPose(referenceSpace); reticle.visible = true; reticle.position.set(hitPose.transform.position.x, hitPose.transform.position.y, hitPose.transform.position.z); reticle.updateMatrixWorld(true); } renderer.render(scene, camera); } }); } function exitAR() { document.getElementById('confirmation-dialog').style.display = 'flex'; } function confirmExit(shouldExit) { if (shouldExit && currentSession) { currentSession.end(); } document.getElementById('confirmation-dialog').style.display = 'none'; } let soundTimeout = false; function playButtonSound() { if (!soundTimeout) { const sound = document.getElementById("button-sound"); sound.currentTime = 0; sound.play(); soundTimeout = true; setTimeout(() => { soundTimeout = false; }, 200); // Verzögerung von 200ms } } if (navigator.xr) { const startButton = document.createElement('button'); startButton.textContent = 'Start AR'; startButton.style.cssText = "position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 15px; font-size: 18px;"; document.body.appendChild(startButton); startButton.onclick = () => { startButton.remove(); activateXR(); }; } else { alert('WebXR wird nicht unterstützt.'); } </script> </body>
</html>