goog.module('gep.world.Exhibit');
const Completer = goog.require('clulib.async.Completer');
const {ExhibitionProvider,ExhibitionItemModel,ExhibitionElementOrbitViewModel} = goog.require('gep.provider.ExhibitionProvider');
const {WorldProvider} = goog.require('gep.provider.WorldProvider');
const {Rect} = goog.require('goog.math');
const {EventTarget} = goog.require('goog.events');
/**
* This class represents one exhibit with all 3D objects (artworks) and physical sensors (collision area and activation zones).
* @extends {EventTarget}
*/
class Exhibit extends EventTarget
{
/**
* This class represents an exhibit, its 3D objects and sensors needed for optical display and physical behaviour.
* @param {number} id Unique ID for the exhibit
* @param {ExhibitionItemModel} model Model which contains all important backend data
*/
constructor(id, model)
{
super();
/**
* Unique ID for the exhibit
* @type {number}
* @private
*/
this.id_ = id;
/**
* Determines whether the exhibit should be displayed in the 3D scene and considered for further interaction.
* @type {boolean}
* @private
*/
this.enabled_ = true;
/**
* Indicates whether the exhibit has been activated by the user (entering the activation zone).
* @type {boolean}
* @private
*/
this.isActive_ = false;
/**
* Reference to the WorldProvider for monitoring actions, loading processes and status changes of the created THREE.Scene.
* @type {WorldProvider}
* @private
*/
this.worldProvider_ = WorldProvider.getInstance();
/**
* Reference to ExhibitionProvider to communicate with the backend and provide and monitor all information that dynamically changes the application.
* @type {ExhibitionProvider}
* @private
*/
this.exhibitionProvider_ = ExhibitionProvider.getInstance();
/**
* Model which contains all important backend data
* @type {ExhibitionItemModel}
* @private
*/
this.model_ = model;
/**
* Main 3d object container
* @type {THREE.Object3D}
*/
this.object3D_ = new THREE.Group();
/**
* THREE collider object height
* @type {number}
* @private
*/
this.colliderHeight_ = .005;
/**
* Value for physical attraction of the exhibit
* @type {number}
* @private
*/
this.attraction_ = 0;
/**
* Value defines if user (visitor object) entered the collider zone
* @type {boolean}
*/
this.enteredByVisitor = false;
/**
* Value defines if user (vistito object) entered the activation zone
* @type {boolean}
*/
this.activatedByVisitor = false;
/**
* Completer is a helper which returns a promise. Its solved if all is loaded.
* @type {Map<string,Completer>}
* @private
*/
this.loadCompleters_ = new Map();
/**
* Sets the activation state for high resolution artworks. Its a map because an exhibit can have multiple artworks (3d objects).
* @type {Map<string,boolean>}
* @private
*/
this.hightResolutionState_ = new Map();
/**
* Cannon mass object which is used for calculating the physic behaviour of the exhibit.
* @type {CANNON.Body}
* @private
*/
this.physicMassBody_ = null;
/**
* Additional Cannon mass object for calculating the physic behaviour of the exhibit.
* @type {Map<string,CANNON.Body>}
* @private
*/
this.physicBodies_ = new Map();
/**
* THREE object which are used for raycast callision detection.
* @type {Array<THREE.Object3D>}
* @private
*/
this.collisionObjects_ = [];
/**
* Value of physical mass object scale
* @type {number}
* @private
*/
this.physicMassBodyScale_ = 1;
/**
* Value which defines if the exhibit is movable or fixed
* @type {boolean}
* @private
*/
this.isImmovable_ = false;
/**
* Value for loading progress of trasform file
* @type {number}
* @private
*/
this.progressTransformFile_ = 0;
/**
* Value for loading progress of shadow texture file
* @type {number}
* @private
*/
this.progressShadowTexture_ = 0;
/**
* Loading progress for each artwork element (glb files) as a list. Artworkes are defined in the backend.
* @type {Array<number>}
* @private
*/
this.progressElements_ = [];
/**
* Additional Cannon mass objects. Used for special interactive artworks.
* @type {Map<string,CANNON.Body>}
* @private
*/
this.physicColliders_ = new Map();
/**
* 3D objects which are hidden at the beginning. This is determined by the 3D object having "_appear" or "_dublicated_wall" in its name.
* As soon as reshuffling starts, these elements are made visible.
* An example of use is separating artworks that visually share a wall at the beginning and need a separate wall for each when they are separated.
* @type {Array<THREE.Object3D>}
* @private
*/
this.hiddenObjects_ = [];
/**
* 3D objects which are only visible at the beginning and will be disappear reshuffling starts. Opposite behaviour to hidden objects.
* @type {Array<THREE.Object3D>}
* @private
*/
this.disappearingObjects_ = [];
/**
* List of objects for each artwork (THREE.Plane) that display an optical image animation.
* For this purpose, only one plane from the list is displayed at a time for 3 seconds.
* For a THREE.Plane to be included in the list, the name must start with "still_".
* @type {Map<string,{index:number,objects:Array<THREE.Object3D>}>}
* @private
*/
this.animationObjects_ = new Map();
/**
* Defines if an object is enabled. If the value is false the exhibit is later disabled in the method checkObjectsAfterFirstInteraction.
* @type {boolean}
* @private
*/
this.hasInteractionObjects_ = false;
}
/**
* Loads the transform glb file and all artwork glb files. The 3D objects are placed in a common THREE.Container.
* In addition, collider boxes for raycasting detection and Cannon physics objects are created based on the 3D elements.
* The object is used as a collider if it has "collider" in its name.
* It is treated as a shadow if it has "ground_shadow" in its name.
* If "activity_zone" is included in the name it will be used to create the Cannon sensor of the activity zone.
* @return {Promise}
*/
async build()
{
let colliderIndex = 0;
// check if a transform file exists and save the values from it in the model
if(this.model_.transformFile.trim() != '')
{
let gltf = await this.exhibitionProvider_.loadSceneObject(this.model_.transformFile, (progress) => {this.progressTransformFile_ = progress['loaded'] / progress['total'];}).then((gltf) => gltf, (error) => {console.warn(error); return null;});
if(gltf && gltf['scene'])
{
let transformMesh = /** @type {THREE.Mesh} */ (/** @type {THREE.Object3D} */ (gltf['scene']).children[0]);
if(transformMesh)
{
this.model_.position.x = transformMesh.position.x;
this.model_.position.z = transformMesh.position.z;
this.model_.rotation = 2 * Math.acos(transformMesh.quaternion.w) * (transformMesh.quaternion.y < 0 ? -1 : 1);
this.model_.collisionArea.left = 0;
this.model_.collisionArea.top = 0;
if (transformMesh.name && transformMesh.name.indexOf('_square') != -1)
{
this.model_.collisionArea.width = (transformMesh.geometry.boundingBox.max.x - transformMesh.geometry.boundingBox.min.x) * transformMesh.scale.x;
this.model_.collisionArea.height = (transformMesh.geometry.boundingBox.max.z - transformMesh.geometry.boundingBox.min.z) * transformMesh.scale.z;
}
else
this.model_.collisionArea.width = (transformMesh.geometry.boundingBox.max.x - transformMesh.geometry.boundingBox.min.x) * transformMesh.scale.x * .5;
}
else
console.error("No transform mesh defined in file: "+this.model_.transformFile);
}
}
// load all artwork glb files and build up the additional 3D objects (physic sensor and mass, THREE colliders)
let sceneObjects = [];
for(let i=0; i<this.model_.elements.length; i++)
{
let element = this.model_.elements[i];
if(element.file3D.trim() != '')
{
this.progressElements_[i] = 0;
let gltf = await this.exhibitionProvider_.loadSceneObject(element.file3D, (progress) => {this.progressElements_[i] = progress['loaded'] / progress['total'];}).then((gltf) => gltf, (error) => {console.warn(error); return null;});
if(gltf && gltf['scene'])
{
let gltfScene = /** @type {THREE.Group} */ (gltf['scene']);
gltfScene.position.set(element.position.x, element.position.y, element.position.z);
sceneObjects.push(gltfScene);
let hasOwnColliders = false;
gltfScene.children.forEach((mesh) => {
if(mesh.name.toLowerCase().indexOf('collider') != -1 && mesh.name.toLowerCase().indexOf('collider_panel_physics_') == -1)
hasOwnColliders = true;
});
let boundingMaterial = new THREE.MeshBasicMaterial({
'color': 0x0000ff,
'opacity': window['PHYSIC_MODE'] === 'on' ? .3 : 0,
'transparent': true,
'side': THREE.FrontSide
});
gltfScene.children.forEach((mesh) => {
// create bounding (collider)
if(mesh.name.toLowerCase().indexOf('collider') != -1)
{
mesh.material = boundingMaterial;
mesh.renderOrder = 999997;
this.collisionObjects_.push(mesh);
this.worldProvider_.intersectables.push(mesh);
mesh.name = 'boundingObject'+(colliderIndex++)+'::'+this.id_.toString()+''+(element && element.id ? '::'+element.id : '');
let physicCollider = this.createPhysicFromBoxMesh_(/** @type {THREE.Mesh} */ (mesh));
this.physicColliders_.set(mesh.name, physicCollider);
this.exhibitionProvider_.physicBodyReferences.set(physicCollider.id, mesh.name);
}
// crteate shadow
else if(mesh.name.toLowerCase().indexOf('ground_shadow') != -1)
{
mesh.renderOrder = 999996;
if(mesh.name.toLowerCase().indexOf('_delete') != -1) {
this.disappearingObjects_.push(mesh);
}
else if(mesh.name.toLowerCase().indexOf('_appear') != -1 || mesh.name.toLowerCase().indexOf('_dublicated_wall') != -1) {
mesh.visible = false;
this.hiddenObjects_.push(mesh);
}
}
// create activation zone sensors
else if(mesh.name.toLowerCase().indexOf('activity_zone') != -1)
{
element.position.x = mesh.position.x;
element.position.z = mesh.position.z;
element.rotation = mesh.rotation.y;
element.interactionArea = new Rect(0,0,0,0);
if(mesh.name.indexOf('_square') != -1)
{
element.interactionArea.width = (mesh.geometry.boundingBox.max.x - mesh.geometry.boundingBox.min.x) * mesh.scale.x;
element.interactionArea.height = (mesh.geometry.boundingBox.max.z - mesh.geometry.boundingBox.min.z) * mesh.scale.z;
}
else
{
element.interactionArea.width = (mesh.geometry.boundingBox.max.x - mesh.geometry.boundingBox.min.x) * mesh.scale.x * .5;
element.interactionArea.height = 0;
}
let sensorHeight = this.colliderHeight_;
let sensorGeometry = element.interactionArea.height != 0 ? new THREE.BoxGeometry(element.interactionArea.width, sensorHeight, element.interactionArea.height) : new THREE.CylinderGeometry(element.interactionArea.width, element.interactionArea.width, sensorHeight, 100);
let sensorMaterial = new THREE.MeshBasicMaterial({'color': 0x000000, 'transparent':true, 'opacity': this.isActive_ ? 0.02 : 0});
let interactionObject = new THREE.Mesh(sensorGeometry, sensorMaterial);
interactionObject.name = 'sensor::'+this.id_.toString()+'::'+element.id.toString();
interactionObject.position.set(element.position.x + element.interactionArea.left, this.colliderHeight_ * .5, element.position.z + element.interactionArea.top);
interactionObject.rotation.y = element.rotation;
interactionObject.castShadow = false;
interactionObject.receiveShadow = false;
this.object3D_.add(interactionObject);
mesh.visible = false;
let shape = null;
if (element.interactionArea.height != 0)
{
shape = new CANNON.Box(new CANNON.Vec3(
element.interactionArea.width * .5 * this.worldProvider_.settings.physic['worldScale'],
4 * this.worldProvider_.settings.physic['worldScale'],
element.interactionArea.height * .5 * this.worldProvider_.settings.physic['worldScale']
));
}
else
{
shape = new CANNON.Sphere(
element.interactionArea.width * this.worldProvider_.settings.physic['worldScale']
);
}
let sensorBody = new CANNON.Body({
'isTrigger': true,
'type': CANNON.Body.STATIC,
'mass': 0.01, // kg
'position': new CANNON.Vec3(
interactionObject.position.x * this.worldProvider_.settings.physic['worldScale'],
interactionObject.position.y * this.worldProvider_.settings.physic['worldScale'],
interactionObject.position.z * this.worldProvider_.settings.physic['worldScale']
),
'quaternion': new CANNON.Quaternion(
interactionObject.quaternion.x,
interactionObject.quaternion.y,
interactionObject.quaternion.z,
interactionObject.quaternion.w
),
'shape': shape,
'collisionFilterGroup': 2,
'collisionFilterMask': 2
});
sensorBody.angularFactor = new CANNON.Vec3(0,1,0);
this.physicBodies_.set('sensor::'+this.id_.toString()+'::'+element.id.toString(), sensorBody);
this.exhibitionProvider_.physicBodyReferences.set(sensorBody.id, 'sensor::'+this.id_.toString()+'::'+element.id.toString());
}
// creats animated planes if name starts with 'still_'
else if(mesh.name.toLowerCase().substr(0, 6) == 'still_')
{
if(!this.animationObjects_.has('animation::'+this.id_.toString()+'::'+element.id.toString()))
this.animationObjects_.set('animation::'+this.id_.toString()+'::'+element.id.toString(), {index: 0, objects:[]});
this.animationObjects_.get('animation::'+this.id_.toString()+'::'+element.id.toString()).objects.push(mesh);
mesh.visible = false;
}
// create 3D artwork or scenographic element
else
{
// If no colider box was defined in the glb file, a new collider box will be generated by the dimension of the artwork/scenographic element bounding box.
if(!hasOwnColliders)
{
let boundingBox = new THREE.Box3().setFromObject(mesh);
if(boundingBox)
{
let boundingSize = boundingBox.getSize(new THREE.Vector3());
let boundingCenter = boundingBox.getCenter(new THREE.Vector3());
let geometry = new THREE.BoxGeometry(boundingSize.x, boundingSize.y, boundingSize.z);
let material = boundingMaterial;
let boundingObject = new THREE.Mesh(geometry, material);
boundingObject.castShadow = false;
boundingObject.name = 'boundingObject'+(colliderIndex++)+'::'+this.id_.toString()+'::'+element.id;
boundingObject.position.set(boundingCenter.x, boundingCenter.y, boundingCenter.z);
boundingObject.rotation.y = element.rotation;
boundingObject.renderOrder = 999997;
this.object3D_.add(boundingObject);
this.collisionObjects_.push(boundingObject);
this.worldProvider_.intersectables.push(boundingObject);
let physicCollider = this.createPhysicFromBoxMesh_(boundingObject, boundingBox);
this.physicColliders_.set(boundingObject.name, physicCollider);
this.exhibitionProvider_.physicBodyReferences.set(physicCollider.id, boundingObject.name);
}
}
// Object is not visible at the beginning, when the reshuffeling starts, it becomes visible.
if(mesh.name.toLowerCase().indexOf('_appear') != -1 || mesh.name.toLowerCase().indexOf('_dublicated_wall') != -1)
{
mesh.visible = false;
this.hiddenObjects_.push(mesh);
}
// Object will become invisible when the reshuffeling starts.
if(mesh.name.toLowerCase().indexOf('_delete') != -1)
this.disappearingObjects_.push(mesh);
else
this.hasInteractionObjects_ = true;
// apply the reflection cube texture
mesh.traverse((child) => {
if(child.isMesh && child.material) {
child.material.envMap = this.worldProvider_.settings.artwork['envMap'] == true ? this.exhibitionProvider_.reflectionTexture : null;
if(this.worldProvider_.settings.artwork['envMap'] == false) {
child.material.metalness = 0;
child.material.roughness = 1;
}
}
});
}
});
gltfScene.rotation.y = element.rotation;
gltfScene.name = 'element::'+element.id;
this.object3D_.add(gltfScene);
}
}
}
// sorts the animated planes y their id (part of the element name)
this.animationObjects_.forEach((animation) => {
animation.objects.sort((mesh1, mesh2) => {
let id1 = parseInt(mesh1.name.substr(6), 10);
let id2 = parseInt(mesh2.name.substr(6), 10);
return id1 < id2 ? -1 : id1 > id2 ? 1 : 0;
});
animation.objects[0].visible = true;
});
// creates the physical mass object as the collision area of the exhibit
if(this.model_.collisionArea)
{
let geometry = this.model_.collisionArea.height != 0 ? new THREE.BoxGeometry(this.model_.collisionArea.width, this.colliderHeight_, this.model_.collisionArea.height) : new THREE.CylinderGeometry(this.model_.collisionArea.width, this.model_.collisionArea.width, this.colliderHeight_, 32);
let material = new THREE.MeshBasicMaterial({'color': 0xff0000, 'opacity': .3, 'transparent': true, 'side': THREE.DoubleSide});
let collisionObject = new THREE.Mesh(geometry, material);
collisionObject.name = 'mass::'+this.id_.toString();
collisionObject.position.y = 0;
collisionObject.visible = window['PHYSIC_MODE'] === 'on';
this.object3D_.add(collisionObject);
let shape = null;
if (this.model_.collisionArea.height != 0)
{
shape = new CANNON.Box(new CANNON.Vec3(
this.model_.collisionArea.width * .5 * this.worldProvider_.settings.physic['worldScale'],
4 * this.worldProvider_.settings.physic['worldScale'],
this.model_.collisionArea.height * .5 * this.worldProvider_.settings.physic['worldScale']
));
}
else
{
shape = new CANNON.Sphere(
this.model_.collisionArea.width * this.worldProvider_.settings.physic['worldScale']
);
}
let quaternion = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0), this.model_.rotation);
this.physicMassBody_ = new CANNON.Body({
'mass': this.worldProvider_.settings.physic['mass'],
'position': new CANNON.Vec3(
this.model_.position.x * this.worldProvider_.settings.physic['worldScale'],
this.model_.position.y * this.worldProvider_.settings.physic['worldScale'],
this.model_.position.z * this.worldProvider_.settings.physic['worldScale']
),
'quaternion': new CANNON.Quaternion(quaternion.x, quaternion.y, quaternion.z, quaternion.w),
'shape': shape,
'collisionFilterGroup': 1,
'collisionFilterMask': 1
});
this.physicMassBody_.linearDamping = this.worldProvider_.settings.physic['linearDamping'];
this.physicMassBody_.angularFactor = new CANNON.Vec3(0,1,0);
this.physicBodies_.set('mass::'+this.id_.toString(), this.physicMassBody_);
this.exhibitionProvider_.physicBodyReferences.set(this.physicMassBody_.id, 'mass::'+this.id_.toString());
}
this.update(null);
}
/**
* Method to enable and disable the reflection environment texture.
* @param {boolean} value
*/
enableMaterialEnvMap(value)
{
this.object3D_.children.forEach((mesh) => {
if(mesh.name.substr(0,9) == 'element::') {
mesh.children.forEach((elementChild) => {
if(elementChild.name.substr(0,9) == 'artwork::') {
elementChild.traverse((child) => {
if(child.isMesh && child.material) {
const nameSplit = child.name.split('::');
child.material.envMap = value == true ? this.exhibitionProvider_.reflectionTexture : null;
child.material.metalness = value == true ? parseFloat(nameSplit[nameSplit.length-2]) : 0;
child.material.roughness = value == true ? parseFloat(nameSplit[nameSplit.length-1]) : 1;
}
});
}
});
}
});
}
/**
* Method creates a Cannon body based on the bounding box of an THREE object or a box object to be used for the physical collision calculation.
* @param {THREE.Mesh} object 3D object whose bounding box serves as the basis for the creation of the Cannon body.
* @param {THREE.Box3=} box Alternative box object if the bounding box of a 3D object shouldn't be used.
* @return {CANNON.Body}
* @private
*/
createPhysicFromBoxMesh_(object, box = null)
{
let boundingBox = box || object.geometry.boundingBox;
if(boundingBox == null)
{
object.geometry.computeBoundingBox();
boundingBox = object.geometry.boundingBox;
}
let boundingSize = boundingBox.getSize(new THREE.Vector3());
boundingSize.x *= object.scale.x;
boundingSize.y *= object.scale.y;
boundingSize.z *= object.scale.z;
let physicBody = new CANNON.Body({
'mass': 0, // kg
'type': CANNON.Body.STATIC,
'position': new CANNON.Vec3(
object.position.x * this.worldProvider_.settings.physic['worldScale'],
object.position.y * this.worldProvider_.settings.physic['worldScale'],
object.position.z * this.worldProvider_.settings.physic['worldScale']
),
'quaternion': new CANNON.Quaternion(object.quaternion.x, object.quaternion.y, object.quaternion.z, object.quaternion.w),
'shape': new CANNON.Box(new CANNON.Vec3(
boundingSize.x * .5 * this.worldProvider_.settings.physic['worldScale'],
boundingSize.y * .5 * this.worldProvider_.settings.physic['worldScale'],
boundingSize.z * .5 * this.worldProvider_.settings.physic['worldScale']
)),
'collisionFilterGroup': 2,
'collisionFilterMask': 2
});
return physicBody;
}
/**
* Method is called when reshuffling starts and hidden objects can now be displayed or certain other objects must disappear.
*/
updateVisibilities()
{
this.hiddenObjects_.forEach((mesh) => {
mesh.visible = true;
});
this.disappearingObjects_.forEach((mesh) => {
const parent = mesh.parent;
parent.remove(mesh);
if(parent.children.length == 0)
this.object3D_.remove(parent);
});
}
/**
* Checks whether certain 3D elements can be removed after the first interaction with an artwork.
*/
checkObjectsAfterFirstInteraction()
{
this.enabled_ = this.hasInteractionObjects_;
if(this.enabled_ == false)
{
this.collisionObjects_.forEach((object) => {
object.parent.remove(object);
});
this.collisionObjects_ = [];
this.physicBodies_.forEach((physicBody) => {
physicBody.world.removeBody(physicBody);
});
this.physicBodies_ = new Map();
this.physicColliders_.forEach((physicBody) => {
physicBody.world.removeBody(physicBody);
});
this.physicColliders_ = new Map();
this.physicMassBody_ = null;
const countChildren = this.object3D_.children.length;
for(let i=0; i<countChildren; i++)
{
if(this.object3D_.children[0])
this.object3D_.children[0].parent.remove(this.object3D_.children[0]);
}
}
}
/**
* The collision area and thus its main Cannon mass are scaled very small at the beginning and only grow to the normal scaling of 1 when reshuffling starts. This method updates the scaling value.
* @param {number} value Scale value of the Cannon collision mass
*/
updateMassBodyScale(value)
{
this.physicMassBodyScale_ = value;
if (this.model_.collisionArea.height != 0)
{
let originVector = new CANNON.Vec3(
this.model_.collisionArea.width * .5 * this.worldProvider_.settings.physic['worldScale'],
4 * this.worldProvider_.settings.physic['worldScale'],
this.model_.collisionArea.height * .5 * this.worldProvider_.settings.physic['worldScale']
);
let box = /** @type {CANNON.Box} */ (this.physicMassBody_.shapes[0]);
box.halfExtents.set(originVector.x * value, originVector.y * value, originVector.z * value);
box.updateConvexPolyhedronRepresentation();
}
else
this.physicMassBody_.shapes[0].radius = this.model_.collisionArea.width * this.worldProvider_.settings.physic['worldScale'] * value;
this.physicMassBody_.shapes[0].updateBoundingSphereRadius();
this.physicMassBody_.updateAABB();
}
/**
* method is called in the main render process by the WebGLRenderer and matches THREE objects to the Cannon physics objecte. It also swaps the defined animation planes every 3 seconds.
* @param {THREE.Clock} clock
*/
update(clock)
{
const time = clock ? clock.getElapsedTime() : 0;
if(!this.enabled_)
return;
// animation of stills
this.animationObjects_.forEach((animation) => {
let timeIndex = Math.floor(time / 3); // update all 3 seconds
let animationIndex = timeIndex % animation.objects.length;
if(animationIndex != animation.index)
{
animation.objects[animation.index].visible = false;
animation.objects[animationIndex].visible = true;
animation.index = animationIndex;
}
});
let massBody = this.physicBodies_.get('mass::'+this.id_.toString());
let massRotation = new CANNON.Vec3();
massBody.quaternion.toEuler(massRotation);
this.physicMassBody_.position.y = this.colliderHeight_ * .5 + 0.01;
this.object3D_.position.set(massBody.position.x, this.model_.position.y, massBody.position.z);
this.object3D_.rotation.set(0, massRotation.y, 0);
this.physicBodies_.forEach((physicBody, name) => {
if(physicBody != massBody)
{
let object = this.object3D_.getObjectByName(name);
let position = object.getWorldPosition(new THREE.Vector3());
let quaternion = object.getWorldQuaternion(new THREE.Quaternion());
physicBody.position.x = position.x;
physicBody.position.z = position.z;
physicBody.quaternion.set(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
}
});
this.physicColliders_.forEach((colliderBody, key) => {
let boundingObject = this.object3D_.getObjectByName(key);
if(boundingObject)
{
let worldPosition = boundingObject.getWorldPosition(new THREE.Vector3());
let worldQuaternion = boundingObject.getWorldQuaternion(new THREE.Quaternion());
colliderBody.position.x = worldPosition.x;
colliderBody.position.y = worldPosition.y;
colliderBody.position.z = worldPosition.z;
colliderBody.quaternion.set(worldQuaternion.x, worldQuaternion.y, worldQuaternion.z, worldQuaternion.w);
}
});
}
/**
* Method determines whether an exhibit is physically movable or fixed.
* The artwork activated by the visitor is always fixed while all artworks around it move due to their attraction.
* @param {boolean} isImmovable
*/
setImmovable(isImmovable)
{
if(!this.enabled_)
return;
this.isImmovable_ = isImmovable;
this.physicMassBody_.linearDamping = isImmovable ? 1 : this.worldProvider_.settings.physic['linearDamping'];
this.physicMassBody_.angularDamping = isImmovable ? 1 : 0.01;
if(isImmovable)
{
this.physicMassBody_.angularVelocity = new CANNON.Vec3(0, 0, 0);
this.physicMassBody_.linearVelocity = new CANNON.Vec3(0, 0, 0);
}
}
/**
* Gets the distance (world space) for a point (world space) to an element selected by its artork id
* @param {number} elementId Artwork id
* @param {THREE.Vector3} point Position to check in world space
* @return {number}
*/
getInteractionDistance(elementId, point)
{
if(!this.enabled_)
return Infinity;
let interactionArea = this.object3D_.getObjectByName('interactionObject::'+this.id_.toString()+'::'+elementId.toString());
if(interactionArea)
{
let interactionPosition = interactionArea.getWorldPosition(new THREE.Vector3());
return interactionPosition.distanceTo(point);
}
return Infinity;
}
/**
* Gets an specific 3D artwork element by its id
* @param {number} elementId Artwork id
* @return {THREE.Object3D}
*/
getArtwork3D(elementId)
{
return this.object3D_.getObjectByName('element::'+elementId.toString());
}
/**
* Gets an orbit view configuration model for a given artwork id
* @param {number} elementId Artwork id
* @return {ExhibitionElementOrbitViewModel}
*/
getOrbitViewOfElement(elementId)
{
let element = this.model_.getElement(elementId);
if(element)
return element.orbitView;
return null;
}
/**
* Gets the global position of a 3D element by a given element name
* @param {string} name THREE element name
* @return {THREE.Vector3}
*/
getWorldPositionOf(name)
{
let object = this.object3D_.getObjectByName(name);
if(object)
return object.getWorldPosition(new THREE.Vector3());
return null;
}
/**
* Starts the loading of a alternative highresultion object (if defined) for a given artwork id
* @param {number} elementId Artwork id
* @param {Function=} progressCallback Progress callback function.
* @return {Promise}
*/
loadHighresObject(elementId, progressCallback = null)
{
if(!this.enabled_)
return Promise.resolve();
let element = this.model_.getElement(elementId);
let completer = this.loadCompleters_.get(elementId.toString());
this.hightResolutionState_.set(elementId.toString(), true);
if(!completer)
{
completer = new Completer();
this.loadCompleters_.set(elementId.toString(), completer);
this.exhibitionProvider_.loadSceneObject(element.fileHighres3D, progressCallback).then((gltf) => {
if(gltf && gltf['scene'])
{
let gltfScene = /** @type {THREE.Group} */ (gltf['scene']);
gltfScene.position.set(element.position.x, element.position.y, element.position.z);
gltfScene.rotation.y = element.rotation;
gltfScene.name = 'elementHighres::'+element.id;
gltfScene.visible = false;
this.object3D_.add(gltfScene);
completer.resolve();
}
}, (error) => {console.warn(error); completer.reject('Highresolution object could\'t be loaded!');});
}
return completer.getPromise();
}
/**
* When it is possible show the high resolution or the normal version of a specified artwork.
* @param {number} elementId Artwork id
* @param {boolean=} enableHighresolution
*/
switchObjectResolution(elementId, enableHighresolution = false)
{
if(!this.enabled_)
return;
this.hightResolutionState_.set(elementId.toString(), enableHighresolution);
let highresObj = this.object3D_.getObjectByName('elementHighres::'+elementId.toString());
if(highresObj)
{
this.object3D_.getObjectByName('element::'+elementId.toString()).visible = !enableHighresolution;
highresObj.visible = enableHighresolution;
}
}
/**
* Returns the display status. Is the high resolution or the normal variant displayed?
* @param {number} elementId Artwork id
* @return {boolean}
*/
getHightResolutionState(elementId)
{
if(this.hightResolutionState_.has(elementId.toString()))
return this.hightResolutionState_.get(elementId.toString());
return false;
}
/**
* method checks if the given positions (world space) is within the collision area (largest extent of an exhibit)
* and if so the defined collisions elements should be used for the collision raycast check.
* @param {Array<THREE.Vector3>} positions
* @return {boolean}
*/
shouldTestColliding(positions)
{
if(!this.enabled_)
return false;
let radius = this.model_.collisionArea.height != 0 ? Math.max(this.model_.collisionArea.width, this.model_.collisionArea.height) * .5 : this.model_.collisionArea.width;
for(let i=0; i<positions.length; i++)
{
let distance = Math.sqrt(Math.pow(positions[i].x - this.object3D_.position.x, 2) + Math.pow(positions[i].z - this.object3D_.position.z, 2));
if(distance < radius)
return true;
}
return false;
}
/**
* Activates sensor objects for THREE raycast check.
*/
enableInteraction()
{
this.object3D_.children.forEach((child) => {
if(child.name.indexOf('sensor::') == 0)
gsap.to(child.material, {'opacity': 0.02, 'duration': 1, 'ease': 'power2.out'});
});
this.isActive_ = true;
}
/**
* Getter
* @return {number}
*/
get id()
{
return this.id_;
}
/**
* Getter
* @return {boolean}
*/
get enabled()
{
return this.enabled_;
}
/**
* Getter
* @return {boolean}
*/
get isActive()
{
return this.isActive_;
}
/**
* Getter
* @return {ExhibitionItemModel}
*/
get model()
{
return this.model_;
}
/**
* Getter
* @return {THREE.Object3D}
*/
get object3D()
{
return this.object3D_;
}
/**
* Setter
* @param {number} value
*/
set attraction(value)
{
this.attraction_ = value;
}
/**
* Getter
* @return {number}
*/
get attraction()
{
return this.attraction_;
}
/**
* Getter
* @return {Map<string,CANNON.Body>}
*/
get physicBodies()
{
return this.physicBodies_;
}
/**
* Getter
* @return {CANNON.Body}
*/
get physicMassBody()
{
return this.physicMassBody_;
}
/**
* Getter
* @return {number}
*/
get physicMassBodyScale()
{
return this.physicMassBodyScale_;
}
/**
* Getter
* @return {Map<string, CANNON.Body>}
*/
get physicColliders()
{
return this.physicColliders_;
}
/**
* Getter
* @return {Array<THREE.Object3D>}
*/
get collisionObjects()
{
return this.collisionObjects_;
}
/**
* Getter
* @return {boolean}
*/
get isImmovable()
{
return this.isImmovable_;
}
/**
* Getter
* @return {number}
*/
get progress()
{
return this.progressElements_.reduce((a, b) => a + b, 0) + this.progressShadowTexture_ + this.progressTransformFile_;
}
}
exports = Exhibit;