Commit 18535aa9 authored by Alfakhori's avatar Alfakhori
Browse files

new test

parent d69bf271
Pipeline #12491 passed with stage
in 23 seconds
// Debug-Konsole aktivieren
const debugConsoleEnabled = true;
if (debugConsoleEnabled) {
const debugConsole = document.getElementById('debug-console');
const debugToggleBtn = document.getElementById('debug-toggle-btn');
debugConsole.style.display = 'block';
debugToggleBtn.style.display = 'block';
// Toggle-Funktion hinzufügen
let isDebugVisible = true;
debugToggleBtn.addEventListener('click', () => {
toggleDebugConsole();
});
// Konsole überschreiben
(function() {
const oldConsoleLog = console.log;
console.log = function(...args) {
oldConsoleLog.apply(console, args); // Normale Konsole weiterhin nutzen
// Logs in das Debug-Element schreiben
debugConsole.innerHTML += args.join(' ') + '<br>';
debugConsole.scrollTop = debugConsole.scrollHeight; // Automatisch nach unten scrollen
};
const oldConsoleError = console.error;
console.error = function(...args) {
oldConsoleError.apply(console, args);
debugConsole.innerHTML += '<span style="color: red;">' + args.join(' ') + '</span><br>';
debugConsole.scrollTop = debugConsole.scrollHeight;
};
// JavaScript-Fehler global abfangen
window.onerror = function(message, source, lineno, colno, error) {
const errorMsg = `[ERROR] ${message} at ${source}:${lineno}:${colno}`;
debugConsole.innerHTML += `<span style="color: red;">${errorMsg}</span><br>`;
debugConsole.scrollTop = debugConsole.scrollHeight;
// Auch in der normalen Konsole anzeigen
oldConsoleError(errorMsg);
};
// Unhandled Promise Rejections abfangen
window.addEventListener('unhandledrejection', function(event) {
const errorMsg = `[PROMISE ERROR] ${event.reason}`;
debugConsole.innerHTML += `<span style="color: orange;">${errorMsg}</span><br>`;
debugConsole.scrollTop = debugConsole.scrollHeight;
oldConsoleError(errorMsg);
});
})();
function toggleDebugConsole() {
isDebugVisible = !isDebugVisible;
debugConsole.style.display = isDebugVisible ? 'block' : 'none';
debugToggleBtn.innerText = isDebugVisible ? 'Close' : 'Open Console';
}
toggleDebugConsole();
console.log("Debug Console enabled");
}
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
/* ========================= */
/* GLOBALE VARIABLEN */
/* ========================= */
let selectedPlacedModel = null;
let currentSession = null;
let reticle = null;
let scene, camera;
let geoLocation;
const modelCache = new Map();
// PERFORMANCE: Reuse one GLTFLoader instead of creating
// a new loader every time a model is placed.
const gltfLoader = new THREE.GLTFLoader();
const menus = [
'menu-bar',
'add-menu',
'edit-menu',
'options-menu',
'map-window'
];
// PERFORMANCE: Cache menu DOM elements once instead of
// repeatedly calling document.getElementById() in showMenu().
const menuElements = {};
menus.forEach(id => {
menuElements[id] = document.getElementById(id);
});
/* ========================= */
/* MODELLE */
/* ========================= */
let models = {
bench: {
name: "Bench",
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: {
name: "Trash bin",
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: {
name: "Lantern",
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: {
name: "Telephone Box",
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: {
name: "Fire Hydrant",
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: {
name: "Statue",
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: {
name: "Fountain",
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 => {
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="assets/icons/back-icon.png"
alt="Zurück"
/>
</div>
`;
}
/* ========================= */
/* MENÜ-STEUERUNG */
/* ========================= */
function showMenu(menuId) {
const isMapWindow =
menuId === 'map-window';
// Check location before opening map
if (isMapWindow && !geoLocation) {
console.log("Standort nicht geladen");
showInfoDialog(
"Ihr Standort wurde noch nicht geladen"
);
return;
}
// PERFORMANCE: Use cached menu elements instead of
// repeatedly querying the DOM.
menus.forEach(id => {
const element =
menuElements[id];
if (element) {
element.style.display =
id === menuId
? 'flex'
: 'none';
}
});
closeDynamicMenu();
// Menu-specific actions
if (menuId === 'menu-bar') {
clearSelectedModel();
} else if (isMapWindow) {
// Initialize map
init_map();
// Leaflet needs to know the actual size of the
// visible map container.
setTimeout(() => {
if (
typeof centerOnPlan === 'function'
) {
centerOnPlan();
}
}, 100);
}
}
/* ============================================================ */
/* Close map and return to AR scene */
/* ============================================================ */
function closeMap() {
const mapWindow =
menuElements['map-window'];
if (mapWindow) {
mapWindow.style.display =
"none";
}
// Show the main AR menu again
const menuBar =
menuElements['menu-bar'];
if (menuBar) {
menuBar.style.display =
"flex";
}
// Stop the map update interval
if (
typeof intervalId !== 'undefined'
) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
// Clear selected model
clearSelectedModel();
}
function closeDynamicMenu() {
const dynamicMenu =
document.getElementById(
"dynamic-menu"
);
if (dynamicMenu) {
dynamicMenu.style.display =
"none";
}
}
function showInfoDialog(message) {
const infoDialog =
document.getElementById(
"info-dialog"
);
const infoText =
document.getElementById(
"info-text"
);
infoText.textContent =
message;
infoDialog.style.display =
"flex";
}
function closeInfoDialog() {
const infoDialog =
document.getElementById(
"info-dialog"
);
infoDialog.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
);
// PERFORMANCE: Reuse the global GLTFLoader.
// Do not create a new loader for every model.
if (modelCache.has(filePath)) {
// Use the already loaded model
const cachedModel =
modelCache.get(filePath);
const model =
cachedModel.clone(true);
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);
}
} else {
// Model has not been loaded yet
gltfLoader.load(
filePath,
(gltf) => {
// Store the original model in the cache
modelCache.set(
filePath,
gltf.scene
);
const model =
gltf.scene.clone(true);
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 selectModel(modelId) {
const model =
models[modelId];
if (
model &&
model.file
) {
loadModel(
model.file
);
showMenu(
'menu-bar'
);
}
}
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 intersects =
raycaster.intersectObjects(
scene.children,
true
);
if (
intersects.length > 0
) {
let selectedObject =
intersects[0].object;
while (
selectedObject.parent &&
selectedObject.parent !== scene
) {
selectedObject =
selectedObject.parent;
}
// Reticle cannot be selected
if (
selectedObject === reticle
) {
console.log(
"Reticle kann nicht ausgewählt werden."
);
return;
}
selectedPlacedModel =
selectedObject;
highlightSelectedModel();
showMenu(
"edit-menu"
);
}
}
function clearSelectedModel() {
if (selectedPlacedModel) {
selectedPlacedModel.traverse(
(child) => {
if (child.isMesh) {
child.material.emissive
.setHex(
0x000000
);
}
}
);
selectedPlacedModel =
null;
}
}
function highlightSelectedModel() {
if (selectedPlacedModel) {
removeHighlightFromAllModels();
selectedPlacedModel.traverse(
(child) => {
if (child.isMesh) {
child.material.emissive
.setHex(
0xff0000
);
}
}
);
}
}
function removeHighlightFromSelectedModel() {
if (selectedPlacedModel) {
selectedPlacedModel.traverse(
(child) => {
if (child.isMesh) {
child.material.emissive
.setHex(
0x000000
);
}
}
);
}
}
function removeHighlightFromAllModels() {
scene.traverse(
(child) => {
if (
child.isMesh &&
child.material &&
child.material.emissive
) {
child.material.emissive
.setHex(
0x000000
);
}
}
);
}
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';
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) {
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
)
);
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;
const currentRotationDisplay =
document.getElementById(
"current-rotation"
);
if (currentRotationDisplay) {
currentRotationDisplay.textContent =
value;
}
}
}
function openScaleMenu() {
if (!selectedPlacedModel) {
console.log(
"Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."
);
return;
}
const currentScale =
selectedPlacedModel.scale.x;
const minScale =
selectedPlacedModel.modelConfig.minScale;
const maxScale =
selectedPlacedModel.modelConfig.maxScale;
const step =
(maxScale - minScale) /
100;
const currentScalePercent =
(
(currentScale - minScale) /
(maxScale - minScale)
) * 100;
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="${step}"
value="${currentScalePercent}"
onchange="updateScale(
this.value,
${minScale},
${maxScale}
)"
>
</label>
<button onclick="closeDynamicMenu()">
Zurück
</button>
`;
}
function updateScale(
percentValue,
minScale,
maxScale
) {
if (selectedPlacedModel) {
const scale =
minScale +
(percentValue / 100) *
(maxScale - minScale);
selectedPlacedModel.scale.set(
scale,
scale,
scale
);
const scaleValueDisplay =
document.getElementById(
"scale-value"
);
if (scaleValueDisplay) {
scaleValueDisplay.textContent =
`${parseInt(percentValue, 10)}%`;
}
}
}
let moveDelta = 0.1;
function openMoveMenu() {
if (!selectedPlacedModel) {
console.log(
"Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten."
);
return;
}
const dynamicMenu =
document.getElementById(
"dynamic-menu"
);
dynamicMenu.style.display =
"flex";
dynamicMenu.innerHTML = `
<h3>Position anpassen</h3>
<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 class="button-row">
<div
class="control-button"
id="left"
onclick="moveModelDynamic('x', -moveDelta)"
>
<img
src="assets/icons/left-arrow.png"
alt="Left"
/>
</div>
<div
class="control-button"
id="right"
onclick="moveModelDynamic('x', moveDelta)"
>
<img
src="assets/icons/right-arrow.png"
alt="Right"
/>
</div>
<div
class="control-button"
id="up"
onclick="moveModelDynamic('z', -moveDelta)"
>
<img
src="assets/icons/up-arrow.png"
alt="Up"
/>
</div>
<div
class="control-button"
id="down"
onclick="moveModelDynamic('z', moveDelta)"
>
<img
src="assets/icons/down-arrow.png"
alt="Down"
/>
</div>
</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,
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;
// ========================================================
// AR lighting
// ========================================================
// Soft overall illumination
const ambientLight = new THREE.AmbientLight(0xffffff, 2.5);
scene.add(ambientLight);
// Sky/ground illumination
const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x888888, 1.5);
scene.add(hemisphereLight);
// Directional light for shadows and form
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(10, 20, 10);
scene.add(directionalLight);
// ========================================================
// Reticle
// ========================================================
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);
},
);
// ========================================================
// Start WebXR AR session
// ========================================================
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,
});
// ========================================================
// Show main menu
// ========================================================
document.getElementById("menu-bar").style.display = "flex";
// ========================================================
// AR session ended
// Reload the complete website
// ========================================================
currentSession.addEventListener("end", () => {
console.log("AR session ended");
// Completely reload the website.
// This resets the AR canvas, menus, scene, map,
// variables, and all other application state.
window.location.reload();
});
// ========================================================
// Select objects in AR
// ========================================================
canvas.addEventListener("pointerdown", selectModelFromScene);
// ========================================================
// 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));
// =================================================
// Load saved scene models
// =================================================
if (sceneData) {
sceneData.models.forEach((model) => {
if (model.name) {
const modelDefinition = Object.values(models).find(
(m) => model.name === m.name,
);
if (!modelDefinition) {
console.error("Modell nicht gefunden:", model.name);
return;
}
const filePath = modelDefinition.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;
}
});
}
});
}
},
(error) => {
console.error(
"Fehler bei der Geolokalisierung:",
JSON.stringify(error),
);
},
{
enableHighAccuracy: true,
maximumAge: 2000,
timeout: 5000,
},
);
}
// ========================================================
// XR render loop
// ========================================================
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);
// =================================================
// Hit testing
// =================================================
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);
}
// =================================================
// Render AR scene
// =================================================
renderer.render(scene, camera);
}
});
}
/* ========================= */
/* AR EXIT */
/* ========================= */
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;
}, 200);
}
}
\ No newline at end of file
let init = false;
let mymap;
let userIcon;
let intervalId;
let modeMarkers = [];
// PERFORMANCE: Reuse one Vector3 instead of allocating a new
// Vector3 every time updateMap() runs.
let cameraPosition = new THREE.Vector3();
// PERFORMANCE: Cache the map window element once.
let mapWindow = document.getElementById("map-window");
// PERFORMANCE: Remember the last mapped camera position so Leaflet
// is updated only after the user has actually moved.
let lastMapX = null;
let lastMapZ = null;
// ============================================================
// Proposed plan overlay
// ============================================================
let planOverlay = null;
let planVisible = false;
// Proposed plan geographic bounds
// DHDN / Gauss-Krüger Zone 3
// Converted to WGS84
//
// Southwest:
// Latitude = 48.405858385
// Longitude = 7.997080070
//
// Northeast:
// Latitude = 48.414966828
// Longitude = 8.010410216
const planBounds = [
[48.405858385, 7.99708007],
[48.414966828, 8.010410216],
];
const earthRadius = 6378137;
// ============================================================
// Initialize map
// ============================================================
function init_map() {
const mapContainer = document.getElementById("map-container");
if (mapContainer) {
if (!init) {
mymap = L.map(mapContainer);
var OpenStreetMap_DE = L.tileLayer(
"https://tile.openstreetmap.de/{z}/{x}/{y}.png",
{
maxZoom: 25,
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
},
);
OpenStreetMap_DE.addTo(mymap);
userIcon = L.icon({
iconUrl: "assets/previewImages/map-user.png",
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -32],
});
mymap.fitBounds(planBounds, {
padding: [20, 20],
});
userIcon = L.marker([geoLocation.latitude, geoLocation.longitude], {
icon: userIcon,
})
.bindPopup("Ihre Position")
.addTo(mymap);
init = true;
}
// ========================================================
// Remove old model markers
// ========================================================
modeMarkers.forEach((marker) => {
marker.remove();
});
modeMarkers = [];
// ========================================================
// Add currently placed models to map
// ========================================================
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);
});
// ========================================================
// Add proposed plan
// ========================================================
addPlanOverlay();
// ========================================================
// Make sure Leaflet knows the map container dimensions
// ========================================================
setTimeout(() => {
centerOnPlan();
}, 100);
// ========================================================
// Update map position continuously
// ========================================================
// PERFORMANCE: Start only one map update interval.
if (!intervalId) {
intervalId = setInterval(updateMap, 500);
}
}
}
// ============================================================
// Center map on proposed plan
// ============================================================
function centerOnPlan() {
if (!mymap) {
console.error("Map ist noch nicht initialisiert.");
return;
}
mymap.invalidateSize();
mymap.fitBounds(planBounds, {
padding: [20, 20],
});
}
// ============================================================
// Add georeferenced proposed plan
// ============================================================
function addPlanOverlay() {
if (!mymap) {
console.error("Map ist noch nicht initialisiert.");
return;
}
if (planOverlay) {
planOverlay.addTo(mymap);
planVisible = true;
return;
}
planOverlay = L.imageOverlay("assets/plans/proposed-plan8k.png", planBounds, {
opacity: 0.7,
interactive: false,
});
planOverlay.addTo(mymap);
planVisible = true;
}
// ============================================================
// Update map
// ============================================================
function updateMap() {
// The interval is normally stopped by closeMap().
// This guard also handles cases where the map is hidden
// by another UI action.
if (mapWindow && mapWindow.style.display === "none" && intervalId) {
clearInterval(intervalId);
intervalId = null;
return;
}
if (!camera || !userIcon) {
return;
}
// PERFORMANCE: Reuse the same Vector3 object.
camera.getWorldPosition(cameraPosition);
// PERFORMANCE: Do not update Leaflet if the user
// has not moved.
if (
lastMapX !== null &&
Math.abs(cameraPosition.x - lastMapX) < 0.1 &&
Math.abs(cameraPosition.z - lastMapZ) < 0.1
) {
return;
}
lastMapX = cameraPosition.x;
lastMapZ = cameraPosition.z;
const { lat, lng } = threeToLeaflet(cameraPosition.x, cameraPosition.z);
userIcon.setLatLng([lat, lng]);
}
// ============================================================
// Convert Three.js coordinates to Leaflet coordinates
// ============================================================
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,
};
}
// ============================================================
// Convert Leaflet coordinates to Three.js coordinates
// ============================================================
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,
};
}
// ============================================================
// Round number
// ============================================================
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);
}
/* ========================= */
/* DEBUGGING UND START */
/* ========================= */
let sceneData = null;
let sceneFileName = null;
let startScreen = null;
let startButton = null;
let loadButton = null;
let predefinedButton1 = null;
let predefinedButton2 = null;
/* ========================= */
/* PREDEFINED SCENE */
/* ========================= */
async function loadPredefinedScene(filePath, fileName) {
try {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
sceneData = data;
sceneFileName = fileName;
console.log("Vordefinierte Szene geladen:", fileName);
return true;
} catch (error) {
console.error("Fehler beim Laden der vordefinierten Szene:", error);
return false;
}
}
/* ============================================================ */
/* Create the Start Screen */
/* ============================================================ */
function showStartScreen() {
// Prevent duplicate start screens
if (
startScreen ||
startButton ||
loadButton ||
predefinedButton1 ||
predefinedButton2
) {
return;
}
/* ==========================================================
START SCREEN CONTAINER
========================================================== */
startScreen = document.createElement("div");
startScreen.classList.add("start-screen");
document.body.appendChild(startScreen);
/* ==========================================================
HFT LOGO
========================================================== */
const hftLogo = document.createElement("img");
hftLogo.src = "assets/images/logo-HFT.svg";
hftLogo.alt = "HFT Stuttgart";
hftLogo.classList.add("start-screen-logo");
startScreen.appendChild(hftLogo);
/* ==========================================================
TITLE
========================================================== */
const title = document.createElement("h1");
title.textContent = "GENGENBACH";
title.classList.add("start-screen-title");
startScreen.appendChild(title);
/* ==========================================================
SUBTITLE
========================================================== */
const subtitle = document.createElement("div");
subtitle.textContent = "AR Urban Planning";
subtitle.classList.add("start-screen-subtitle");
startScreen.appendChild(subtitle);
/* ==========================================================
DIVIDER
========================================================== */
const divider = document.createElement("div");
divider.classList.add("start-screen-divider");
startScreen.appendChild(divider);
/* ==========================================================
START AR
========================================================== */
startButton = document.createElement("button");
startButton.textContent = "Start AR";
startButton.classList.add("start-screen-button", "start-ar-button");
startScreen.appendChild(startButton);
/* ==========================================================
LOAD SCENE
========================================================== */
loadButton = document.createElement("button");
loadButton.textContent = sceneFileName || "Umgebung Laden";
loadButton.classList.add(
"start-screen-button",
"load-scene-button",
"scene-button",
);
startScreen.appendChild(loadButton);
/* ==========================================================
PREDEFINED SCENE 1
========================================================== */
predefinedButton1 = document.createElement("button");
predefinedButton1.textContent = "Quartiersplatz 1";
predefinedButton1.classList.add(
"start-screen-button",
"predefined-scene-button-1",
"scene-button",
);
startScreen.appendChild(predefinedButton1);
/* ==========================================================
PREDEFINED SCENE 2
========================================================== */
predefinedButton2 = document.createElement("button");
predefinedButton2.textContent = "Quartiersplatz 2";
predefinedButton2.classList.add(
"start-screen-button",
"predefined-scene-button-2",
"scene-button",
);
startScreen.appendChild(predefinedButton2);
/* ==========================================================
START AR CLICK
========================================================== */
startButton.onclick = () => {
// Remove complete start screen
startScreen.remove();
// Reset references
startScreen = null;
startButton = null;
loadButton = null;
predefinedButton1 = null;
predefinedButton2 = null;
// Start AR
activateXR(sceneData);
};
/* ==========================================================
LOAD SCENE CLICK
========================================================== */
loadButton.onclick = () => {
loadSceneFromFile((data, fileName) => {
if (data && fileName) {
sceneData = data;
sceneFileName = fileName;
loadButton.textContent = fileName;
// Select uploaded scene
loadButton.classList.add("selected");
// Deselect predefined scenes
predefinedButton1.classList.remove("selected");
predefinedButton2.classList.remove("selected");
} else {
loadButton.textContent = sceneFileName || "Umgebung Laden";
}
});
};
/* ==========================================================
PREDEFINED SCENE 1
========================================================== */
predefinedButton1.onclick = async () => {
const success = await loadPredefinedScene(
"scenes/gengenbach-plaza.json",
"Quartiersplatz 1",
);
if (success) {
// Select Plaza
predefinedButton1.classList.add("selected");
// Deselect Street
predefinedButton2.classList.remove("selected");
// Deselect uploaded scene
loadButton.classList.remove("selected");
}
};
/* ==========================================================
PREDEFINED SCENE 2
========================================================== */
predefinedButton2.onclick = async () => {
const success = await loadPredefinedScene(
"scenes/gengenbach-street.json",
"Quartiersplatz 2",
);
if (success) {
// Select Street
predefinedButton2.classList.add("selected");
// Deselect Plaza
predefinedButton1.classList.remove("selected");
// Deselect uploaded scene
loadButton.classList.remove("selected");
}
};
}
/* ============================================================ */
/* INITIAL START */
/* ============================================================ */
if (navigator.xr) {
showStartScreen();
} else {
alert("WebXR wird nicht unterstützt.");
}
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gengenbach AR Projekt</title>
<link rel="stylesheet" href="style.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>
<body>
<!-- ============================================================
DEBUG
============================================================ -->
<div id="debug-container">
<div id="debug-console"></div>
<button id="debug-toggle-btn"></button>
</div>
<!-- ============================================================
STANDARDMENÜ
============================================================ -->
<div id="menu-bar">
<div
class="menu-item"
id="add-section"
onclick="showMenu('add-menu')">
<img
src="assets/icons/add-icon.png"
alt="Hinzufügen" />
</div>
<div
class="menu-item"
onclick="showMenu('map-window')">
<img
src="assets/icons/map-icon.png"
alt="Karte" />
</div>
<div
class="menu-item"
id="options-section"
onclick="showMenu('options-menu')">
<img
src="assets/icons/options-icon.png"
alt="Optionen" />
</div>
</div>
<!-- ============================================================
HINZUFÜGEN-MENÜ
============================================================ -->
<div
id="add-menu"
class="menu-placeholder">
</div>
<!-- ============================================================
BEARBEITEN-MENÜ
============================================================ -->
<div
id="edit-menu"
class="menu-placeholder">
<div
class="menu-item"
onclick="openRotationMenu()">
<img
src="assets/icons/rotate-icon.png"
alt="Rotation" />
</div>
<div
class="menu-item"
onclick="openScaleMenu()">
<img
src="assets/icons/scale-icon.png"
alt="Skalierung" />
</div>
<div
class="menu-item"
onclick="openMoveMenu()">
<img
src="assets/icons/move-icon.png"
alt="Bewegen" />
</div>
<div
class="menu-item"
onclick="deleteModel()">
<img
src="assets/icons/delete-icon.png"
alt="Löschen" />
</div>
<div
class="menu-item"
onclick="completeEditing()">
<img
src="assets/icons/check-icon.png"
alt="Fertigstellen" />
</div>
</div>
<!-- ============================================================
OPTIONEN-MENÜ
============================================================ -->
<div
id="options-menu"
class="menu-placeholder">
<div
class="menu-item"
onclick="exitAR()">
<img
src="assets/icons/exit-icon.png"
alt="Beenden" />
</div>
<div
class="menu-item"
onclick="downloadScene()">
<img
src="assets/icons/download-icon.png"
alt="Download" />
</div>
<div
class="menu-item"
onclick="showMenu('menu-bar')">
<img
src="assets/icons/back-icon.png"
alt="Zurück" />
</div>
</div>
<!-- ============================================================
DYNAMISCHES MENÜ
============================================================ -->
<div id="dynamic-menu"></div>
<!-- ============================================================
BESTÄTIGUNGSDIALOG LÖSCHEN
============================================================ -->
<div id="delete-confirmation-dialog">
<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 BEENDEN
============================================================ -->
<div id="confirmation-dialog">
<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">
<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>
<!-- ============================================================
MAP WINDOW
============================================================ -->
<div
id="map-window"
class="menu-placeholder">
<div id="map-container"></div>
<div
class="menu-item map-back-button"
onclick="showMenu('menu-bar')">
<img
src="assets/icons/back-icon.png"
alt="Zurück" />
</div>
</div>
<!-- ============================================================
APP VERSION
Change this manually when needed.
============================================================ -->
<div id="app-version">
v0.2.0
</div>
<!-- ============================================================
JAVASCRIPT
============================================================ -->
<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>
<script src="ar_overviewmap.js"></script>
<script src="ar_download.js"></script>
<!-- ============================================================
BUTTON SOUND
============================================================ -->
<audio
id="button-sound"
src="assets/sounds/button-sound.mp3"
preload="auto">
</audio>
</body>
</html>
\ No newline at end of file
/* ============================================================
GLOBAL / START SCREEN
============================================================ */
body {
margin: 0;
font-family: Arial, sans-serif;
color: white;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
background-image:
linear-gradient(
rgba(0, 0, 0, 0.35),
rgba(0, 0, 0, 0.50)
),
url('assets/images/gengenbach.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
/* ============================================================
GENERAL CONTAINER
============================================================ */
.container {
text-align: center;
background: rgba(20, 20, 20, 0.55);
padding: 40px 30px;
border-radius: 20px;
color: white;
max-width: 80%;
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.35);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.15);
}
/* ============================================================
GENERAL HEADINGS / TEXT
============================================================ */
h1 {
font-size: 2.5em;
margin-bottom: 10px;
color: white;
}
p {
font-size: 1.1em;
margin-bottom: 30px;
color: rgba(255, 255, 255, 0.85);
}
/* ============================================================
GENERAL BUTTONS
============================================================ */
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;
touch-action: manipulation;
}
button:hover {
background-color: #45a049;
}
button:active {
background-color: #387a39;
}
/* ============================================================
START SCREEN
============================================================ */
.start-screen {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(340px, 82vw);
display: flex;
flex-direction: column;
align-items: center;
gap: 11px;
z-index: 10;
text-align: center;
}
/* ==========================================================
HFT LOGO
========================================================== */
.start-screen-logo {
width: 110px;
height: auto;
display: block;
margin: 0 auto 18px auto;
object-fit: contain;
}
/* ============================================================
START SCREEN TITLE
============================================================ */
.start-screen-title {
margin: 0;
color: white;
font-size: clamp(28px, 7vw, 42px);
font-weight: 700;
letter-spacing: 3px;
line-height: 1.1;
text-shadow:
0 3px 12px rgba(0, 0, 0, 0.45);
}
.start-screen-subtitle {
margin: 4px 0 4px;
color: rgba(255, 255, 255, 0.88);
font-size: 15px;
font-weight: 400;
letter-spacing: 1px;
text-shadow:
0 2px 8px rgba(0, 0, 0, 0.45);
}
/* ============================================================
START SCREEN DIVIDER
============================================================ */
.start-screen-divider {
width: 55px;
height: 2px;
margin: 8px 0 12px;
background: rgba(255, 255, 255, 0.75);
border-radius: 2px;
}
/* ============================================================
START SCREEN BUTTONS
============================================================ */
.start-screen-button {
position: relative;
width: 100%;
box-sizing: border-box;
min-height: 54px;
padding: 15px 48px 15px 48px;
font-size: 16px;
font-weight: 500;
letter-spacing: 0.2px;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.18);
color: white;
cursor: pointer;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow:
0 6px 20px rgba(0, 0, 0, 0.25);
margin: 0;
touch-action: manipulation;
transition:
background-color 0.2s ease,
transform 0.15s ease,
box-shadow 0.2s ease;
}
/* ============================================================
BUTTON ICONS
============================================================ */
.start-screen-button::before {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
font-size: 19px;
opacity: 0.9;
}
/* Start AR */
.start-ar-button::before {
content: "◎";
}
/* Load scene */
.load-scene-button::before {
content: "↥";
}
/* Plaza */
.predefined-scene-button-1::before {
content: "⌂";
}
/* Street */
.predefined-scene-button-2::before {
content: "▥";
}
/* ============================================================
SELECTED SCENE CHECKMARK
============================================================ */
.scene-button.selected::after {
content: "✓";
position: absolute;
right: 18px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
font-weight: 700;
color: white;
}
/* ============================================================
START AR BUTTON
============================================================ */
.start-ar-button {
background: #4CAF50;
min-height: 60px;
font-size: 19px;
font-weight: 600;
letter-spacing: 0.3px;
border-color:
rgba(255, 255, 255, 0.28);
box-shadow:
0 8px 25px rgba(0, 0, 0, 0.30),
0 3px 12px rgba(76, 175, 80, 0.20);
margin-bottom: 7px;
}
.start-ar-button:hover {
background: #45a049;
transform: scale(1.03);
box-shadow:
0 10px 30px rgba(0, 0, 0, 0.35),
0 4px 16px rgba(76, 175, 80, 0.25);
}
.start-ar-button:active {
background: #387a39;
transform: scale(0.98);
}
/* ============================================================
SCENE BUTTONS
============================================================ */
.scene-button {
background: rgba(45, 45, 45, 0.78);
}
.scene-button:hover {
background: rgba(70, 70, 70, 0.88);
transform: scale(1.02);
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.30);
}
.scene-button.selected {
background: #4CAF50;
border-color:
rgba(255, 255, 255, 0.30);
box-shadow:
0 7px 24px rgba(0, 0, 0, 0.30),
0 3px 14px rgba(76, 175, 80, 0.25);
}
.scene-button.selected:hover {
background: #45a049;
transform: scale(1.02);
}
.scene-button:active {
transform: scale(0.98);
}
/* ============================================================
STANDARD MENÜ
============================================================ */
#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 INITIAL STATE
============================================================ */
#menu-bar,
#add-menu,
#edit-menu,
#options-menu {
display: none;
}
/* ============================================================
MENU ITEMS
============================================================ */
.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;
}
/* ============================================================
ADD MENU
============================================================ */
#add-menu {
overflow-y: auto;
}
/* ============================================================
DYNAMIC MENU
============================================================ */
#dynamic-menu {
position: absolute;
bottom: 90px;
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background: #1e1e1e;
box-shadow:
0 -2px 6px rgba(0, 0, 0, 0.8);
color: white;
width: 100vw;
height: 250px;
z-index: 20;
overflow-y: auto;
border-radius: 8px;
}
/* ============================================================
RANGE SLIDER
============================================================ */
#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;
}
/* ============================================================
CONFIRMATION / INFORMATION DIALOGS
============================================================ */
#confirmation-dialog,
#delete-confirmation-dialog,
#info-dialog {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: none;
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);
}
/* ============================================================
MAP WINDOW
============================================================ */
#map-window {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: none;
flex-direction: column;
background: #ffffff;
z-index: 100;
box-sizing: border-box;
padding: 10px;
}
/* ============================================================
MAP CONTAINER
============================================================ */
#map-container {
position: relative;
z-index: 1500;
flex: 15;
width: 100%;
height: 100%;
}
/* ============================================================
MAP BACK BUTTON
============================================================ */
.map-back-button {
cursor: pointer;
flex: 1;
align-items: center;
justify-content: center;
}
.map-back-button img {
width: 30px;
height: 30px;
}
/* ============================================================
LEAFLET MAP
============================================================ */
#map-container .leaflet-container {
width: 100%;
height: 100%;
}
/* ============================================================
DEBUG CONSOLE
============================================================ */
#debug-container {
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;
}
#debug-console {
width: 100%;
max-height: 200px;
overflow-y: auto;
background: rgba(0, 0, 0, 0.7);
color: #00ff00;
padding: 5px;
display: none;
}
#debug-toggle-btn {
background: rgba(0, 0, 0, 0.7);
color: #00ff00;
font-size: 14px;
border: none;
padding: 5px 10px;
cursor: pointer;
display: none;
}
/* ============================================================
MOVE MENU
============================================================ */
.button-row {
display: flex;
gap: 15px;
justify-content: center;
align-items: center;
}
.control-button {
flex: 0 0 auto;
border: none;
border-radius: 20px;
padding: 8px;
background-color: #007BFF;
cursor: pointer;
transition:
background-color 0.3s,
transform 0.2s;
}
.control-button img {
width: 30px;
height: 30px;
}
.control-button:active {
background-color: #0056b3;
transform: scale(0.95);
}
/* ============================================================
APP VERSION
Manual version number for development
============================================================ */
#app-version {
position: fixed;
right: 8px;
bottom: 6px;
z-index: 9998;
font-family: monospace;
font-size: 10px;
color: rgba(255, 255, 255, 0.55);
background: rgba(0, 0, 0, 0.25);
padding: 3px 6px;
border-radius: 5px;
pointer-events: none;
user-select: none;
}
/* ============================================================
MOBILE
============================================================ */
@media (max-width: 600px) {
.start-screen {
width: 78vw;
}
.start-screen-title {
font-size: 30px;
letter-spacing: 2.5px;
}
.start-screen-subtitle {
font-size: 14px;
}
.start-screen-button {
min-height: 52px;
padding: 14px 45px;
font-size: 16px;
}
.start-ar-button {
min-height: 58px;
font-size: 18px;
}
}
/* ============================================================
SMALL HEIGHT DEVICES
============================================================ */
@media (max-height: 650px) {
.start-screen {
gap: 7px;
width: min(310px, 76vw);
}
.start-screen-title {
font-size: 27px;
}
.start-screen-subtitle {
font-size: 13px;
}
.start-screen-divider {
margin: 4px 0 6px;
}
.start-screen-button {
min-height: 46px;
padding-top: 11px;
padding-bottom: 11px;
}
.start-ar-button {
min-height: 50px;
}
}
/* ============================================================
VERY SMALL SCREENS / LANDSCAPE
============================================================ */
@media (max-height: 500px) {
.start-screen {
transform:
translate(-50%, -50%)
scale(0.85);
transform-origin: center;
}
}
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment