From 67715bc332b6018518754149ff5e8b34fefd18b5 Mon Sep 17 00:00:00 2001
From: Cantuerk <21caog1bif@hft-stuttgart.de>
Date: Tue, 21 Jan 2025 14:26:53 +0000
Subject: [PATCH] Update public/ar_main.js

---
 public/ar_main.js | 730 +++++++++++++++++++++++-----------------------
 1 file changed, 370 insertions(+), 360 deletions(-)

diff --git a/public/ar_main.js b/public/ar_main.js
index 690286d..2b2f484 100644
--- a/public/ar_main.js
+++ b/public/ar_main.js
@@ -1,93 +1,93 @@
-    /* ========================= */
-    /*    GLOBALE VARIABLEN      */
-    /* ========================= */
-    let selectedPlacedModel = null;
-    let currentSession = null;
-    let reticle = null;
-    let scene, camera;
-    let geoLocation;
-    const menus = ['menu-bar', 'add-menu', 'edit-menu', 'options-menu', 'map-window'];
-    
-    /* ========================= */
-    /*          MODELLE          */
-    /* ========================= */
-    let models = {
-      bench: {
+/* ========================= */
+/*    GLOBALE VARIABLEN      */
+/* ========================= */
+let selectedPlacedModel = null;
+let currentSession = null;
+let reticle = null;
+let scene, camera;
+let geoLocation;
+const menus = ['menu-bar', 'add-menu', 'edit-menu', 'options-menu', 'map-window'];
+
+/* ========================= */
+/*          MODELLE          */
+/* ========================= */
+let models = {
+    bench: {
         name: "Bench",
-        image: "previewImages/bench.PNG",
+        image: "assets/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,
         maxScale: 0.5
-      },
-      trashbin: {
+    },
+    trashbin: {
         name: "Trash bin",
-        image: "previewImages/trash_can.PNG",
+        image: "assets/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,
         maxScale: 0.1
-      },
-      lantern: {
+    },
+    lantern: {
         name: "Lantern",
-        image: "previewImages/park_light.png",
+        image: "assets/previewImages/park_light.png",
         file: "https://transfer.hft-stuttgart.de/gitlab/geovistoogsi/ar/-/raw/master/public/assets/models/park_light_model/scene.gltf",
         scale: { x: 0.5, y: 0.5, z: 0.5 },
         minScale: 0.2,
         maxScale: 5
-      },
-      telephone_box: {
+    },
+    telephone_box: {
         name: "Telephone Box",
-        image: "previewImages/telephone_box.PNG",
+        image: "assets/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: {
+    },
+    fire_hydrant_model: {
         name: "Fire Hydrant",
-        image: "previewImages/hydrant.PNG",
+        image: "assets/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: {
+    },
+    statue: {
         name: "Statue",
-        image: "previewImages/statue.PNG",
+        image: "assets/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: {
+    },
+    fountain: {
         name: "Fountain",
-        image: "previewImages/fountain.PNG",
+        image: "assets/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
-      }
-    };
-
-    /* ========================= */
-    /*     INITIALISIERUNG       */
-    /* ========================= */
-    window.onload = () => {
-      initializeAddMenu();
-
-      // Allen Buttons den Sound hinzufügen
-      const buttons = document.querySelectorAll("button, .menu-item");
-      buttons.forEach(button => {
+    }
+};
+
+/* ========================= */
+/*     INITIALISIERUNG       */
+/* ========================= */
+window.onload = () => {
+    initializeAddMenu();
+
+    // Allen Buttons den Sound hinzufügen
+    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)
+function initializeAddMenu() {
+    const addMenu = document.getElementById('add-menu');
+    addMenu.innerHTML = Object.entries(models)
         .map(
-          ([key, model]) => `
+            ([key, model]) => `
             <div class="menu-item" id="${key}-item" onclick="selectModel('${key}')">
               <img src="${model.image}" alt="${model.name}" />
             </div>
@@ -96,243 +96,243 @@
         .join('') +
         `
         <div class="menu-item" onclick="showMenu('menu-bar')">
-          <img src="previewImages/back-icon.png" alt="Zurück" />
+          <img src="assets/icons/back-icon.png" alt="Zurück" />
         </div>
       `;
-    }
-
-    /* ========================= */
-    /*     MENÜ-STEUERUNG        */
-    /* ========================= */
-    function showMenu(menuId) {
-      const isMapWindow = menuId === 'map-window';
-      if (isMapWindow && !geoLocation) {
+}
+
+/* ========================= */
+/*     MENÜ-STEUERUNG        */
+/* ========================= */
+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 closeDynamicMenu() {
-      const dynamicMenu = document.getElementById("dynamic-menu");
-      dynamicMenu.style.display = "none";
     }
 
-    /* ========================= */
-    /*     MODELL-HANDLING       */
-    /* ========================= */
-    function getAllPlacedModels() {
-      return scene.children.filter(child => child.isPlacedModel === true);
-    }
-
-    function loadModel(filePath) {
-      placeModel(filePath, reticle.position, (model) => {
+    menus.forEach(id => {
+        document.getElementById(id).style.display = id === menuId ? 'flex' : 'none';
+    });
+    closeDynamicMenu();
+    if (menuId === 'menu-bar') clearSelectedModel();
+    else if (isMapWindow) init_map();
+}
+
+function closeDynamicMenu() {
+    const dynamicMenu = document.getElementById("dynamic-menu");
+    dynamicMenu.style.display = "none";
+}
+
+/* ========================= */
+/*     MODELL-HANDLING       */
+/* ========================= */
+function getAllPlacedModels() {
+    return scene.children.filter(child => child.isPlacedModel === true);
+}
+
+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(
+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);
-          }
+            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);
+            console.error("Fehler beim Laden des Modells:", error);
         }
-      );
-    }
+    );
+}
 
