Commit e3227928 authored by Stoll's avatar Stoll
Browse files

Placement improvement

parent ff804daa
...@@ -30,13 +30,13 @@ export const XRRenderer: React.FC = () => { ...@@ -30,13 +30,13 @@ export const XRRenderer: React.FC = () => {
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight); renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true; renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType("local"); renderer.xr.setReferenceSpaceType("local-floor");
mountRef.current.appendChild(renderer.domElement); mountRef.current.appendChild(renderer.domElement);
// --- AR button --- // --- AR Button ---
const button = XRButton.createButton(renderer, { const button = XRButton.createButton(renderer, {
requiredFeatures: ["hit-test", "dom-overlay"], requiredFeatures: ["hit-test", "dom-overlay"],
optionalFeatures: ["anchors"], optionalFeatures: ["local-floor"],
domOverlay: { root: overlayRef.current }, domOverlay: { root: overlayRef.current },
}); });
mountRef.current.appendChild(button); mountRef.current.appendChild(button);
...@@ -48,76 +48,37 @@ export const XRRenderer: React.FC = () => { ...@@ -48,76 +48,37 @@ export const XRRenderer: React.FC = () => {
// --- Block manager --- // --- Block manager ---
const blockManager = new BlockManager(scene); const blockManager = new BlockManager(scene);
// --- Reticle for placement --- // --- Placement indicator (reticle) ---
const reticle = new THREE.Mesh( const reticleGeometry = new THREE.RingGeometry(0.1, 0.12, 32);
new THREE.RingGeometry(0.08, 0.1, 32).rotateX(-Math.PI / 2), const reticleMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
new THREE.MeshBasicMaterial({ color: 0x00ff00 }) const reticle = new THREE.Mesh(reticleGeometry, reticleMaterial);
); reticle.rotation.x = -Math.PI / 2;
reticle.visible = false; reticle.visible = false;
scene.add(reticle); scene.add(reticle);
// --- Hit-test variables --- // --- Hit-test setup ---
let hitTestSource: XRHitTestSource | null = null; let hitTestSource: XRHitTestSource | null = null;
let localRefSpace: XRReferenceSpace | null = null; let localRefSpace: XRReferenceSpace | null = null;
let latestHitPose: Float32Array | null = null;
// --- Session start ---
const sessionStartHandler = async () => { const sessionStartHandler = async () => {
setIsSessionActive(true); setIsSessionActive(true);
const session = renderer.xr.getSession(); const session = renderer.xr.getSession();
localRefSpace = await session.requestReferenceSpace("local"); localRefSpace = await session.requestReferenceSpace("local-floor");
const viewerSpace = await session.requestReferenceSpace("viewer"); const viewerSpace = await session.requestReferenceSpace("viewer");
hitTestSource = await session.requestHitTestSource({ space: viewerSpace }); 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 = () => { const sessionEndHandler = () => {
setIsSessionActive(false); setIsSessionActive(false);
hitTestSource = null; hitTestSource = null;
localRefSpace = null; localRefSpace = null;
latestHitPose = null;
reticle.visible = false; reticle.visible = false;
}; };
renderer.xr.addEventListener("sessionstart", sessionStartHandler); renderer.xr.addEventListener("sessionstart", sessionStartHandler);
renderer.xr.addEventListener("sessionend", sessionEndHandler); renderer.xr.addEventListener("sessionend", sessionEndHandler);
// --- Resize handling --- // --- Handle resize ---
const handleResize = () => { const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight; camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
...@@ -132,21 +93,13 @@ export const XRRenderer: React.FC = () => { ...@@ -132,21 +93,13 @@ export const XRRenderer: React.FC = () => {
if (hitTestResults.length > 0) { if (hitTestResults.length > 0) {
const hit = hitTestResults[0]; const hit = hitTestResults[0];
const pose = hit.getPose(localRefSpace); const pose = hit.getPose(localRefSpace);
if (pose) { if (pose) {
const mat = new THREE.Matrix4().fromArray(pose.transform.matrix);
reticle.visible = true; reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix); reticle.position.set(
reticle.matrix.decompose(
reticle.position,
reticle.quaternion,
reticle.scale
);
latestHitPose = new Float32Array([
pose.transform.position.x, pose.transform.position.x,
pose.transform.position.y, pose.transform.position.y,
pose.transform.position.z, pose.transform.position.z
]); );
} }
} else { } else {
reticle.visible = false; reticle.visible = false;
...@@ -156,6 +109,29 @@ export const XRRenderer: React.FC = () => { ...@@ -156,6 +109,29 @@ export const XRRenderer: React.FC = () => {
renderer.render(scene, camera); 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 --- // --- Cleanup ---
return () => { return () => {
renderer.xr.removeEventListener("sessionstart", sessionStartHandler); renderer.xr.removeEventListener("sessionstart", sessionStartHandler);
...@@ -164,9 +140,13 @@ export const XRRenderer: React.FC = () => { ...@@ -164,9 +140,13 @@ export const XRRenderer: React.FC = () => {
renderer.dispose(); renderer.dispose();
mountRef.current?.removeChild(renderer.domElement); 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 ( return (
<div style={{ width: "100vw", height: "100vh", position: "relative" }}> <div style={{ width: "100vw", height: "100vh", position: "relative" }}>
......
...@@ -6,7 +6,12 @@ export class GLTFModel implements IRenderable { ...@@ -6,7 +6,12 @@ export class GLTFModel implements IRenderable {
mesh: THREE.Object3D; mesh: THREE.Object3D;
position: THREE.Vector3; position: THREE.Vector3;
constructor(url: string, position: THREE.Vector3, onLoad?: () => void) { constructor(
url: string,
position: THREE.Vector3,
textureUrl?: string, // optional PNG texture
onLoad?: () => void
) {
this.mesh = new THREE.Group(); // placeholder until loaded this.mesh = new THREE.Group(); // placeholder until loaded
this.position = position.clone(); this.position = position.clone();
this.mesh.position.copy(this.position); this.mesh.position.copy(this.position);
...@@ -15,21 +20,51 @@ export class GLTFModel implements IRenderable { ...@@ -15,21 +20,51 @@ export class GLTFModel implements IRenderable {
fetch(url, { method: "HEAD" }) fetch(url, { method: "HEAD" })
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
// Show popup if file not found
alert(`GLB file not found: ${url}`); alert(`GLB file not found: ${url}`);
throw new Error(`GLB file not found: ${url}`); throw new Error(`GLB file not found: ${url}`);
} }
// Load the GLB
const loader = new GLTFLoader(); const loader = new GLTFLoader();
loader.load( loader.load(
url, url,
(gltf) => { (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); this.mesh.add(gltf.scene);
gltf.scene.scale.set(0.5, 0.5, 0.5); // adjust scale gltf.scene.scale.set(0.5, 0.5, 0.5);
if (onLoad) onLoad(); if (onLoad) onLoad();
}, },
undefined, 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) => { (error) => {
alert(`Error loading GLB: ${error}`); alert(`Error loading GLB: ${error}`);
console.error("Error loading GLTF model:", error); console.error("Error loading GLTF model:", error);
......
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