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
e3227928
Commit
e3227928
authored
Oct 11, 2025
by
Stoll
Browse files
Placement improvement
parent
ff804daa
Changes
2
Hide whitespace changes
Inline
Side-by-side
webxr-cube/src/components/XRRenderer.tsx
View file @
e3227928
...
...
@@ -30,13 +30,13 @@ export const XRRenderer: React.FC = () => {
const
renderer
=
new
THREE
.
WebGLRenderer
({
antialias
:
true
,
alpha
:
true
});
renderer
.
setSize
(
window
.
innerWidth
,
window
.
innerHeight
);
renderer
.
xr
.
enabled
=
true
;
renderer
.
xr
.
setReferenceSpaceType
(
"
local
"
);
renderer
.
xr
.
setReferenceSpaceType
(
"
local
-floor
"
);
mountRef
.
current
.
appendChild
(
renderer
.
domElement
);
// --- AR
b
utton ---
// --- AR
B
utton ---
const
button
=
XRButton
.
createButton
(
renderer
,
{
requiredFeatures
:
[
"
hit-test
"
,
"
dom-overlay
"
],
optionalFeatures
:
[
"
anch
or
s
"
],
optionalFeatures
:
[
"
local-flo
or
"
],
domOverlay
:
{
root
:
overlayRef
.
current
},
});
mountRef
.
current
.
appendChild
(
button
);
...
...
@@ -48,76 +48,37 @@ export const XRRenderer: React.FC = () => {
// --- Block manager ---
const
blockManager
=
new
BlockManager
(
scene
);
// ---
Reticle for placement
---
const
reticle
=
new
THREE
.
Mesh
(
new
THREE
.
RingGeometry
(
0.08
,
0.1
,
32
).
rotateX
(
-
Math
.
PI
/
2
),
new
THREE
.
MeshBasicMaterial
({
color
:
0x00ff00
})
)
;
// ---
Placement indicator (reticle)
---
const
reticle
Geometry
=
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
;
scene
.
add
(
reticle
);
// --- Hit-test
variables
---
// --- Hit-test
setup
---
let
hitTestSource
:
XRHitTestSource
|
null
=
null
;
let
localRefSpace
:
XRReferenceSpace
|
null
=
null
;
let
latestHitPose
:
Float32Array
|
null
=
null
;
// --- Session start ---
const
sessionStartHandler
=
async
()
=>
{
setIsSessionActive
(
true
);
const
session
=
renderer
.
xr
.
getSession
();
localRefSpace
=
await
session
.
requestReferenceSpace
(
"
local
"
);
localRefSpace
=
await
session
.
requestReferenceSpace
(
"
local
-floor
"
);
const
viewerSpace
=
await
session
.
requestReferenceSpace
(
"
viewer
"
);
hitTestSource
=
await
session
.
requestHitTestSource
({
space
:
viewerSpace
});
// --- Tap to place ---
session
.
addEventListener
(
"
select
"
,
async
()
=>
{
if
(
!
latestHitPose
)
return
;
const
position
=
new
THREE
.
Vector3
(
latestHitPose
[
0
],
latestHitPose
[
1
],
latestHitPose
[
2
]
);
let
objectToAdd
;
if
(
selectedObject
===
"
Cube
"
)
{
objectToAdd
=
new
Cube
(
position
);
}
else
if
(
selectedObject
===
"
Watermill
"
)
{
objectToAdd
=
new
GLTFModel
(
"
/watermill.glb
"
,
position
);
}
else
if
(
selectedObject
===
"
Tree
"
)
{
objectToAdd
=
new
GLTFModel
(
"
/tree.glb
"
,
position
);
}
else
{
objectToAdd
=
new
Cube
(
position
);
}
blockManager
.
addObject
(
objectToAdd
);
// --- Optionally create an anchor so it stays fixed in the world ---
if
(
session
.
requestAnchor
&&
position
)
{
const
anchorPose
=
new
XRRigidTransform
(
{
x
:
position
.
x
,
y
:
position
.
y
,
z
:
position
.
z
}
);
try
{
await
session
.
requestAnchor
(
anchorPose
,
localRefSpace
);
}
catch
(
e
)
{
console
.
warn
(
"
Anchors not supported:
"
,
e
);
}
}
});
};
const
sessionEndHandler
=
()
=>
{
setIsSessionActive
(
false
);
hitTestSource
=
null
;
localRefSpace
=
null
;
latestHitPose
=
null
;
reticle
.
visible
=
false
;
};
renderer
.
xr
.
addEventListener
(
"
sessionstart
"
,
sessionStartHandler
);
renderer
.
xr
.
addEventListener
(
"
sessionend
"
,
sessionEndHandler
);
// ---
Resize handling
---
// ---
Handle resize
---
const
handleResize
=
()
=>
{
camera
.
aspect
=
window
.
innerWidth
/
window
.
innerHeight
;
camera
.
updateProjectionMatrix
();
...
...
@@ -132,21 +93,13 @@ export const XRRenderer: React.FC = () => {
if
(
hitTestResults
.
length
>
0
)
{
const
hit
=
hitTestResults
[
0
];
const
pose
=
hit
.
getPose
(
localRefSpace
);
if
(
pose
)
{
const
mat
=
new
THREE
.
Matrix4
().
fromArray
(
pose
.
transform
.
matrix
);
reticle
.
visible
=
true
;
reticle
.
matrix
.
fromArray
(
pose
.
transform
.
matrix
);
reticle
.
matrix
.
decompose
(
reticle
.
position
,
reticle
.
quaternion
,
reticle
.
scale
reticle
.
position
.
set
(
pose
.
transform
.
position
.
x
,
pose
.
transform
.
position
.
y
,
pose
.
transform
.
position
.
z
);
latestHitPose
=
new
Float32Array
([
pose
.
transform
.
position
.
x
,
pose
.
transform
.
position
.
y
,
pose
.
transform
.
position
.
z
,
]);
}
}
else
{
reticle
.
visible
=
false
;
...
...
@@ -156,6 +109,29 @@ export const XRRenderer: React.FC = () => {
renderer
.
render
(
scene
,
camera
);
});
// === Function to add object at reticle position ===
const
addSelectedObject
=
(
name
:
string
)
=>
{
if
(
!
reticle
.
visible
)
return
;
// don't place in mid-air
const
position
=
reticle
.
position
.
clone
();
let
objectToAdd
;
if
(
name
===
"
Cube
"
)
{
objectToAdd
=
new
Cube
(
position
);
}
else
if
(
name
===
"
Watermill
"
)
{
objectToAdd
=
new
GLTFModel
(
"
/watermill.glb
"
,
position
,
"
/Textures/variation-a.png
"
);
}
else
if
(
name
===
"
Tree
"
)
{
objectToAdd
=
new
GLTFModel
(
"
/tree.glb
"
,
position
,
"
/Textures/variation-a.png
"
);
}
else
{
objectToAdd
=
new
Cube
(
position
);
}
blockManager
.
addObject
(
objectToAdd
);
};
// Expose function for debug
(
window
as
any
).
addSelectedObject
=
addSelectedObject
;
// --- Cleanup ---
return
()
=>
{
renderer
.
xr
.
removeEventListener
(
"
sessionstart
"
,
sessionStartHandler
);
...
...
@@ -164,9 +140,13 @@ export const XRRenderer: React.FC = () => {
renderer
.
dispose
();
mountRef
.
current
?.
removeChild
(
renderer
.
domElement
);
};
},
[
selectedObject
]);
},
[]);
const
handleSelect
=
(
name
:
string
)
=>
setSelectedObject
(
name
);
// --- When user clicks an object in the bottom bar ---
const
handleSelect
=
(
name
:
string
)
=>
{
setSelectedObject
(
name
);
(
window
as
any
).
addSelectedObject
?.(
name
);
};
return
(
<
div
style
=
{
{
width
:
"
100vw
"
,
height
:
"
100vh
"
,
position
:
"
relative
"
}
}
>
...
...
webxr-cube/src/model/GLTFModel.ts
View file @
e3227928
...
...
@@ -3,50 +3,85 @@ import type { IRenderable } from "./IRenderable";
import
{
GLTFLoader
}
from
"
three/examples/jsm/loaders/GLTFLoader
"
;
export
class
GLTFModel
implements
IRenderable
{
mesh
:
THREE
.
Object3D
;
position
:
THREE
.
Vector3
;
constructor
(
url
:
string
,
position
:
THREE
.
Vector3
,
onLoad
?:
()
=>
void
)
{
this
.
mesh
=
new
THREE
.
Group
();
// placeholder until loaded
this
.
position
=
position
.
clone
();
this
.
mesh
.
position
.
copy
(
this
.
position
);
// Check if the file exists
fetch
(
url
,
{
method
:
"
HEAD
"
})
.
then
((
response
)
=>
{
if
(
!
response
.
ok
)
{
// Show popup if file not found
alert
(
`GLB file not found:
${
url
}
`
);
throw
new
Error
(
`GLB file not found:
${
url
}
`
);
}
// Load the GLB
const
loader
=
new
GLTFLoader
();
loader
.
load
(
url
,
(
gltf
)
=>
{
this
.
mesh
.
add
(
gltf
.
scene
);
gltf
.
scene
.
scale
.
set
(
0.5
,
0.5
,
0.5
);
// adjust scale
if
(
onLoad
)
onLoad
();
},
undefined
,
(
error
)
=>
{
alert
(
`Error loading GLB:
${
error
}
`
);
console
.
error
(
"
Error loading GLTF model:
"
,
error
);
}
);
})
.
catch
((
err
)
=>
{
console
.
error
(
err
.
message
);
});
}
setPosition
(
position
:
THREE
.
Vector3
)
{
this
.
position
.
copy
(
position
);
this
.
mesh
.
position
.
copy
(
position
);
}
enableInteraction
()
{
// Placeholder for drag-and-drop logic
}
mesh
:
THREE
.
Object3D
;
position
:
THREE
.
Vector3
;
constructor
(
url
:
string
,
position
:
THREE
.
Vector3
,
textureUrl
?:
string
,
// optional PNG texture
onLoad
?:
()
=>
void
)
{
this
.
mesh
=
new
THREE
.
Group
();
// placeholder until loaded
this
.
position
=
position
.
clone
();
this
.
mesh
.
position
.
copy
(
this
.
position
);
// Check if the file exists
fetch
(
url
,
{
method
:
"
HEAD
"
})
.
then
((
response
)
=>
{
if
(
!
response
.
ok
)
{
alert
(
`GLB file not found:
${
url
}
`
);
throw
new
Error
(
`GLB file not found:
${
url
}
`
);
}
const
loader
=
new
GLTFLoader
();
loader
.
load
(
url
,
(
gltf
)
=>
{
// If a texture URL is provided, load it and apply
if
(
textureUrl
)
{
const
textureLoader
=
new
THREE
.
TextureLoader
();
textureLoader
.
load
(
textureUrl
,
(
texture
)
=>
{
//texture.encoding = THREE.sRGBEncoding;
gltf
.
scene
.
traverse
((
child
)
=>
{
if
((
child
as
THREE
.
Mesh
).
isMesh
)
{
const
mesh
=
child
as
THREE
.
Mesh
;
const
material
=
new
THREE
.
MeshStandardMaterial
({
map
:
texture
,
});
mesh
.
material
=
material
;
}
});
this
.
mesh
.
add
(
gltf
.
scene
);
gltf
.
scene
.
scale
.
set
(
0.5
,
0.5
,
0.5
);
if
(
onLoad
)
onLoad
();
},
undefined
,
(
err
)
=>
{
console
.
error
(
"
Error loading texture:
"
,
err
);
alert
(
"
Failed to load texture:
"
+
textureUrl
);
}
);
}
else
{
// No texture: just add model
this
.
mesh
.
add
(
gltf
.
scene
);
gltf
.
scene
.
scale
.
set
(
0.5
,
0.5
,
0.5
);
if
(
onLoad
)
onLoad
();
}
},
undefined
,
(
error
)
=>
{
alert
(
`Error loading GLB:
${
error
}
`
);
console
.
error
(
"
Error loading GLTF model:
"
,
error
);
}
);
})
.
catch
((
err
)
=>
{
console
.
error
(
err
.
message
);
});
}
setPosition
(
position
:
THREE
.
Vector3
)
{
this
.
position
.
copy
(
position
);
this
.
mesh
.
position
.
copy
(
position
);
}
enableInteraction
()
{
// Placeholder for drag-and-drop logic
}
}
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