-    function selectModel(modelId) {
-      const model = models[modelId];
-      if (model && model.file) {
+function selectModel(modelId) {
+    const model = models[modelId];
+    if (model && model.file) {
         loadModel(model.file);
         showMenu('menu-bar');
-      }
     }
+}
 
-    function selectModelFromScene(event) {
-      const mouse = new THREE.Vector2(
+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);
+    const raycaster = new THREE.Raycaster();
+    raycaster.setFromCamera(mouse, camera);
 
-      // Prüfe Kollisionen mit Objekten in der Szene
-      const intersects = raycaster.intersectObjects(scene.children, true);
+    // Prüfe Kollisionen mit Objekten in der Szene
+    const intersects = raycaster.intersectObjects(scene.children, true);
 
-      if (intersects.length > 0) {
+    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;
+            selectedObject = selectedObject.parent;
         }
 
         // Überprüfe, ob das ausgewählte Objekt das Reticle ist
         if (selectedObject === reticle) {
-          console.log("Reticle kann nicht ausgewählt werden.");
-          return;
+            console.log("Reticle kann nicht ausgewählt werden.");
+            return;
         }
 
         // Markiere das gesamte Modell als ausgewählt
         selectedPlacedModel = selectedObject;
         highlightSelectedModel();
         showMenu("edit-menu");
-      }
     }
+}
 
