Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
HFT-informatics-weekend
AR - Urban Planner
Commits
7d1e0011
Commit
7d1e0011
authored
Oct 11, 2025
by
Kang
Browse files
add comments for better understnading and add the camera button
parent
2761c9ae
Changes
1
Hide whitespace changes
Inline
Side-by-side
webxr-cube/src/components/XRRenderer.tsx
View file @
7d1e0011
// Import necessary tools and libraries
import
React
,
{
useEffect
,
useRef
,
useState
}
from
"
react
"
;
import
*
as
THREE
from
"
three
"
;
import
{
XRButton
}
from
"
three/examples/jsm/webxr/XRButton
"
;
import
{
BlockManager
}
from
"
./BlockManager
"
;
import
{
Cube
}
from
"
../model/Cube
"
;
import
{
GLTFModel
}
from
"
../model/GLTFModel
"
;
import
{
ARBottomBar
}
from
"
./ARBottomBar
"
;
import
{
BlockManager
}
from
"
./BlockManager
"
;
// manages placed 3D objects in the scene
import
{
Cube
}
from
"
../model/Cube
"
;
// custom class for a simple cube
import
{
GLTFModel
}
from
"
../model/GLTFModel
"
;
// custom loader for .glb 3D models
import
{
ARBottomBar
}
from
"
./ARBottomBar
"
;
// bottom UI for selecting objects
export
const
XRRenderer
:
React
.
FC
=
()
=>
{
const
mountRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
overlayRef
=
useRef
<
HTMLDivElement
>
(
null
);
const
[
isSessionActive
,
setIsSessionActive
]
=
useState
(
false
);
const
[
selectedObject
,
setSelectedObject
]
=
useState
<
string
>
(
"
Cube
"
);
// References to DOM elements (we connect three.js canvas + overlay here)
const
mountRef
=
useRef
<
HTMLDivElement
>
(
null
);
// the div that holds the canvas
const
overlayRef
=
useRef
<
HTMLDivElement
>
(
null
);
// the overlay UI inside AR (buttons etc.)
const
reticleRef
=
useRef
<
THREE
.
Mesh
|
null
>
(
null
);
// reference to the green reticle ring
// State variables
const
[
isSessionActive
,
setIsSessionActive
]
=
useState
(
false
);
// true when AR is running
const
[
selectedObject
,
setSelectedObject
]
=
useState
<
string
>
(
"
Cube
"
);
// default object type
// The main setup logic — runs once when component mounts
useEffect
(()
=>
{
if
(
!
mountRef
.
current
||
!
overlayRef
.
current
)
return
;
// --- Scene setup ---
// ---
1️⃣
Scene setup ---
const
scene
=
new
THREE
.
Scene
();
scene
.
background
=
null
;
scene
.
background
=
null
;
// WebXR automatically shows camera feed behind 3D scene
// Camera: standard perspective camera
const
camera
=
new
THREE
.
PerspectiveCamera
(
70
,
window
.
innerWidth
/
window
.
innerHeight
,
0.01
,
20
70
,
// field of view (in degrees)
window
.
innerWidth
/
window
.
innerHeight
,
// aspect ratio
0.01
,
// near clipping plane
20
// far clipping plane
);
const
renderer
=
new
THREE
.
WebGLRenderer
({
antialias
:
true
,
alpha
:
true
});
// Renderer: where 3D things are drawn
const
renderer
=
new
THREE
.
WebGLRenderer
({
antialias
:
true
,
// smooth edges
alpha
:
true
,
// allow transparency (so camera feed is visible)
preserveDrawingBuffer
:
true
,
// so we can take screenshots later
});
renderer
.
setSize
(
window
.
innerWidth
,
window
.
innerHeight
);
renderer
.
xr
.
enabled
=
true
;
renderer
.
xr
.
setReferenceSpaceType
(
"
local-floor
"
);
mountRef
.
current
.
appendChild
(
renderer
.
domElement
);
renderer
.
xr
.
enabled
=
true
;
// turn on WebXR support
renderer
.
xr
.
setReferenceSpaceType
(
"
local-floor
"
);
// reference at floor level
mountRef
.
current
.
appendChild
(
renderer
.
domElement
);
// attach to our div
// ---
AR Button
---
// ---
2️⃣ Create AR Button (to enter AR mode)
---
const
button
=
XRButton
.
createButton
(
renderer
,
{
requiredFeatures
:
[
"
hit-test
"
,
"
dom-overlay
"
],
optionalFeatures
:
[
"
local-floor
"
],
domOverlay
:
{
root
:
overlayRef
.
current
},
requiredFeatures
:
[
"
hit-test
"
,
"
dom-overlay
"
],
// needed to detect planes + show UI overlay
optionalFeatures
:
[
"
local-floor
"
],
// extra feature (some devices only)
domOverlay
:
{
root
:
overlayRef
.
current
},
// attach HTML overlay (UI)
});
mountRef
.
current
.
appendChild
(
button
);
// ---
L
ighting ---
const
light
=
new
THREE
.
AmbientLight
(
0xffffff
,
0.5
);
// ---
3️⃣ Add simple l
ighting ---
const
light
=
new
THREE
.
AmbientLight
(
0xffffff
,
0.5
);
// soft white light
scene
.
add
(
light
);
// --- Block manager ---
// ---
4️⃣
Block manager
(handles placed 3D objects)
---
const
blockManager
=
new
BlockManager
(
scene
);
// ---
Placement indicator (reticle
) ---
// ---
5️⃣ Create reticle (green ring where we can place objects
) ---
const
reticleGeometry
=
new
THREE
.
RingGeometry
(
0.1
,
0.12
,
32
);
const
reticleMaterial
=
new
THREE
.
MeshBasicMaterial
({
color
:
0x00ff00
});
const
reticle
=
new
THREE
.
Mesh
(
reticleGeometry
,
reticleMaterial
);
reticle
.
rotation
.
x
=
-
Math
.
PI
/
2
;
reticle
.
visible
=
false
;
reticle
.
rotation
.
x
=
-
Math
.
PI
/
2
;
// lay flat on the ground
reticle
.
visible
=
false
;
// hidden until we detect a plane
scene
.
add
(
reticle
);
reticleRef
.
current
=
reticle
;
// save for later reference
// ---
H
it-test setup ---
// ---
6️⃣ WebXR h
it-test setup
(detect real-world planes)
---
let
hitTestSource
:
XRHitTestSource
|
null
=
null
;
let
localRefSpace
:
XRReferenceSpace
|
null
=
null
;
// Called when AR session starts (when user clicks "Enter AR")
const
sessionStartHandler
=
async
()
=>
{
setIsSessionActive
(
true
);
setIsSessionActive
(
true
);
// show bottom UI
const
session
=
renderer
.
xr
.
getSession
();
// Reference space: maps AR coordinates to real world
localRefSpace
=
await
session
.
requestReferenceSpace
(
"
local-floor
"
);
// Viewer space: based on device’s camera position
const
viewerSpace
=
await
session
.
requestReferenceSpace
(
"
viewer
"
);
// Hit-test source: sends out rays from device to find planes
hitTestSource
=
await
session
.
requestHitTestSource
({
space
:
viewerSpace
});
};
// Called when AR session ends
const
sessionEndHandler
=
()
=>
{
setIsSessionActive
(
false
);
hitTestSource
=
null
;
localRefSpace
=
null
;
reticle
.
visible
=
false
;
reticle
.
visible
=
false
;
// hide reticle when AR stops
};
// Listen to AR session start/end
renderer
.
xr
.
addEventListener
(
"
sessionstart
"
,
sessionStartHandler
);
renderer
.
xr
.
addEventListener
(
"
sessionend
"
,
sessionEndHandler
);
// ---
Handle resize
---
// ---
7️⃣ Adjust to window size changes
---
const
handleResize
=
()
=>
{
camera
.
aspect
=
window
.
innerWidth
/
window
.
innerHeight
;
camera
.
updateProjectionMatrix
();
...
...
@@ -86,14 +108,18 @@ export const XRRenderer: React.FC = () => {
};
window
.
addEventListener
(
"
resize
"
,
handleResize
);
// --- Animation loop ---
// ---
8️⃣
Animation loop
(runs every frame)
---
renderer
.
setAnimationLoop
((
timestamp
,
frame
)
=>
{
// Only run hit-test logic if AR session and frame exist
if
(
frame
&&
hitTestSource
&&
localRefSpace
)
{
// Check if there’s a detected surface (like a floor or table)
const
hitTestResults
=
frame
.
getHitTestResults
(
hitTestSource
);
if
(
hitTestResults
.
length
>
0
)
{
// Take the first hit and get its position in space
const
hit
=
hitTestResults
[
0
];
const
pose
=
hit
.
getPose
(
localRefSpace
);
if
(
pose
)
{
// Move the reticle to that position
reticle
.
visible
=
true
;
reticle
.
position
.
set
(
pose
.
transform
.
position
.
x
,
...
...
@@ -102,20 +128,23 @@ export const XRRenderer: React.FC = () => {
);
}
}
else
{
// No surface found → hide the reticle
reticle
.
visible
=
false
;
}
}
// Render 3D scene on each frame
renderer
.
render
(
scene
,
camera
);
});
//
=== Function to add object at reticle position ===
//
--- 9️⃣ Add object to the scene when user selects one ---
const
addSelectedObject
=
(
name
:
string
)
=>
{
if
(
!
reticle
.
visible
)
return
;
//
d
on
't
place
in mid-air
if
(
!
reticle
.
visible
)
return
;
// on
ly
place
when reticle is visible
const
position
=
reticle
.
position
.
clone
();
const
position
=
reticle
.
position
.
clone
();
// get current reticle position
let
objectToAdd
;
// Choose what object to add based on selection
if
(
name
===
"
Cube
"
)
{
objectToAdd
=
new
Cube
(
position
);
}
else
if
(
name
===
"
Watermill
"
)
{
...
...
@@ -123,38 +152,109 @@ export const XRRenderer: React.FC = () => {
}
else
if
(
name
===
"
Tree
"
)
{
objectToAdd
=
new
GLTFModel
(
"
/tree.glb
"
,
position
,
"
/Textures/variation-a.png
"
);
}
else
{
objectToAdd
=
new
Cube
(
position
);
objectToAdd
=
new
Cube
(
position
);
// fallback
}
// Add the object to the scene (via manager)
blockManager
.
addObject
(
objectToAdd
);
};
// Expose function for debug
// Expose function globally for debugging in browser console
// You can type `window.addSelectedObject("Cube")` to spawn manually
(
window
as
any
).
addSelectedObject
=
addSelectedObject
;
// --- Cleanup ---
// ---
🔚
Cleanup
when component unmounts
---
return
()
=>
{
renderer
.
xr
.
removeEventListener
(
"
sessionstart
"
,
sessionStartHandler
);
renderer
.
xr
.
removeEventListener
(
"
sessionend
"
,
sessionEndHandler
);
window
.
removeEventListener
(
"
resize
"
,
handleResize
);
renderer
.
dispose
();
renderer
.
dispose
();
// free GPU memory
mountRef
.
current
?.
removeChild
(
renderer
.
domElement
);
if
(
reticleRef
.
current
===
reticle
)
reticleRef
.
current
=
null
;
};
},
[]);
},
[]);
// empty dependency array → runs only once
// --- 🔸 Screenshot function ---
const
captureAndDownload
=
()
=>
{
// Get the <canvas> element from the renderer
const
canvas
=
(
mountRef
.
current
?.
querySelector
(
"
canvas
"
)
||
document
.
querySelector
(
"
canvas
"
))
as
HTMLCanvasElement
|
null
;
if
(
!
canvas
||
!
overlayRef
.
current
)
return
;
// Hide UI overlay for a clean capture
const
overlayEl
=
overlayRef
.
current
;
const
prevPointer
=
overlayEl
.
style
.
pointerEvents
;
const
prevOpacity
=
overlayEl
.
style
.
opacity
;
overlayEl
.
style
.
pointerEvents
=
"
none
"
;
overlayEl
.
style
.
opacity
=
"
0
"
;
// --- When user clicks an object in the bottom bar ---
// Hide reticle (optional)
const
reticle
=
reticleRef
.
current
;
const
prevReticleVisible
=
reticle
?.
visible
??
false
;
if
(
reticle
)
reticle
.
visible
=
false
;
// Wait one frame → take screenshot
requestAnimationFrame
(()
=>
{
canvas
.
toBlob
((
blob
)
=>
{
// Restore UI and reticle visibility
overlayEl
.
style
.
pointerEvents
=
prevPointer
;
overlayEl
.
style
.
opacity
=
prevOpacity
;
if
(
reticle
)
reticle
.
visible
=
prevReticleVisible
;
if
(
!
blob
)
return
;
// Create download link
const
url
=
URL
.
createObjectURL
(
blob
);
const
a
=
document
.
createElement
(
"
a
"
);
const
ts
=
new
Date
().
toISOString
().
replace
(
/
[
:.
]
/g
,
"
-
"
);
a
.
href
=
url
;
a
.
download
=
`ar-snapshot-
${
ts
}
.png`
;
document
.
body
.
appendChild
(
a
);
a
.
click
();
// trigger download
a
.
remove
();
URL
.
revokeObjectURL
(
url
);
},
"
image/png
"
);
});
};
// --- 🔸 Handle user selection from bottom bar ---
const
handleSelect
=
(
name
:
string
)
=>
{
setSelectedObject
(
name
);
(
window
as
any
).
addSelectedObject
?.(
name
);
setSelectedObject
(
name
);
// update state
(
window
as
any
).
addSelectedObject
?.(
name
);
// add object immediately
};
// --- 🔸 Render layout ---
return
(
<
div
style
=
{
{
width
:
"
100vw
"
,
height
:
"
100vh
"
,
position
:
"
relative
"
}
}
>
{
/* where the WebGL canvas mounts */
}
<
div
ref
=
{
mountRef
}
style
=
{
{
width
:
"
100%
"
,
height
:
"
100%
"
}
}
/>
{
/* AR overlay that floats above 3D view */
}
<
div
ref
=
{
overlayRef
}
style
=
{
{
position
:
"
absolute
"
,
width
:
"
100%
"
,
height
:
"
100%
"
}
}
>
{
/* 📷 Screenshot button (top-right corner) */
}
<
button
onClick
=
{
captureAndDownload
}
aria-label
=
"Save snapshot"
style
=
{
{
position
:
"
absolute
"
,
top
:
12
,
right
:
12
,
zIndex
:
1000
,
padding
:
"
10px 12px
"
,
borderRadius
:
12
,
border
:
"
none
"
,
background
:
"
white
"
,
fontSize
:
18
,
boxShadow
:
"
0 2px 8px rgba(0,0,0,0.25)
"
,
cursor
:
"
pointer
"
,
}
}
>
📷
</
button
>
{
/* Bottom bar appears only during active AR session */
}
{
isSessionActive
&&
(
<
ARBottomBar
objectOptions
=
{
[
...
...
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