import * as THREE from 'three'; import { RigidBodyDesc, ColliderDesc } from '@dimforge/rapier3d-compat'; import { ModelLoader } from './ModelLoader'; import { AudioContent, Content, TextContent } from './Content'; export class Item { constructor(rapierWorld, scene, player, isCollider = false, spawnPosition = new THREE.Vector3(0, 1, 0), scale, name, model, texture = null, spawnedObjects, content = null, object = null) { this.name = name; this.id = null; this.groupId = null; this.model = model; this.texture = texture; this.object = object this.spawnedObjects = spawnedObjects; this.scene = scene; this.spawnPosition = spawnPosition.clone(); this.spawnRotation = new THREE.Euler(0, 0, 0, 'YXZ'); this.scale = new THREE.Vector3(1 * scale, 1 * scale, 1 * scale); this.rotationSpeed = new THREE.Vector3(); this.initialPositon = null; this.targetPosition = null; this.centerOffset = new THREE.Vector3(); this.rapierWorld = rapierWorld; this.rb = null; this.coll = null; this.isCollider = isCollider; this.debugBox = null; this.isActive = false; this.modelLoader = new ModelLoader(); this.player = player; this.content = content; this.contentDisplay = null; this.loadState = "unloaded"; this.lastPosition = null; this.links = []; this._init(); this.isVisible = true; } _init() { } async loadModel() { if (this.loadState !== 'unloaded') return; this.loadState = 'loading'; try { if (!this.object) { this.object = await this.modelLoader.loadDRACOModelURL(this.model, this.texture, this.scale); } this.object.traverse((child) => { if (child.isMesh) { this.spawnedObjects.push( { mesh: child, body: null} ); } }); this._addObject(); if(!this.isCollider) { this.loadState = 'loaded'; return; } const boundingBox = new THREE.Box3().setFromObject(this.object); const size = new THREE.Vector3(); boundingBox.getSize(size); // Calculate the world center of the bounding box const worldCenter = new THREE.Vector3(); boundingBox.getCenter(worldCenter); // Calculate the local center by subtracting the object's world position this.centerOffset.subVectors(worldCenter, this.object.position); let rbDesc; if (this.content === null) { rbDesc = RigidBodyDesc.kinematicPositionBased(); } else { rbDesc = RigidBodyDesc.dynamic(); } rbDesc.setTranslation(this.object.position.x, this.object.position.y, this.object.position.z); this.rb = this.rapierWorld.createRigidBody(rbDesc); const colDesc = ColliderDesc.cuboid(size.x / 2, size.y / 2, size.z / 2) .setTranslation(this.centerOffset.x, this.centerOffset.y, this.centerOffset.z); // Use the calculated local center offset this.coll = this.rapierWorld.createCollider(colDesc, this.rb); this.loadState = 'loaded'; } catch (error) { console.error(`Failed to load model for tiem: ${this.name}`); this.loadState = 'unloaded'; } } unloadModel() { if (this.loadState !== 'loaded') return; // Remove physics body if (this.rb) { this.rapierWorld.removeRigidBody(this.rb); this.rb = null; this.coll = null; } // Remove visual object if (this.object) { this.scene.remove(this.object); // Dispose of geometries and materials to free memory this.object.traverse(child => { if (child.isMesh) { child.geometry.dispose(); child.material.dispose(); } }); this.object = null; } // Clean up content display this._removeContentDisplay(); this.loadState = 'unloaded'; } _addObject() { if (this.object != null) { if (this.lastPosition != null) { this.object.position.copy(this.lastPosition); } else { this.object.position.copy(this.spawnPosition); } this.scene.add(this.object); } } _drawBoundingBox() { if (this.isCollider && this.isVisible) { // 1. Remove the previous frame's box if (this.debugBox) { this.scene.remove(this.debugBox); this.debugBox.geometry.dispose(); this.debugBox.material.dispose(); } if (this.loadState !== 'loaded') return; // 2. Calculate the new world-space bounding box const worldAABB = new THREE.Box3().setFromObject(this.object); const size = new THREE.Vector3(); worldAABB.getSize(size); const center = new THREE.Vector3(); worldAABB.getCenter(center); const scaleFactor = 1.2; size.multiplyScalar(scaleFactor); // 3. Create a new 2D box with the updated dimensions const w = size.x / 2; const h = size.y / 2; const points = [ new THREE.Vector3(-w, -h, 0), new THREE.Vector3(w, -h, 0), new THREE.Vector3(w, h, 0), new THREE.Vector3(-w, h, 0) ]; const geometry = new THREE.BufferGeometry().setFromPoints(points); const material = new THREE.LineBasicMaterial({ linewidth: 10, color: 'red' }); this.debugBox = new THREE.LineLoop(geometry, material); // 4. Position and orient the new box this.debugBox.position.copy(center); this.debugBox.quaternion.copy(this.player.camera.quaternion); this.scene.add(this.debugBox); } } show() { if (this.object) { this.object.visible = true; } if (this.rb) { this.rb.setEnabled(true); } this.isVisible = true; } hide() { if (this.object) { this.object.visible = false; } if (this.rb) { this.rb.setEnabled(false); } this.isVisible = false; } async _moveActiveObject(delta) { if (this.isActive) { if (this.rb && !this.rb.isKinematic()) { this.rb.setBodyType(1); // 1 is kinematic } if (!this.initialPositon) { this.initialPositon = this.object.position.clone(); this.targetPosition = this.initialPositon.clone().add(new THREE.Vector3(0, 10, 0)); this.rotationSpeed.set( (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5 ); await this._createContentDisplay(); } // Directly update the object's position for smooth animation this.object.position.lerp(this.targetPosition, 0.01); this.rb.setNextKinematicTranslation(this.object.position); this.object.rotation.x += this.rotationSpeed.x * delta; this.object.rotation.y += this.rotationSpeed.y * delta; this.object.rotation.z += this.rotationSpeed.z * delta; this.rb.setNextKinematicRotation(this.object.quaternion); if (this.content) this.content.update(); } else { if (this.rb && !this.rb.isDynamic()) { // Set the rigid body's position to the animated object's final position // before switching back to dynamic. this.rb.setTranslation(this.object.position, true); this.rb.setRotation(this.object.quaternion, true); this.rb.setBodyType(0); // 0 is dynamic this.initialPositon = null; this.targetPosition = null; this._removeContentDisplay(); } // When not active, the physics engine controls the object if (this.rb) { const pos = this.rb.translation(); const rot = this.rb.rotation(); this.object.position.set(pos.x, pos.y, pos.z); this.object.quaternion.set(rot.x, rot.y, rot.z, rot.w); } } } async _createContentDisplay() { //console.log(this.contentDisplay) // Prevent creating a new display if the old one is still fading out. if (this.content instanceof AudioContent && this.content.isFadingOut) { return; } if (this.content && !this.contentDisplay) { // Check if content needs to receieve a promise (async loading) const meshOrPromise = this.content.createDisplayMesh(); if (meshOrPromise instanceof Promise) { this.contentDisplay = await meshOrPromise; } else { this.contentDisplay = meshOrPromise; } // Only add the resolved mesh, attach to either object or player if (!(this.content instanceof AudioContent)) { this.player.camera.add(this.contentDisplay); this.contentDisplay.position.set(0, 0, -5); this.contentDisplay.rotation.set(0, 0, 0); console.log("Added content display to screen"); } else { this.object.add(this.contentDisplay); this.contentDisplay.position.set(0, 0, 0); this.contentDisplay.rotation.set(0, 0, 0); console.log("Added content display to Object"); } } } _removeContentDisplay() { if (this.content && this.contentDisplay) { if (!(this.content instanceof AudioContent)) { this.player.camera.remove(this.contentDisplay); this.content.dispose(); this.contentDisplay = null; console.log("Removed content display"); } else { // If audio fade out this.content.fadeOutAndDispose(3000, this.object, this.contentDisplay); this.contentDisplay = null; } } } async update(delta) { this._drawBoundingBox(); if (this.loadState !== 'loaded') return; this.lastPosition = this.object.position.clone(); await this._moveActiveObject(delta); if (this.content) { this.content.update(); } } }