Skip to content
GitLab
Explore
Projects
Groups
Snippets
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
GeoVistoogsi
AR
Commits
67715bc3
Commit
67715bc3
authored
2 months ago
by
Cantuerk
Browse files
Options
Download
Email Patches
Plain Diff
Update public/ar_main.js
parent
9d0b421b
master
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
public/ar_main.js
+370
-360
public/ar_main.js
with
370 additions
and
360 deletions
+370
-360
public/ar_main.js
+
370
-
360
View file @
67715bc3
/* ========================= */
/* ========================= */
/* GLOBALE VARIABLEN */
/* GLOBALE VARIABLEN */
/* ========================= */
/* ========================= */
let
selectedPlacedModel
=
null
;
let
selectedPlacedModel
=
null
;
let
currentSession
=
null
;
let
currentSession
=
null
;
let
reticle
=
null
;
let
reticle
=
null
;
let
scene
,
camera
;
let
scene
,
camera
;
let
geoLocation
;
let
geoLocation
;
const
menus
=
[
'
menu-bar
'
,
'
add-menu
'
,
'
edit-menu
'
,
'
options-menu
'
,
'
map-window
'
];
const
menus
=
[
'
menu-bar
'
,
'
add-menu
'
,
'
edit-menu
'
,
'
options-menu
'
,
'
map-window
'
];
/* ========================= */
/* ========================= */
/* MODELLE */
/* MODELLE */
/* ========================= */
/* ========================= */
let
models
=
{
let
models
=
{
bench
:
{
bench
:
{
name
:
"
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
"
,
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
},
scale
:
{
x
:
0.1
,
y
:
0.1
,
z
:
0.1
},
minScale
:
0.05
,
minScale
:
0.05
,
maxScale
:
0.5
maxScale
:
0.5
},
},
trashbin
:
{
trashbin
:
{
name
:
"
Trash bin
"
,
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
"
,
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
},
scale
:
{
x
:
0.03
,
y
:
0.03
,
z
:
0.03
},
minScale
:
0.01
,
minScale
:
0.01
,
maxScale
:
0.1
maxScale
:
0.1
},
},
lantern
:
{
lantern
:
{
name
:
"
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
"
,
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
},
scale
:
{
x
:
0.5
,
y
:
0.5
,
z
:
0.5
},
minScale
:
0.2
,
minScale
:
0.2
,
maxScale
:
5
maxScale
:
5
},
},
telephone_box
:
{
telephone_box
:
{
name
:
"
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
"
,
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
},
scale
:
{
x
:
0.5
,
y
:
0.5
,
z
:
0.5
},
minScale
:
0.05
,
minScale
:
0.05
,
maxScale
:
1
maxScale
:
1
},
},
fire_hydrant_model
:
{
fire_hydrant_model
:
{
name
:
"
Fire Hydrant
"
,
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
"
,
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
},
scale
:
{
x
:
0.3
,
y
:
0.3
,
z
:
0.3
},
minScale
:
0.1
,
minScale
:
0.1
,
maxScale
:
1
maxScale
:
1
},
},
statue
:
{
statue
:
{
name
:
"
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
"
,
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
},
scale
:
{
x
:
0.5
,
y
:
0.5
,
z
:
0.5
},
minScale
:
0.05
,
minScale
:
0.05
,
maxScale
:
2
maxScale
:
2
},
},
fountain
:
{
fountain
:
{
name
:
"
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
"
,
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
},
scale
:
{
x
:
0.001
,
y
:
0.001
,
z
:
0.001
},
minScale
:
0.0005
,
minScale
:
0.0005
,
maxScale
:
0.005
maxScale
:
0.005
}
}
};
};
/* ========================= */
/* ========================= */
/* INITIALISIERUNG */
/* INITIALISIERUNG */
/* ========================= */
/* ========================= */
window
.
onload
=
()
=>
{
window
.
onload
=
()
=>
{
initializeAddMenu
();
initializeAddMenu
();
// Allen Buttons den Sound hinzufügen
// Allen Buttons den Sound hinzufügen
const
buttons
=
document
.
querySelectorAll
(
"
button, .menu-item
"
);
const
buttons
=
document
.
querySelectorAll
(
"
button, .menu-item
"
);
buttons
.
forEach
(
button
=>
{
buttons
.
forEach
(
button
=>
{
button
.
addEventListener
(
"
click
"
,
playButtonSound
);
button
.
addEventListener
(
"
click
"
,
playButtonSound
);
});
});
};
};
function
initializeAddMenu
()
{
function
initializeAddMenu
()
{
const
addMenu
=
document
.
getElementById
(
'
add-menu
'
);
const
addMenu
=
document
.
getElementById
(
'
add-menu
'
);
addMenu
.
innerHTML
=
Object
.
entries
(
models
)
addMenu
.
innerHTML
=
Object
.
entries
(
models
)
.
map
(
.
map
(
([
key
,
model
])
=>
`
([
key
,
model
])
=>
`
<div class="menu-item" id="
${
key
}
-item" onclick="selectModel('
${
key
}
')">
<div class="menu-item" id="
${
key
}
-item" onclick="selectModel('
${
key
}
')">
<img src="
${
model
.
image
}
" alt="
${
model
.
name
}
" />
<img src="
${
model
.
image
}
" alt="
${
model
.
name
}
" />
</div>
</div>
...
@@ -96,243 +96,243 @@
...
@@ -96,243 +96,243 @@
.
join
(
''
)
+
.
join
(
''
)
+
`
`
<div class="menu-item" onclick="showMenu('menu-bar')">
<div class="menu-item" onclick="showMenu('menu-bar')">
<img src="
previewImage
s/back-icon.png" alt="Zurück" />
<img src="
assets/icon
s/back-icon.png" alt="Zurück" />
</div>
</div>
`
;
`
;
}
}
/* ========================= */
/* ========================= */
/* MENÜ-STEUERUNG */
/* MENÜ-STEUERUNG */
/* ========================= */
/* ========================= */
function
showMenu
(
menuId
)
{
function
showMenu
(
menuId
)
{
const
isMapWindow
=
menuId
===
'
map-window
'
;
const
isMapWindow
=
menuId
===
'
map-window
'
;
if
(
isMapWindow
&&
!
geoLocation
)
{
if
(
isMapWindow
&&
!
geoLocation
)
{
console
.
log
(
"
Standort nicht geladen
"
);
console
.
log
(
"
Standort nicht geladen
"
);
showInfoDialog
(
"
Ihr Standort wurde noch nicht geladen
"
);
showInfoDialog
(
"
Ihr Standort wurde noch nicht geladen
"
);
return
;
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
"
;
}
}
/* ========================= */
menus
.
forEach
(
id
=>
{
/* MODELL-HANDLING */
document
.
getElementById
(
id
).
style
.
display
=
id
===
menuId
?
'
flex
'
:
'
none
'
;
/* ========================= */
});
function
getAllPlacedModels
()
{
closeDynamicMenu
();
return
scene
.
children
.
filter
(
child
=>
child
.
isPlacedModel
===
true
);
if
(
menuId
===
'
menu-bar
'
)
clearSelectedModel
();
}
else
if
(
isMapWindow
)
init_map
();
}
function
loadModel
(
filePath
)
{
placeModel
(
filePath
,
reticle
.
position
,
(
model
)
=>
{
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
;
selectedPlacedModel
=
model
;
highlightSelectedModel
();
highlightSelectedModel
();
showMenu
(
'
edit-menu
'
);
showMenu
(
'
edit-menu
'
);
});
});
}
}
function
placeModel
(
filePath
,
position
,
onPlace
=
null
)
{
function
placeModel
(
filePath
,
position
,
onPlace
=
null
)
{
const
modelConfig
=
Object
.
values
(
models
).
find
(
model
=>
model
.
file
===
filePath
);
const
modelConfig
=
Object
.
values
(
models
).
find
(
model
=>
model
.
file
===
filePath
);
const
loader
=
new
THREE
.
GLTFLoader
();
const
loader
=
new
THREE
.
GLTFLoader
();
loader
.
load
(
loader
.
load
(
filePath
,
filePath
,
(
gltf
)
=>
{
(
gltf
)
=>
{
const
model
=
gltf
.
scene
;
const
model
=
gltf
.
scene
;
if
(
modelConfig
&&
modelConfig
.
scale
)
{
if
(
modelConfig
&&
modelConfig
.
scale
)
{
model
.
scale
.
set
(
modelConfig
.
scale
.
x
,
modelConfig
.
scale
.
y
,
modelConfig
.
scale
.
z
);
model
.
scale
.
set
(
modelConfig
.
scale
.
x
,
modelConfig
.
scale
.
y
,
modelConfig
.
scale
.
z
);
}
}
model
.
position
.
copy
(
position
);
model
.
position
.
copy
(
position
);
model
.
isPlacedModel
=
true
;
model
.
isPlacedModel
=
true
;
model
.
modelConfig
=
modelConfig
;
model
.
modelConfig
=
modelConfig
;
scene
.
add
(
model
);
scene
.
add
(
model
);
if
(
onPlace
)
{
if
(
onPlace
)
{
onPlace
(
model
);
onPlace
(
model
);
}
}
},
},
undefined
,
undefined
,
(
error
)
=>
{
(
error
)
=>
{
console
.
error
(
"
Fehler beim Laden des Modells:
"
,
error
);
console
.
error
(
"
Fehler beim Laden des Modells:
"
,
error
);
}
}
);
);
}
}
function
selectModel
(
modelId
)
{
function
selectModel
(
modelId
)
{
const
model
=
models
[
modelId
];
const
model
=
models
[
modelId
];
if
(
model
&&
model
.
file
)
{
if
(
model
&&
model
.
file
)
{
loadModel
(
model
.
file
);
loadModel
(
model
.
file
);
showMenu
(
'
menu-bar
'
);
showMenu
(
'
menu-bar
'
);
}
}
}
}
function
selectModelFromScene
(
event
)
{
function
selectModelFromScene
(
event
)
{
const
mouse
=
new
THREE
.
Vector2
(
const
mouse
=
new
THREE
.
Vector2
(
(
event
.
clientX
/
window
.
innerWidth
)
*
2
-
1
,
(
event
.
clientX
/
window
.
innerWidth
)
*
2
-
1
,
-
(
event
.
clientY
/
window
.
innerHeight
)
*
2
+
1
-
(
event
.
clientY
/
window
.
innerHeight
)
*
2
+
1
);
);
const
raycaster
=
new
THREE
.
Raycaster
();
const
raycaster
=
new
THREE
.
Raycaster
();
raycaster
.
setFromCamera
(
mouse
,
camera
);
raycaster
.
setFromCamera
(
mouse
,
camera
);
// Prüfe Kollisionen mit Objekten in der Szene
// Prüfe Kollisionen mit Objekten in der Szene
const
intersects
=
raycaster
.
intersectObjects
(
scene
.
children
,
true
);
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
// Finde das Hauptobjekt (Root-Parent), falls Mesh ausgewählt wurde
let
selectedObject
=
intersects
[
0
].
object
;
let
selectedObject
=
intersects
[
0
].
object
;
while
(
selectedObject
.
parent
&&
selectedObject
.
parent
!==
scene
)
{
while
(
selectedObject
.
parent
&&
selectedObject
.
parent
!==
scene
)
{
selectedObject
=
selectedObject
.
parent
;
selectedObject
=
selectedObject
.
parent
;
}
}
// Überprüfe, ob das ausgewählte Objekt das Reticle ist
// Überprüfe, ob das ausgewählte Objekt das Reticle ist
if
(
selectedObject
===
reticle
)
{
if
(
selectedObject
===
reticle
)
{
console
.
log
(
"
Reticle kann nicht ausgewählt werden.
"
);
console
.
log
(
"
Reticle kann nicht ausgewählt werden.
"
);
return
;
return
;
}
}
// Markiere das gesamte Modell als ausgewählt
// Markiere das gesamte Modell als ausgewählt
selectedPlacedModel
=
selectedObject
;
selectedPlacedModel
=
selectedObject
;
highlightSelectedModel
();
highlightSelectedModel
();
showMenu
(
"
edit-menu
"
);
showMenu
(
"
edit-menu
"
);
}
}
}
}
function
clearSelectedModel
()
{
function
clearSelectedModel
()
{
if
(
selectedPlacedModel
)
{
if
(
selectedPlacedModel
)
{
selectedPlacedModel
.
traverse
((
child
)
=>
{
selectedPlacedModel
.
traverse
((
child
)
=>
{
if
(
child
.
isMesh
)
{
if
(
child
.
isMesh
)
{
child
.
material
.
emissive
.
setHex
(
0x000000
);
// Markierung entfernen
child
.
material
.
emissive
.
setHex
(
0x000000
);
// Markierung entfernen
}
}
});
});
selectedPlacedModel
=
null
;
selectedPlacedModel
=
null
;
}
}
}
}
function
highlightSelectedModel
()
{
function
highlightSelectedModel
()
{
if
(
selectedPlacedModel
)
{
if
(
selectedPlacedModel
)
{
removeHighlightFromAllModels
();
removeHighlightFromAllModels
();
selectedPlacedModel
.
traverse
((
child
)
=>
{
selectedPlacedModel
.
traverse
((
child
)
=>
{
if
(
child
.
isMesh
)
{
if
(
child
.
isMesh
)
{
child
.
material
.
emissive
.
setHex
(
0xff0000
);
// Rote Hervorhebung
child
.
material
.
emissive
.
setHex
(
0xff0000
);
// Rote Hervorhebung
}
}
});
});
}
}
}
}
function
removeHighlightFromSelectedModel
()
{
function
removeHighlightFromSelectedModel
()
{
if
(
selectedPlacedModel
)
{
if
(
selectedPlacedModel
)
{
selectedPlacedModel
.
traverse
((
child
)
=>
{
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
()
{
function
removeHighlightFromAllModels
()
{
scene
.
traverse
((
child
)
=>
{
scene
.
traverse
((
child
)
=>
{
if
(
child
.
isMesh
&&
child
.
material
&&
child
.
material
.
emissive
)
{
if
(
child
.
isMesh
&&
child
.
material
&&
child
.
material
.
emissive
)
{
child
.
material
.
emissive
.
setHex
(
0x000000
);
// Markierung entfernen
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
)
{
function
deleteModel
()
{
const
deleteDialog
=
document
.
getElementById
(
'
delete-confirmation-dialog
'
);
if
(
!
selectedPlacedModel
)
{
deleteDialog
.
style
.
display
=
'
none
'
;
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
);
scene
.
remove
(
selectedPlacedModel
);
selectedPlacedModel
=
null
;
selectedPlacedModel
=
null
;
showMenu
(
'
menu-bar
'
);
showMenu
(
'
menu-bar
'
);
}
}
}
}
function
completeEditing
()
{
removeHighlightFromSelectedModel
();
function
completeEditing
()
{
closeDynamicMenu
();
removeHighlightFromSelectedModel
();
selectedPlacedModel
=
null
;
closeDynamicMenu
();
document
.
getElementById
(
'
edit-menu
'
).
style
.
display
=
'
none
'
;
selectedPlacedModel
=
null
;
document
.
getElementById
(
'
menu-bar
'
).
style
.
display
=
'
flex
'
;
document
.
getElementById
(
'
edit-menu
'
).
style
.
display
=
'
none
'
;
}
document
.
getElementById
(
'
menu-bar
'
).
style
.
display
=
'
flex
'
;
}
/* ========================= */
/* BEARBEITUNGS-MENÜS */
/* ========================= */
/* ========================= */
/* BEARBEITUNGS-MENÜS */
function
openRotationMenu
()
{
/* ========================= */
if
(
!
selectedPlacedModel
)
{
function
openRotationMenu
()
{
if
(
!
selectedPlacedModel
)
{
console
.
log
(
"
Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten.
"
);
console
.
log
(
"
Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten.
"
);
return
;
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
"
);
const
dynamicMenu
=
document
.
getElementById
(
"
dynamic-menu
"
);
dynamicMenu
.
style
.
display
=
"
flex
"
;
dynamicMenu
.
style
.
display
=
"
flex
"
;
dynamicMenu
.
innerHTML
=
`
dynamicMenu
.
innerHTML
=
`
<h3>Rotation anpassen</h3>
<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>
<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>
<button onclick="closeDynamicMenu()">Zurück</button>
`
;
`
;
}
}
function
updateRotation
(
axis
,
value
)
{
function
updateRotation
(
axis
,
value
)
{
if
(
selectedPlacedModel
)
{
if
(
selectedPlacedModel
)
{
const
radians
=
(
value
/
180
)
*
Math
.
PI
;
const
radians
=
(
value
/
180
)
*
Math
.
PI
;
selectedPlacedModel
.
rotation
[
axis
]
=
radians
;
selectedPlacedModel
.
rotation
[
axis
]
=
radians
;
// Anzeige der aktuellen Rotation im Dynamic-Menü aktualisieren
// Anzeige der aktuellen Rotation im Dynamic-Menü aktualisieren
const
currentRotationDisplay
=
document
.
getElementById
(
"
current-rotation
"
);
const
currentRotationDisplay
=
document
.
getElementById
(
"
current-rotation
"
);
if
(
currentRotationDisplay
)
{
if
(
currentRotationDisplay
)
{
currentRotationDisplay
.
textContent
=
value
;
// Zeige den aktuellen Wert in Grad an
currentRotationDisplay
.
textContent
=
value
;
// Zeige den aktuellen Wert in Grad an
}
}
}
}
}
}
function
openScaleMenu
()
{
function
openScaleMenu
()
{
if
(
!
selectedPlacedModel
)
{
if
(
!
selectedPlacedModel
)
{
console
.
log
(
"
Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten.
"
);
console
.
log
(
"
Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bearbeiten.
"
);
return
;
return
;
}
}
// Aktuelle Skalierung und Grenzen bestimmen
// Aktuelle Skalierung und Grenzen bestimmen
const
currentScale
=
selectedPlacedModel
.
scale
.
x
;
const
currentScale
=
selectedPlacedModel
.
scale
.
x
;
const
minScale
=
selectedPlacedModel
.
modelConfig
.
minScale
;
const
minScale
=
selectedPlacedModel
.
modelConfig
.
minScale
;
const
maxScale
=
selectedPlacedModel
.
modelConfig
.
maxScale
;
const
maxScale
=
selectedPlacedModel
.
modelConfig
.
maxScale
;
const
step
=
(
maxScale
-
minScale
)
/
100
;
// Dynamische Schrittgröße basierend auf Grenzen
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
"
);
const
dynamicMenu
=
document
.
getElementById
(
"
dynamic-menu
"
);
dynamicMenu
.
style
.
display
=
"
flex
"
;
dynamicMenu
.
style
.
display
=
"
flex
"
;
dynamicMenu
.
innerHTML
=
`
dynamicMenu
.
innerHTML
=
`
<h3>Skalierung anpassen</h3>
<h3>Skalierung anpassen</h3>
<label>Größe: <span id="scale-value">
${
currentScalePercent
.
toFixed
(
0
)}
%</span>
<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>
<button onclick="closeDynamicMenu()">Zurück</button>
`
;
`
;
}
}
function
updateScale
(
percentValue
,
minScale
,
maxScale
)
{
function
updateScale
(
percentValue
,
minScale
,
maxScale
)
{
if
(
selectedPlacedModel
)
{
if
(
selectedPlacedModel
)
{
// Berechnung der Skalierung basierend auf dem Prozentwert
// Berechnung der Skalierung basierend auf dem Prozentwert
const
scale
=
minScale
+
(
percentValue
/
100
)
*
(
maxScale
-
minScale
);
const
scale
=
minScale
+
(
percentValue
/
100
)
*
(
maxScale
-
minScale
);
selectedPlacedModel
.
scale
.
set
(
scale
,
scale
,
scale
);
selectedPlacedModel
.
scale
.
set
(
scale
,
scale
,
scale
);
...
@@ -340,207 +340,217 @@
...
@@ -340,207 +340,217 @@
// Anzeige des aktuellen Prozentsatzes im Dynamic-Menü aktualisieren
// Anzeige des aktuellen Prozentsatzes im Dynamic-Menü aktualisieren
const
scaleValueDisplay
=
document
.
getElementById
(
"
scale-value
"
);
const
scaleValueDisplay
=
document
.
getElementById
(
"
scale-value
"
);
if
(
scaleValueDisplay
)
{
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
()
{
function
openMoveMenu
()
{
if
(
!
selectedPlacedModel
)
{
if
(
!
selectedPlacedModel
)
{
console
.
log
(
"
Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bewegen.
"
);
console
.
log
(
"
Kein Modell ausgewählt. Bitte wählen Sie ein Modell aus, bevor Sie es bewegen.
"
);
return
;
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>`
;
}
}
}
}
/* ========================= */
const
dynamicMenu
=
document
.
getElementById
(
"
dynamic-menu
"
);
/* KARTENSTEUERUNG */
dynamicMenu
.
style
.
display
=
"
flex
"
;
/* ========================= */
function
refreshMapDialog
()
{
dynamicMenu
.
innerHTML
=
`
const
mapDialog
=
document
.
getElementById
(
'
map-dialog
'
);
<h3>Modell bewegen</h3>
mapDialog
.
style
.
display
=
'
flex
'
;
<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>
function
closeMapDialog
()
{
<button onclick="closeDynamicMenu()">Zurück</button>
const
mapDialog
=
document
.
getElementById
(
'
map-dialog
'
);
`
;
mapDialog
.
style
.
display
=
'
none
'
;
}
const
container
=
document
.
getElementById
(
"
joystick-container
"
);
const
knob
=
document
.
getElementById
(
"
joystick-knob
"
);
/* ========================= */
let
isDragging
=
false
;
/* AR-HANDLING */
/* ========================= */
const
center
=
{
x
:
container
.
offsetWidth
/
2
,
y
:
container
.
offsetHeight
/
2
};
async
function
activateXR
(
sceneData
=
null
)
{
const
maxDistance
=
container
.
offsetWidth
/
2
;
const
canvas
=
document
.
createElement
(
'
canvas
'
);
document
.
body
.
appendChild
(
canvas
);
knob
.
addEventListener
(
"
mousedown
"
,
()
=>
(
isDragging
=
true
));
const
gl
=
canvas
.
getContext
(
'
webgl
'
,
{
xrCompatible
:
true
});
document
.
addEventListener
(
"
mouseup
"
,
()
=>
{
const
renderer
=
new
THREE
.
WebGLRenderer
({
alpha
:
true
,
canvas
,
context
:
gl
});
isDragging
=
false
;
renderer
.
autoClear
=
false
;
knob
.
style
.
left
=
"
50%
"
;
knob
.
style
.
top
=
"
50%
"
;
scene
=
new
THREE
.
Scene
();
moveModelDynamic
(
'
x
'
,
0
);
// Bewegung stoppen, wenn Maus losgelassen wird
camera
=
new
THREE
.
PerspectiveCamera
();
moveModelDynamic
(
'
z
'
,
0
);
camera
.
matrixAutoUpdate
=
false
;
});
const
light
=
new
THREE
.
DirectionalLight
(
0xffffff
,
1
);
document
.
addEventListener
(
"
mousemove
"
,
(
event
)
=>
{
light
.
position
.
set
(
10
,
10
,
10
);
if
(
!
isDragging
)
return
;
scene
.
add
(
light
);
const
rect
=
container
.
getBoundingClientRect
();
const
loader
=
new
THREE
.
GLTFLoader
();
const
dx
=
event
.
clientX
-
rect
.
left
-
center
.
x
;
loader
.
load
(
"
https://immersive-web.github.io/webxr-samples/media/gltf/reticle/reticle.gltf
"
,
(
gltf
)
=>
{
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
=
gltf
.
scene
;
reticle
.
visible
=
false
;
reticle
.
visible
=
false
;
scene
.
add
(
reticle
);
scene
.
add
(
reticle
);
});
});
currentSession
=
await
navigator
.
xr
.
requestSession
(
'
immersive-ar
'
,
{
currentSession
=
await
navigator
.
xr
.
requestSession
(
'
immersive-ar
'
,
{
optionalFeatures
:
[
"
dom-overlay
"
],
optionalFeatures
:
[
"
dom-overlay
"
],
domOverlay
:
{
root
:
document
.
body
},
domOverlay
:
{
root
:
document
.
body
},
requiredFeatures
:
[
'
hit-test
'
]
requiredFeatures
:
[
'
hit-test
'
]
});
});
currentSession
.
updateRenderState
({
baseLayer
:
new
XRWebGLLayer
(
currentSession
,
gl
)
});
currentSession
.
updateRenderState
({
baseLayer
:
new
XRWebGLLayer
(
currentSession
,
gl
)
});
const
referenceSpace
=
await
currentSession
.
requestReferenceSpace
(
'
local
'
);
const
referenceSpace
=
await
currentSession
.
requestReferenceSpace
(
'
local
'
);
const
viewerSpace
=
await
currentSession
.
requestReferenceSpace
(
'
viewer
'
);
const
viewerSpace
=
await
currentSession
.
requestReferenceSpace
(
'
viewer
'
);
const
hitTestSource
=
await
currentSession
.
requestHitTestSource
({
space
:
viewerSpace
});
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
;
currentSession
=
null
;
document
.
getElementById
(
"
dynamic-menu
"
).
style
.
display
=
"
none
"
;
document
.
getElementById
(
"
dynamic-menu
"
).
style
.
display
=
"
none
"
;
menus
.
forEach
(
id
=>
{
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
=>
{
navigator
.
geolocation
.
getCurrentPosition
(
position
=>
{
geoLocation
=
{
geoLocation
=
{
latitude
:
roundTo
(
position
.
coords
.
latitude
,
5
),
latitude
:
roundTo
(
position
.
coords
.
latitude
,
5
),
longitude
:
roundTo
(
position
.
coords
.
longitude
,
5
),
longitude
:
roundTo
(
position
.
coords
.
longitude
,
5
),
};
};
console
.
log
(
"
GeoLocation:
"
+
JSON
.
stringify
(
geoLocation
));
console
.
log
(
"
GeoLocation:
"
+
JSON
.
stringify
(
geoLocation
));
if
(
sceneData
)
{
if
(
sceneData
)
{
sceneData
.
models
.
forEach
((
model
)
=>
{
sceneData
.
models
.
forEach
((
model
)
=>
{
if
(
model
.
name
)
{
if
(
model
.
name
)
{
const
filePath
=
Object
.
values
(
models
).
find
(
m
=>
model
.
name
===
m
.
name
).
file
;
const
filePath
=
Object
.
values
(
models
).
find
(
m
=>
model
.
name
===
m
.
name
).
file
;
const
{
x
,
z
}
=
leafletToThree
(
model
.
lat
,
model
.
lng
);
const
{
x
,
z
}
=
leafletToThree
(
model
.
lat
,
model
.
lng
);
const
positionVector
=
new
THREE
.
Vector3
(
x
,
model
.
position
.
y
,
z
);
const
positionVector
=
new
THREE
.
Vector3
(
x
,
model
.
position
.
y
,
z
);
placeModel
(
filePath
,
positionVector
,
(
placed
)
=>
{
placeModel
(
filePath
,
positionVector
,
(
placed
)
=>
{
if
(
model
.
rotation
)
{
if
(
model
.
rotation
)
{
placed
.
rotation
.
_x
=
model
.
rotation
.
_x
;
placed
.
rotation
.
_x
=
model
.
rotation
.
_x
;
placed
.
rotation
.
_y
=
model
.
rotation
.
_y
;
placed
.
rotation
.
_y
=
model
.
rotation
.
_y
;
placed
.
rotation
.
_z
=
model
.
rotation
.
_z
;
placed
.
rotation
.
_z
=
model
.
rotation
.
_z
;
}
}
if
(
model
.
scale
)
{
if
(
model
.
scale
)
{
placed
.
scale
.
x
=
model
.
scale
.
x
;
placed
.
scale
.
x
=
model
.
scale
.
x
;
placed
.
scale
.
y
=
model
.
scale
.
y
;
placed
.
scale
.
y
=
model
.
scale
.
y
;
placed
.
scale
.
z
=
model
.
scale
.
z
;
placed
.
scale
.
z
=
model
.
scale
.
z
;
}
}
});
});
}
}
});
});
}
}
},
function
(
error
)
{
},
function
(
error
)
{
console
.
error
(
"
Fehler bei der Geolokalisierung:
"
,
JSON
.
stringify
(
error
));
console
.
error
(
"
Fehler bei der Geolokalisierung:
"
,
JSON
.
stringify
(
error
));
},
{
enableHighAccuracy
:
true
,
maximumAge
:
2000
,
timeout
:
5000
});
},
{
enableHighAccuracy
:
true
,
maximumAge
:
2000
,
timeout
:
5000
});
}
}
currentSession
.
requestAnimationFrame
(
function
onXRFrame
(
time
,
frame
)
{
currentSession
.
requestAnimationFrame
(
function
onXRFrame
(
time
,
frame
)
{
currentSession
.
requestAnimationFrame
(
onXRFrame
);
currentSession
.
requestAnimationFrame
(
onXRFrame
);
gl
.
bindFramebuffer
(
gl
.
FRAMEBUFFER
,
currentSession
.
renderState
.
baseLayer
.
framebuffer
);
gl
.
bindFramebuffer
(
gl
.
FRAMEBUFFER
,
currentSession
.
renderState
.
baseLayer
.
framebuffer
);
const
pose
=
frame
.
getViewerPose
(
referenceSpace
);
const
pose
=
frame
.
getViewerPose
(
referenceSpace
);
if
(
pose
)
{
if
(
pose
)
{
const
view
=
pose
.
views
[
0
];
const
view
=
pose
.
views
[
0
];
const
viewport
=
currentSession
.
renderState
.
baseLayer
.
getViewport
(
view
);
const
viewport
=
currentSession
.
renderState
.
baseLayer
.
getViewport
(
view
);
renderer
.
setSize
(
viewport
.
width
,
viewport
.
height
);
renderer
.
setSize
(
viewport
.
width
,
viewport
.
height
);
camera
.
matrix
.
fromArray
(
view
.
transform
.
matrix
);
camera
.
matrix
.
fromArray
(
view
.
transform
.
matrix
);
camera
.
projectionMatrix
.
fromArray
(
view
.
projectionMatrix
);
camera
.
projectionMatrix
.
fromArray
(
view
.
projectionMatrix
);
camera
.
updateMatrixWorld
(
true
);
camera
.
updateMatrixWorld
(
true
);
const
hitTestResults
=
frame
.
getHitTestResults
(
hitTestSource
);
const
hitTestResults
=
frame
.
getHitTestResults
(
hitTestSource
);
if
(
hitTestResults
.
length
>
0
)
{
if
(
hitTestResults
.
length
>
0
)
{
const
hitPose
=
hitTestResults
[
0
].
getPose
(
referenceSpace
);
const
hitPose
=
hitTestResults
[
0
].
getPose
(
referenceSpace
);
reticle
.
visible
=
true
;
reticle
.
visible
=
true
;
reticle
.
position
.
set
(
hitPose
.
transform
.
position
.
x
,
hitPose
.
transform
.
position
.
y
,
hitPose
.
transform
.
position
.
z
);
reticle
.
position
.
set
(
hitPose
.
transform
.
position
.
x
,
hitPose
.
transform
.
position
.
y
,
hitPose
.
transform
.
position
.
z
);
reticle
.
updateMatrixWorld
(
true
);
reticle
.
updateMatrixWorld
(
true
);
}
}
renderer
.
render
(
scene
,
camera
);
renderer
.
render
(
scene
,
camera
);
}
}
});
});
}
}
function
exitAR
()
{
function
exitAR
()
{
document
.
getElementById
(
'
confirmation-dialog
'
).
style
.
display
=
'
flex
'
;
document
.
getElementById
(
'
confirmation-dialog
'
).
style
.
display
=
'
flex
'
;
}
}
function
confirmExit
(
shouldExit
)
{
function
confirmExit
(
shouldExit
)
{
if
(
shouldExit
&&
currentSession
)
currentSession
.
end
();
if
(
shouldExit
&&
currentSession
)
currentSession
.
end
();
document
.
getElementById
(
'
confirmation-dialog
'
).
style
.
display
=
'
none
'
;
document
.
getElementById
(
'
confirmation-dialog
'
).
style
.
display
=
'
none
'
;
}
}
/* ========================= */
/* ========================= */
/* BENUTZERINTERAKTIONEN */
/* BENUTZERINTERAKTIONEN */
/* ========================= */
/* ========================= */
let
soundTimeout
=
false
;
let
soundTimeout
=
false
;
function
playButtonSound
()
{
function
playButtonSound
()
{
if
(
!
soundTimeout
)
{
if
(
!
soundTimeout
)
{
const
sound
=
document
.
getElementById
(
"
button-sound
"
);
const
sound
=
document
.
getElementById
(
"
button-sound
"
);
sound
.
currentTime
=
0
;
sound
.
currentTime
=
0
;
sound
.
play
();
sound
.
play
();
soundTimeout
=
true
;
soundTimeout
=
true
;
setTimeout
(()
=>
{
setTimeout
(()
=>
{
soundTimeout
=
false
;
soundTimeout
=
false
;
},
200
);
// Verzögerung von 200ms
},
200
);
// Verzögerung von 200ms
}
}
}
}
This diff is collapsed.
Click to expand it.
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment
Menu
Explore
Projects
Groups
Snippets