Commit e3227928 authored by Stoll's avatar Stoll
Browse files

Placement improvement

parent ff804daa
......@@ -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 button ---
// --- AR Button ---
const button = XRButton.createButton(renderer, {
requiredFeatures: ["hit-test", "dom-overlay"],
optionalFeatures: ["anchors"],
optionalFeatures: ["local-floor"],
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 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;
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" }}>
......
......@@ -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
}
}
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