You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
11 KiB
328 lines
11 KiB
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();
|
|
}
|
|
}
|
|
}
|