Commit 899f3a9c authored by Stoll's avatar Stoll
Browse files

Stable object implemented,

Bottom navbar added
Two models
parent 0458da0b
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
import React from "react";
interface ARBottomBarProps {
objectOptions: { name: string; emoji: string }[];
onSelect: (name: string) => void;
}
export const ARBottomBar: React.FC<ARBottomBarProps> = ({ objectOptions, onSelect }) => {
return (
<div
style={{
position: "absolute",
bottom: 0,
width: "100%",
height: "90px",
background: "rgba(0, 0, 0, 0.6)",
display: "flex",
alignItems: "center",
justifyContent: "space-around",
color: "white",
zIndex: 10,
pointerEvents: "auto",
backdropFilter: "blur(10px)",
}}
>
{objectOptions.map((obj) => (
<button
key={obj.name}
style={{
background: "#333",
color: "white",
border: "none",
borderRadius: "10px",
padding: "10px 20px",
fontSize: "16px",
}}
onClick={() => onSelect(obj.name)}
>
{obj.emoji} {obj.name}
</button>
))}
</div>
);
};
import * as THREE from 'three';
import type { IRenderable } from '../model/IRenderable';
export class BlockManager {
objects: IRenderable[] = [];
scene: THREE.Scene;
constructor(scene: THREE.Scene) {
this.scene = scene;
}
addObject(obj: IRenderable) {
this.objects.push(obj);
this.scene.add(obj.mesh);
}
removeObject(obj: IRenderable) {
this.scene.remove(obj.mesh);
this.objects = this.objects.filter(o => o !== obj);
}
enableInteraction() {
this.objects.forEach(obj => obj.enableInteraction());
}
}
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";
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");
useEffect(() => {
if (!mountRef.current || !overlayRef.current) return;
// Basic scene setup
const scene = new THREE.Scene();
scene.background = null;
const camera = new THREE.PerspectiveCamera(
70,
window.innerWidth / window.innerHeight,
0.01,
20
);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.xr.enabled = true;
renderer.xr.setReferenceSpaceType("local-floor");
mountRef.current.appendChild(renderer.domElement);
const button = XRButton.createButton(renderer, {
requiredFeatures: ["hit-test", "dom-overlay"],
domOverlay: { root: overlayRef.current },
});
mountRef.current.appendChild(button);
// Lighting
const light = new THREE.HemisphereLight(0xffffff, 0xbbbbff, 1);
scene.add(light);
// Block manager
const blockManager = new BlockManager(scene);
// Track hit-test info
let hitTestSource: XRHitTestSource | null = null;
let localRefSpace: XRReferenceSpace | null = null;
let lastHitPose: THREE.Vector3 | null = null;
const sessionStartHandler = async () => {
setIsSessionActive(true);
const session = renderer.xr.getSession();
localRefSpace = await session.requestReferenceSpace("local-floor");
const viewerSpace = await session.requestReferenceSpace("viewer");
hitTestSource = await session.requestHitTestSource({ space: viewerSpace });
};
const sessionEndHandler = () => {
setIsSessionActive(false);
hitTestSource = null;
localRefSpace = null;
lastHitPose = null;
};
renderer.xr.addEventListener("sessionstart", sessionStartHandler);
renderer.xr.addEventListener("sessionend", sessionEndHandler);
// Resize handling
const handleResize = () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
};
window.addEventListener("resize", handleResize);
// Animation loop
renderer.setAnimationLoop((timestamp, frame) => {
if (frame && hitTestSource && localRefSpace) {
const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0) {
const hit = hitTestResults[0];
const pose = hit.getPose(localRefSpace);
if (pose) {
// Save the latest hit pose (for new object placement)
lastHitPose = new THREE.Vector3(
pose.transform.position.x,
pose.transform.position.y,
pose.transform.position.z
);
}
}
}
renderer.render(scene, camera);
});
// === Add object at the hit-test location when selected ===
const addSelectedObject = () => {
if (!lastHitPose) {
console.warn("No surface detected yet. Move camera around to find a plane.");
return;
}
let objectToAdd;
if (selectedObject === "Cube") {
objectToAdd = new Cube(lastHitPose);
} else if (selectedObject === "Watermill") {
objectToAdd = new GLTFModel("/watermill.glb", lastHitPose);
} else if (selectedObject === "Tree") {
objectToAdd = new GLTFModel("/tree.glb", lastHitPose);
} else {
objectToAdd = new Cube(lastHitPose);
}
blockManager.addObject(objectToAdd);
};
// Expose function to the window for debugging (optional)
(window as any).addSelectedObject = addSelectedObject;
// Cleanup
return () => {
renderer.xr.removeEventListener("sessionstart", sessionStartHandler);
renderer.xr.removeEventListener("sessionend", sessionEndHandler);
window.removeEventListener("resize", handleResize);
renderer.dispose();
mountRef.current?.removeChild(renderer.domElement);
};
}, [selectedObject]);
// --- Handle object placement from bottom bar ---
const handleSelect = (name: string) => {
setSelectedObject(name);
// Trigger placement
(window as any).addSelectedObject?.();
};
return (
<div style={{ width: "100vw", height: "100vh", position: "relative" }}>
<div ref={mountRef} style={{ width: "100%", height: "100%" }} />
<div
ref={overlayRef}
style={{ position: "absolute", width: "100%", height: "100%" }}
>
{isSessionActive && (
<ARBottomBar
objectOptions={[
{ name: "Cube", emoji: "🧊" },
{ name: "Watermill", emoji: "🌊" },
{ name: "Tree", emoji: "🌳" },
]}
onSelect={handleSelect}
/>
)}
</div>
</div>
);
};
import * as THREE from 'three';
import type { IRenderable } from './IRenderable';
export class Cube implements IRenderable {
mesh: THREE.Mesh;
position: THREE.Vector3;
constructor(position: THREE.Vector3, color = 0xff0000, size = 0.3) {
this.mesh = new THREE.Mesh(
new THREE.BoxGeometry(size, size, size),
new THREE.MeshStandardMaterial({ color })
);
this.position = position.clone();
this.mesh.position.copy(this.position);
}
setPosition(position: THREE.Vector3) {
this.position.copy(position);
this.mesh.position.copy(position);
}
enableInteraction() {
// Placeholder for drag-and-drop logic
}
}
import * as THREE from "three";
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
}
}
import * as THREE from 'three';
export interface IRenderable {
mesh: THREE.Object3D; // The Three.js object (Mesh, Group, etc.)
position: THREE.Vector3; // Current position in the scene
setPosition(position: THREE.Vector3): void; // Method to update position
enableInteraction(): void; // Placeholder for drag/drop, selection, etc.
}
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