-    function clearSelectedModel() {
-      if (selectedPlacedModel) {
+function clearSelectedModel() {
+    if (selectedPlacedModel) {
         selectedPlacedModel.traverse((child) => {
-          if (child.isMesh) {
-            child.material.emissive.setHex(0x000000); // Markierung entfernen
-          }
+            if (child.isMesh) {
+                child.material.emissive.setHex(0x000000); // Markierung entfernen
+            }
         });
         selectedPlacedModel = null;
-      }
     }
+}
 
-    function highlightSelectedModel() {
-      if (selectedPlacedModel) {
+function highlightSelectedModel() {
+    if (selectedPlacedModel) {
         removeHighlightFromAllModels();
         selectedPlacedModel.traverse((child) => {
-          if (child.isMesh) {
-            child.material.emissive.setHex(0xff0000); // Rote Hervorhebung
-          }
+            if (child.isMesh) {
+                child.material.emissive.setHex(0xff0000); // Rote Hervorhebung
+            }
         });
-      }
     }
+}
 
-    function removeHighlightFromSelectedModel() {
-      if (selectedPlacedModel) {
+function removeHighlightFromSelectedModel() {
+    if (selectedPlacedModel) {
         selectedPlacedModel.traverse((child) => {
-          if (child.isMesh) child.material.emissive.setHex(0x000000); // Markierung entfernen
+            if (child.isMesh) child.material.emissive.setHex(0x000000); // Markierung entfernen
         });
-      }
     }
+}
 
 
-      function removeHighlightFromAllModels() {
-        scene.traverse((child) => {
-          if (child.isMesh && child.material && child.material.emissive) {
+function removeHighlightFromAllModels() {
+    scene.traverse((child) => {
+        if (child.isMesh && child.material && child.material.emissive) {
             child.material.emissive.setHex(0x000000); // Markierung entfernen
-          }
-        });
-      }
-  
-      function deleteModel() {
-        if (!selectedPlacedModel) {
-          console.log("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es löschen.");
-          return;
         }
-  
-        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';
+function deleteModel() {
+    if (!selectedPlacedModel) {
+        console.log("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es löschen.");
+        return;
+    }
 
-      if (shouldDelete && selectedPlacedModel) {
+    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 completeEditing() {
-      removeHighlightFromSelectedModel();
-      closeDynamicMenu();
-      selectedPlacedModel = null;
-      document.getElementById('edit-menu').style.display = 'none';
-      document.getElementById('menu-bar').style.display = 'flex';
-    }
-
-    /* ========================= */
-    /*    BEARBEITUNGS-MENÜS     */
-    /* ========================= */
-    function openRotationMenu() {
-      if (!selectedPlacedModel) {
+}
+
+function completeEditing() {
+    removeHighlightFromSelectedModel();
+    closeDynamicMenu();
+    selectedPlacedModel = null;
+    document.getElementById('edit-menu').style.display = 'none';
+    document.getElementById('menu-bar').style.display = 'flex';
+}
+
+/* ========================= */
+/*    BEARBEITUNGS-MENÜS     */
+/* ========================= */
+function openRotationMenu() {
+    if (!selectedPlacedModel) {
         console.log("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten.");
         return;
-      }
+    }
 
-      const currentRotation = Math.round(THREE.MathUtils.radToDeg(selectedPlacedModel.rotation.y)); // Aktuelle Y-Rotation des Modells (in Grad)
+    const currentRotation = Math.round(THREE.MathUtils.radToDeg(selectedPlacedModel.rotation.y)); // Aktuelle Y-Rotation des Modells (in Grad)
 
-      const dynamicMenu = document.getElementById("dynamic-menu");
-      dynamicMenu.style.display = "flex";
-      dynamicMenu.innerHTML = `
+    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) {
+function updateRotation(axis, value) {
+    if (selectedPlacedModel) {
         const radians = (value / 180) * Math.PI;
         selectedPlacedModel.rotation[axis] = radians;
 
         // Anzeige der aktuellen Rotation im Dynamic-Menü aktualisieren
         const currentRotationDisplay = document.getElementById("current-rotation");
         if (currentRotationDisplay) {
-          currentRotationDisplay.textContent = value; // Zeige den aktuellen Wert in Grad an
+            currentRotationDisplay.textContent = value; // Zeige den aktuellen Wert in Grad an
         }
-      }
     }
+}
 
-    function openScaleMenu() {
-      if (!selectedPlacedModel) {
+function openScaleMenu() {
+    if (!selectedPlacedModel) {
         console.log("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten.");
         return;
-      }
+    }
 
-      // Aktuelle Skalierung und Grenzen 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
+    // Aktuelle Skalierung und Grenzen 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
 
-      const currentScalePercent = ((currentScale - minScale) / (maxScale - minScale)) * 100; // Umrechnung der Werte in Prozent
+    const currentScalePercent = ((currentScale - minScale) / (maxScale - minScale)) * 100; // Umrechnung der Werte in Prozent
 
-      const dynamicMenu = document.getElementById("dynamic-menu");
-      dynamicMenu.style.display = "flex";
-      dynamicMenu.innerHTML = `
+    const dynamicMenu = document.getElementById("dynamic-menu");
+    dynamicMenu.style.display = "flex";
+    dynamicMenu.innerHTML = `
         <h3>Skalierung anpassen</h3>
         <label>Größe: <span id="scale-value">${currentScalePercent.toFixed(0)}%</span>
-        <input type="range" min="0" max="100" step="1" value="${currentScalePercent}" onchange="updateScale(this.value, ${minScale}, ${maxScale})"></label>
+        <input type="range" min="0" max="100" step=""${step}"" value="${currentScalePercent}" onchange="updateScale(this.value, ${minScale}, ${maxScale})"></label>
         <button onclick="closeDynamicMenu()">Zurück</button>
       `;
-    }
+}
 
-    function updateScale(percentValue, minScale, maxScale) {
-      if (selectedPlacedModel) {
+function updateScale(percentValue, minScale, maxScale) {
+    if (selectedPlacedModel) {
         // Berechnung der Skalierung basierend auf dem Prozentwert
         const scale = minScale + (percentValue / 100) * (maxScale - minScale);
         selectedPlacedModel.scale.set(scale, scale, scale);
@@ -340,207 +340,217 @@
         // Anzeige des aktuellen Prozentsatzes im Dynamic-Menü aktualisieren
         const scaleValueDisplay = document.getElementById("scale-value");
         if (scaleValueDisplay) {
-          scaleValueDisplay.textContent = `${parseInt(percentValue, 10)}%`;
+            scaleValueDisplay.textContent = `${parseInt(percentValue, 10)}%`;
         }
-      }
     }
-
-    let moveDelta = 0.1; // Standardwert für die Verschiebungsgröße, kann mit dem Slider geändert werden
+}
 
 function openMoveMenu() {
-      if (!selectedPlacedModel) {
+    if (!selectedPlacedModel) {
         console.log("Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bewegen.");
         return;
-      }
-
-      const dynamicMenu = document.getElementById("dynamic-menu");
-      dynamicMenu.style.display = "flex";
-
-      dynamicMenu.innerHTML = `
-    <h3>Modell bewegen</h3>
-    <label>
-      Aktuelle Position: X=${selectedPlacedModel.position.x.toFixed(2)}, Z=${selectedPlacedModel.position.z.toFixed(2)}
-    </label>
-    <label>
-      Verschiebungsgröße: <span id="move-delta-display">${moveDelta.toFixed(2)}</span>
-      <input type="range" min="0.01" max="1.0" step="0.01" value="${moveDelta}" onchange="updateMoveDelta(this.value)">
-    </label>
-    <div style="display: flex; gap: 4px;">
-      <button onclick="moveModelDynamic('x', -1)">← X</button>
-      <button onclick="moveModelDynamic('x', 1)">→ X</button>
-      <button onclick="moveModelDynamic('z', -1)">- Z</button>
-      <button onclick="moveModelDynamic('z', 1)">+ Z</button>
-    </div>
-    <button onclick="closeDynamicMenu()">Zurück</button>
-  `;
-    }
-
-    function updateMoveDelta(value) {
-      moveDelta = parseFloat(value);
-      const moveDeltaDisplay = document.getElementById("move-delta-display");
-      if (moveDeltaDisplay) {
-        moveDeltaDisplay.textContent = moveDelta.toFixed(2);
-      }
-    }
-
-    function moveModelDynamic(axis, direction) {
-      if (selectedPlacedModel) {
-        const delta = direction * moveDelta; // Dynamischer Wert basierend auf Slider
-        selectedPlacedModel.position[axis] += delta;
-
-        // Position im Menü aktualisieren
-        const positionInfo = document.getElementById("position-info");
-        if (positionInfo) {
-          positionInfo.innerHTML = `
-        <p>Aktuelle Position: X=${selectedPlacedModel.position.x.toFixed(2)}, Z=${selectedPlacedModel.position.z.toFixed(2)}</p>`;
-        }
-      }
     }
 
-    /* ========================= */
-    /*      KARTENSTEUERUNG      */
-    /* ========================= */
-    function refreshMapDialog() {
-      const mapDialog = document.getElementById('map-dialog');
-      mapDialog.style.display = 'flex';
-    }
-
-    function closeMapDialog() {
-      const mapDialog = document.getElementById('map-dialog');
-      mapDialog.style.display = 'none';
-    }
-
-    /* ========================= */
-    /*        AR-HANDLING        */
-    /* ========================= */
-    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) => {
+    const dynamicMenu = document.getElementById("dynamic-menu");
+    dynamicMenu.style.display = "flex";
+
+    dynamicMenu.innerHTML = `
+          <h3>Modell bewegen</h3>
+          <div id="joystick-container" style="position: relative; width: 100px; height: 100px; border: 2px solid #ccc; border-radius: 50%; margin: 20px auto;">
+            <div id="joystick-knob" style="position: absolute; width: 30px; height: 30px; background: #007BFF; border-radius: 50%; top: 50%; left: 50%; transform: translate(-50%, -50%);"></div>
+          </div>
+          <button onclick="closeDynamicMenu()">Zurück</button>
+        `;
+
+    const container = document.getElementById("joystick-container");
+    const knob = document.getElementById("joystick-knob");
+    let isDragging = false;
+
+    const center = { x: container.offsetWidth / 2, y: container.offsetHeight / 2 };
+    const maxDistance = container.offsetWidth / 2;
+
+    knob.addEventListener("mousedown", () => (isDragging = true));
+    document.addEventListener("mouseup", () => {
+        isDragging = false;
+        knob.style.left = "50%";
+        knob.style.top = "50%";
+        moveModelDynamic('x', 0); // Bewegung stoppen, wenn Maus losgelassen wird
+        moveModelDynamic('z', 0);
+    });
+
+    document.addEventListener("mousemove", (event) => {
+        if (!isDragging) return;
+
+        const rect = container.getBoundingClientRect();
+        const dx = event.clientX - rect.left - center.x;
+        const dy = event.clientY - rect.top - center.y;
+        const distance = Math.min(Math.sqrt(dx * dx + dy * dy), maxDistance);
+
+        const angle = Math.atan2(dy, dx);
+        const offsetX = Math.cos(angle) * distance;
+        const offsetY = Math.sin(angle) * distance;
+
+        // Knopfposition aktualisieren
+        knob.style.left = `${center.x + offsetX}px`;
+        knob.style.top = `${center.y + offsetY}px`;
+
+        // Bewegung basierend auf Joystick-Position anwenden
+        const normalizedX = offsetX / maxDistance;
+        const normalizedY = offsetY / maxDistance;
+        moveModelDynamic('x', normalizedX * 0.1); // Feine Anpassung
+        moveModelDynamic('z', -normalizedY * 0.1); // Feine Anpassung
+    });
+}
+
+function moveModelDynamic(axis, value) {
+    if (selectedPlacedModel) selectedPlacedModel.position[axis] += value;
+}
+
+
+/* ========================= */
+/*      KARTENSTEUERUNG      */
+/* ========================= */
+function refreshMapDialog() {
+    const mapDialog = document.getElementById('map-dialog');
+    mapDialog.style.display = 'flex';
+}
+
+function closeMapDialog() {
+    const mapDialog = document.getElementById('map-dialog');
+    mapDialog.style.display = 'none';
+}
+
+/* ========================= */
+/*        AR-HANDLING        */
+/* ========================= */
+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', {
+    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 });
+    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';
+    document.getElementById('menu-bar').style.display = 'flex';
 
-      currentSession.addEventListener("end", () => {
+    currentSession.addEventListener("end", () => {
         currentSession = null;
         document.getElementById("dynamic-menu").style.display = "none";
         menus.forEach(id => {
-          document.getElementById(id).style.display = 'none';
+            document.getElementById(id).style.display = 'none';
         });
-      });
+    });
 
-      canvas.addEventListener("pointerdown", selectModelFromScene);
+    canvas.addEventListener("pointerdown", selectModelFromScene);
 
 
-      if (navigator.geolocation) {
+    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) => {
-                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) {
+            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) => {
+                    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);
+            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';
-    }
-
-    /* ========================= */
-    /*   BENUTZERINTERAKTIONEN   */
-    /* ========================= */
-    let soundTimeout = false;
-    function playButtonSound() {
-      if (!soundTimeout) {
+    });
+}
+
+function exitAR() {
+    document.getElementById('confirmation-dialog').style.display = 'flex';
+}
+
+function confirmExit(shouldExit) {
+    if (shouldExit && currentSession) currentSession.end();
+    document.getElementById('confirmation-dialog').style.display = 'none';
+}
+
+/* ========================= */
+/*   BENUTZERINTERAKTIONEN   */
+/* ========================= */
+let soundTimeout = false;
+function playButtonSound() {
+    if (!soundTimeout) {
         const sound = document.getElementById("button-sound");
         sound.currentTime = 0;
         sound.play();
 
         soundTimeout = true;
         setTimeout(() => {
-          soundTimeout = false;
+            soundTimeout = false;
         }, 200); // Verzögerung von 200ms
-      }
     }
+}
-- 
GitLab