src/world/Exhibit.js

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;