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( pose.transform.position.x,
reticle.position, pose.transform.position.y,
reticle.quaternion, pose.transform.position.z
reticle.scale
); );
latestHitPose = new Float32Array([
pose.transform.position.x,
pose.transform.position.y,
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" }}>
......
...@@ -3,50 +3,85 @@ import type { IRenderable } from "./IRenderable"; ...@@ -3,50 +3,85 @@ import type { IRenderable } from "./IRenderable";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
export class GLTFModel implements IRenderable { 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(
this.mesh = new THREE.Group(); // placeholder until loaded url: string,
this.position = position.clone(); position: THREE.Vector3,
this.mesh.position.copy(this.position); textureUrl?: string, // optional PNG texture
onLoad?: () => void
// Check if the file exists ) {
fetch(url, { method: "HEAD" }) this.mesh = new THREE.Group(); // placeholder until loaded
.then((response) => { this.position = position.clone();
if (!response.ok) { this.mesh.position.copy(this.position);
// Show popup if file not found
alert(`GLB file not found: ${url}`); // Check if the file exists
throw new Error(`GLB file not found: ${url}`); fetch(url, { method: "HEAD" })
} .then((response) => {
if (!response.ok) {
// Load the GLB alert(`GLB file not found: ${url}`);
const loader = new GLTFLoader(); throw new Error(`GLB file not found: ${url}`);
loader.load( }
url,
(gltf) => { const loader = new GLTFLoader();
this.mesh.add(gltf.scene); loader.load(
gltf.scene.scale.set(0.5, 0.5, 0.5); // adjust scale url,
if (onLoad) onLoad(); (gltf) => {
}, // If a texture URL is provided, load it and apply
undefined, if (textureUrl) {
(error) => { const textureLoader = new THREE.TextureLoader();
alert(`Error loading GLB: ${error}`); textureLoader.load(
console.error("Error loading GLTF model:", error); textureUrl,
} (texture) => {
); //texture.encoding = THREE.sRGBEncoding;
})
.catch((err) => { gltf.scene.traverse((child) => {
console.error(err.message); if ((child as THREE.Mesh).isMesh) {
}); const mesh = child as THREE.Mesh;
} const material = new THREE.MeshStandardMaterial({
map: texture,
setPosition(position: THREE.Vector3) { });
this.position.copy(position); mesh.material = material;
this.mesh.position.copy(position); }
} });
enableInteraction() { this.mesh.add(gltf.scene);
// Placeholder for drag-and-drop logic 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
}
} }
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