diff --git a/public/ar_download.js b/public/ar_download.js new file mode 100644 index 0000000000000000000000000000000000000000..8ea47a6f9d3b767d79e728fd2df4f7a367087b5c --- /dev/null +++ b/public/ar_download.js @@ -0,0 +1,73 @@ +function downloadScene() { + const placedModels = getAllPlacedModels().map(model => { + const { lat, lng } = threeToLeaflet(model.position.x, model.position.z); + return { + name: model.modelConfig.name, + position: model.position, + rotation: model.rotation, + scale: model.scale, + lat: lat, + lng: lng, + } + }); + + console.log("Modelle: " + placedModels.length) + if (placedModels.length === 0) { + showInfoDialog("Szene ist leer"); + return; + } + + if (!geoLocation) { + console.log("Standort nicht geladen"); + showInfoDialog("Ihr Standort wurde noch nicht geladen"); + return; + } + + const data = { + lat: geoLocation.latitude, + lng: geoLocation.longitude, + models: placedModels, + } + + const sceneData = JSON.stringify(data, null, 1); + const blob = new Blob([sceneData], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "scene.json"; + a.style.display = "none"; + + document.body.appendChild(a); + a.click(); + + URL.revokeObjectURL(url); + document.body.removeChild(a); +} + +function loadSceneFromFile(loadedData) { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'application/json'; + + fileInput.addEventListener('change', function(event) { + const file = event.target.files[0]; + const fileName = file ? file.name : "Keine Datei ausgewählt" + if (!file) { + console.log("Keine Datei ausgewählt"); + return; + } + + const reader = new FileReader(); + reader.onload = function(e) { + try { + const sceneData = JSON.parse(e.target.result); + loadedData(sceneData, fileName); + } catch (error) { + console.error("Fehler beim Verarbeiten der Datei:", error); + } + }; + + reader.readAsText(file); + }); + fileInput.click(); +} \ No newline at end of file diff --git a/public/ar_main.js b/public/ar_main.js new file mode 100644 index 0000000000000000000000000000000000000000..51f038738b059c8dc8e2e11bceda62a09c9bbde0 --- /dev/null +++ b/public/ar_main.js @@ -0,0 +1,520 @@ + // Variablen + let selectedModel = 'robot'; + let selectedPlacedModel = null; + let currentSession = null; + let reticle = null; + let scene, camera; + let geoLocation; + + let models = { + bench: { + name: "Bench", + image: "previewImages/bench.PNG", + file: "/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: "/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: "/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: "/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: "/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: "/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 + } + }; + + 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) { + const isMapWindow = menuId === 'map-window'; + if (isMapWindow && !geoLocation) { + console.log("Standort nicht geladen"); + showInfoDialog("Ihr Standort wurde noch nicht geladen"); + return; + } + + menus.forEach(id => { + document.getElementById(id).style.display = id === menuId ? 'flex' : 'none'; + }); + closeDynamicMenu(); + if (menuId === 'menu-bar') clearSelectedModel(); + else if (isMapWindow) 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) { + placeModel(filePath, reticle.position, (model) => { + selectedPlacedModel = model; + highlightSelectedModel(); + showMenu('edit-menu'); + }); + } + + function placeModel(filePath, position, onPlace = null) { + 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(position); + model.isPlacedModel = true; + model.modelConfig = modelConfig; + scene.add(model); + + if (onPlace) { + onPlace(model); + } + }, + 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 getAllPlacedModels() { + return scene.children.filter(child => child.isPlacedModel === true); + } + + 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(sceneData = null) { + 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); + + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(position => { + geoLocation = { + latitude: roundTo(position.coords.latitude, 5), + longitude: roundTo(position.coords.longitude, 5), + }; + console.log("GeoLocation: " + JSON.stringify(geoLocation)); + if (sceneData) { + sceneData.models.forEach((model) => { + const offSet = leafletToThree(sceneData.lat, sceneData.lng) + if (model.name) { + const filePath = Object.values(models).find(m => model.name === m.name).file; + const { x, z } = leafletToThree(model.lat, model.lng); + const positionVector = new THREE.Vector3(x, model.position.y, z); + placeModel(filePath, positionVector, (placed) => { + if (model.rotation) { + placed.rotation._x = model.rotation._x; + placed.rotation._y = model.rotation._y; + placed.rotation._z = model.rotation._z; + } + if (model.scale) { + placed.scale.x = model.scale.x; + placed.scale.y = model.scale.y; + placed.scale.z = model.scale.z; + } + }); + } + }); + } + }, function(error) { + console.error("Fehler bei der Geolokalisierung:", JSON.stringify(error)); + }, {enableHighAccuracy: true, maximumAge: 2000, timeout: 5000}); + } + + 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 + } + } \ No newline at end of file diff --git a/public/ar_overviewmap.js b/public/ar_overviewmap.js index 2db762ee2505031d8826bd0d714f897ff027b752..c7e0e47b3814f9de5b18d526ae8200dd9bc40d3c 100644 --- a/public/ar_overviewmap.js +++ b/public/ar_overviewmap.js @@ -1,70 +1,127 @@ - - +let init = false; +let mymap; +let userIcon; +let intervalId; +let modeMarkers = [] +const earthRadius = 6378137; // Erdradius in Metern (WGS84) function init_map() { - const mapContainer = document.getElementById('map-container'); + const mapContainer = document.getElementById("map-container"); if (mapContainer) { - var mymap = L.map(mapContainer); - var OpenStreetMap_DE = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { - maxZoom: 30, - minZoom: 15, - attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' - }); - // select one basemap from https://leaflet-extras.github.io/leaflet-providers/preview/ - var CartoDB_Positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { - attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', - subdomains: 'abcd', - maxZoom: 30, - minZoom: 15 + if (!init) { + mymap = L.map(mapContainer); + var OpenStreetMap_DE = L.tileLayer('https://{s}.tile.openstreetmap.de/tiles/osmde/{z}/{x}/{y}.png', { + maxZoom: 30, + minZoom: 15, + attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' + }); + // select one basemap from https://leaflet-extras.github.io/leaflet-providers/preview/ + var CartoDB_Positron = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { + attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', + subdomains: 'abcd', + maxZoom: 30, + minZoom: 15 + }); + CartoDB_Positron.addTo(mymap); + + userIcon = L.icon({ + iconUrl: 'previewImages/map-user.png', + iconSize: [32, 32], + iconAnchor: [16, 32], + popupAnchor: [0, -32] + }); + + mymap.setView([geoLocation.latitude, geoLocation.longitude], 20) + userIcon = L.marker([geoLocation.latitude, geoLocation.longitude], { icon: userIcon }).bindPopup('Ihre Position').addTo(mymap); + init = true; + } + + modeMarkers.forEach(marker => { + marker.remove(); }); - CartoDB_Positron.addTo(mymap); - - // Marker initialisieren - var userIcon = L.icon({ - iconUrl: 'previewImages/map-user.png', // Dein Männchen-Bild - iconSize: [32, 32], // Die Größe des Icons - iconAnchor: [16, 32], // Der Punkt des Markers, der auf der Position zeigt - popupAnchor: [0, -32] // Die Position des Popups relativ zum Marker + const models = getAllPlacedModels(); + models.forEach(model => { + const { lat, lng } = threeToLeaflet(model.position.x, model.position.z); + const modelMarker = L.marker([lat, lng], { icon: L.icon({ + iconUrl: model.modelConfig.image, + iconSize: [16, 16], + iconAnchor: [16, 32], + popupAnchor: [0, -32] + })}).addTo(mymap).bindPopup(model.modelConfig?.name || "Unbenannt"); + modeMarkers.push(modelMarker); }); + intervalId = setInterval(updateMap, 500); + } + } - // Marker initialisieren - var userMarker = L.marker([0, 0], { icon: userIcon }).bindPopup('Ihre Position').addTo(mymap); - let initPosition = false; - let lastPosition = { lat: 0, lng: 0 }; // Letzte bekannte Position - const updateThreshold = 1.5; // Schwellenwert für den Abstand (in Metern) - - // Funktion, um die Position zu aktualisieren - function updatePosition(position) { - const userLat = position.coords.latitude; - const userLng = position.coords.longitude; - - const currentPosition = L.latLng(userLat, userLng); - const lastKnownPosition = L.latLng(lastPosition.lat, lastPosition.lng); - const distance = currentPosition.distanceTo(lastKnownPosition); - - // Nur aktualisieren, wenn der Abstand größer als der Schwellenwert ist - if (distance > updateThreshold || !initPosition) { - if (!initPosition) { - mymap.setView([userLat, userLng], 20); // Zentrische Ansicht beim ersten Mal - initPosition = true; - } - - // Marker auf die neue Position setzen - userMarker.setLatLng([userLat, userLng]); - console.log(`Setze Position auf lat=${userLat} lng=${userLng}, Abstand: ${distance}m`); - lastPosition = { lat: userLat, lng: userLng }; - } - } + function updateMap() { + if (document.getElementById("map-window").style.display === 'none' && intervalId) { + clearInterval(intervalId); + intervalId = null; + return; + } - // Die Position alle 5 Sekunden aktualisieren - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(updatePosition); - navigator.geolocation.watchPosition(updatePosition, function(error) { - console.error("Fehler bei der Geolokalisierung:", error); - }, {enableHighAccuracy: true, maximumAge: 2000,timeout: 5000}); - } else { - console.error("Geolocation wird nicht unterstützt."); - } + let vec = new THREE.Vector3(); + const userPosition = camera.getWorldPosition(vec); + const { lat, lng } = threeToLeaflet(userPosition.x, userPosition.z); + + if (userIcon) { + userIcon.setLatLng([lat, lng]); } } + + + + function threeToLeaflet(threeX, threeZ) { + if (!geoLocation) { + console.error("GeoLocation ist noch nicht verfügbar"); + return; + } + + const originLat = geoLocation.latitude; + const originLng = geoLocation.longitude; + + const deltaLat = (threeZ / earthRadius) * (180 / Math.PI); + const deltaLng = (threeX / (earthRadius * Math.cos(Math.PI * originLat / 180))) * (180 / Math.PI); + + const globalLat = originLat + deltaLat; + const globalLng = originLng + deltaLng; + return { lat: globalLat, lng: globalLng }; +} + +function leafletToThree(lat, lng) { + if (!geoLocation) { + console.error("GeoLocation ist noch nicht verfügbar"); + return; + } + + const originLat = geoLocation.latitude; + const originLng = geoLocation.longitude; + + const deltaLat = lat - originLat; + const deltaLng = lng - originLng; + + const threeZ = deltaLat * (Math.PI / 180) * earthRadius; + const threeX = deltaLng * (Math.PI / 180) * earthRadius * Math.cos(Math.PI * originLat / 180); + + return { x: threeX, z: threeZ }; +} + +function roundTo(n, digits) { + var negative = false; + if (digits === undefined) { + digits = 0; + } + if (n < 0) { + negative = true; + n = n * -1; + } + var multiplicator = Math.pow(10, digits); + n = parseFloat((n * multiplicator).toFixed(11)); + n = (Math.round(n) / multiplicator).toFixed(digits); + if (negative) { + n = (n * -1).toFixed(digits); + } + return parseFloat(n); +} \ No newline at end of file diff --git a/public/ar_start.js b/public/ar_start.js new file mode 100644 index 0000000000000000000000000000000000000000..f537a70fd2a5c83a87eb210fcdcff68be681147c --- /dev/null +++ b/public/ar_start.js @@ -0,0 +1,29 @@ +if (navigator.xr) { + let sceneData; + const startButton = document.createElement('button'); + startButton.textContent = 'Start AR'; + startButton.style.cssText = "position: fixed; top: 45%; left: 50%; transform: translate(-50%, -50%); padding: 15px; font-size: 18px;"; + document.body.appendChild(startButton); + startButton.onclick = () => { + startButton.remove(); + loadButton.remove(); + activateXR(sceneData); + }; + + const loadButton = document.createElement('button'); + loadButton.textContent = 'Umgebung Laden'; + loadButton.style.cssText = "position: fixed; top: 55%; left: 50%; transform: translate(-50%, -50%); padding: 15px; font-size: 18px;"; + document.body.appendChild(loadButton); + loadButton.onclick = () => { + loadSceneFromFile((data, fileName) => { + if (data && fileName) { + loadButton.textContent = fileName; + sceneData = data + } else { + loadButton.textContent = 'Umgebung Laden'; + } + }); + }; + } else { + alert('WebXR wird nicht unterstützt.'); + } \ No newline at end of file diff --git a/public/index.html b/public/index.html index 8686486d233b0c58c409741d760454008ba0030d..f297f1b57a0b6c50f6c2f9ab78990f3e6db07424 100644 --- a/public/index.html +++ b/public/index.html @@ -5,215 +5,7 @@ <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> + <link rel="stylesheet" href="styles.css"> <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> @@ -289,6 +81,9 @@ <div class="menu-item" onclick="exitAR()"> <img src="previewImages/exit-icon.png" alt="Beenden" /> </div> + <div class="menu-item" onclick="downloadScene()"> + <img src="previewImages/download-icon.png" alt="Download" /> + </div> <div class="menu-item" onclick="showMenu('menu-bar')"> <img src="previewImages/back-icon.png" alt="Zurück" /> </div> @@ -336,498 +131,16 @@ </div> - + <script src="ar_start.js"></script> + <script src="ar_main.js"></script> <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> + <script src="ar_download.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 - } - }; - - 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> diff --git a/public/previewImages/download-icon.png b/public/previewImages/download-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..663c90144dd713cc4abddfb476d1dfa428571231 Binary files /dev/null and b/public/previewImages/download-icon.png differ diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..b34fa5f74f9f0bebbba193416f507528d616fa42 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,207 @@ +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; + } \ No newline at end of file