Source: primitives/zincObject.js

const THREE = require('three');
const createBufferGeometry = require('../utilities').createBufferGeometry;

let uniqueiId = 0;

const getUniqueId = function () {
  return "pr" + uniqueiId++;
}

/**
 * Provides the base object for other primitive types.
 * This class contains multiple base methods.
 * 
 * @class
 * @author Alan Wu
 * @return {ZincObject}
 */
const ZincObject = function() {
  this.isZincObject = true;
  this.geometry = undefined;
  // THREE.Mesh
  this.morph = undefined;
  this.group = new THREE.Group();
  this._lod = new (require("./lod").LOD)(this);
  /**
	 * Groupname given to this geometry.
	 */
  this.groupName = undefined;
  this.timeEnabled = false;
  this.morphColour = false;
  this.inbuildTime = 0;
  this.mixer = undefined;
  this.animationGroup = undefined;
	/**
	 * Total duration of the animation, this value interacts with the 
	 * {@link Renderer#playRate} to produce the actual duration of the
	 * animation. Actual time in second = duration / playRate.
	 */
  this.duration = 6000;
  this.clipAction = undefined;
  this.userData = {};
  this.videoHandler = undefined;
  this.marker = undefined;
  this.markerNumber = undefined;
  this.markerUpdateRequired = true;
  this.closestVertexIndex = -1;
  this.boundingBoxUpdateRequired = true;
  this.cachedBoundingBox = new THREE.Box3();
  this.anatomicalId = undefined;
  this.region = undefined;
  this.animationClip = undefined;
  this.markerMode = "inherited";
  this.uuid = getUniqueId();
  this._v1 = new THREE.Vector3();
  this._v2 = new THREE.Vector3();
  this._b1 = new THREE.Box3();
  this.center = new THREE.Vector3();
  this.radius = 0;
  this.visible = true;
  //Draw range is only used by primitives added
  //programatically with addVertices function
  this.drawRange = -1;
}

/**
 * Set the duration of the animation of this object.
 * 
 * @param {Number} durationIn - Duration of the animation.
 */
ZincObject.prototype.setDuration = function(durationIn) {
  this.duration = durationIn;
  if (this.clipAction) {
    this.clipAction.setDuration(this.duration);
  }
}

/**
 * Get the duration of the animation of this object.
 * 
 * @return {Number}
 */
ZincObject.prototype.getDuration = function() {
  return this.duration;
}

/**
 * Set the region this object belongs to.
 *
 * @param {Region} region
 */
ZincObject.prototype.setRegion = function(region) {
  this.region = region;
}

/**
 * Get the region this object belongs to.
 * 
 * @return {Region}
 */
ZincObject.prototype.getRegion = function() {
  return this.region;
}

/**
 * Get the threejs object3D. 
 * 
 * @return {Object}
 */
 ZincObject.prototype.getMorph = function() {
  const morph =  this._lod.getCurrentMorph();
  return morph ? morph : this.morph;
}

/**
 * Get the threejs object3D. 
 * 
 * @return {Object}
 */
 ZincObject.prototype.getGroup = function() {
  return this.group;
}

/**
 * Set the internal threejs object3D. 
 */
 ZincObject.prototype.setMorph = function(mesh) {
  this.morph = mesh;
  this.group.add(this.morph);
  //this is the base level object
  const distance = this._lod.calculateDistance("far");
  this._lod.addLevel(mesh, distance);
  this._lod.setMaterial(mesh.material);
}

/**
 * Handle transparent mesh, create a clone for backside rendering if it is
 * transparent.
 */
ZincObject.prototype.checkTransparentMesh = function() {
  return;
}

/**
 * Set the mesh function for zincObject.
 * 
 * @param {THREE.Mesh} mesh - Mesh to be set for this zinc object.
 * @param {Boolean} localTimeEnabled - A flag to indicate either the mesh is
 * time dependent.
 * @param {Boolean} localMorphColour - A flag to indicate either the colour is
 * time dependent.
 */
ZincObject.prototype.setMesh = function(mesh, localTimeEnabled, localMorphColour) {
  //Note: we assume all layers are consistent with time frame
  //Thus adding them to the same animation group should work.
  //This step is only required for the primary (level 0) mesh.
  this.animationGroup = new THREE.AnimationObjectGroup(mesh);
  this.mixer = new THREE.AnimationMixer(this.animationGroup);
  const geometry = mesh.geometry;
  this.geometry = mesh.geometry;
  this.clipAction = undefined;
  if (geometry && geometry.morphAttributes) {
    let morphAttribute = geometry.morphAttributes.position;
    if (!morphAttribute) {
      morphAttribute = geometry.morphAttributes.color ?
        geometry.morphAttributes.color :
        geometry.morphAttributes.normal;
    }
    if (morphAttribute) {
      this.animationClip = THREE.AnimationClip.CreateClipsFromMorphTargetSequences(
        morphAttribute, 10, true);
      if (this.animationClip && (this.animationClip[0] != undefined)) {
        this.clipAction = this.mixer.clipAction(this.animationClip[0]).setDuration(
          this.duration);
        this.clipAction.loop = THREE.loopOnce;
        this.clipAction.clampWhenFinished = true;
        this.clipAction.play();
      }
    }
  }
  this.timeEnabled = localTimeEnabled;
  this.morphColour = localMorphColour;
  mesh.userData = this;
  mesh.matrixAutoUpdate = false;
  this.setMorph(mesh);
  this.checkTransparentMesh();
  if (this.timeEnabled) {
    this.setFrustumCulled(false);
  } else {
    if (this.morphColour) {
      geometry.setAttribute('morphTarget0', geometry.getAttribute( 'position' ) );
      geometry.setAttribute('morphTarget1', geometry.getAttribute( 'position' ) );
    }
  }
  this.boundingBoxUpdateRequired = true;
}

/**
 * Set the name for this ZincObject.
 * 
 * @param {String} groupNameIn - Name to be set.
 */
ZincObject.prototype.setName = function(groupNameIn) {
  this.groupName = groupNameIn;
  this._lod.setName(groupNameIn);
}

/**
 * Get the local time of this geometry, it returns a value between 
 * 0 and the duration.
 * 
 * @return {Number}
 */
ZincObject.prototype.getCurrentTime = function() {
  if (this.clipAction) {
    const ratio = this.clipAction.time / this.clipAction._clip.duration;
    return this.duration * ratio;
  } else {
    return this.inbuildTime;
  }
}

/**
 * Set the local time of this geometry.
 * 
 * @param {Number} time - Can be any value between 0 to duration.
 */
ZincObject.prototype.setMorphTime = function(time) {
  let timeChanged = false;
  if (this.clipAction) {
    const ratio = time / this.duration;
    const actualDuration = this.clipAction._clip.duration;
    let newTime = ratio * actualDuration;
    if (newTime != this.clipAction.time) {
      this.clipAction.time = newTime;
      timeChanged = true;
    }
    if (timeChanged && this.isTimeVarying()) {
      this.mixer.update( 0.0 );
    }
  } else {
    let newTime = time; 
    if (time > this.duration)
      newTime = this.duration;
    else if (0 > time)
      newTime = 0;
    else
      newTime = time;
    if (newTime != this.inbuildTime) {
      this.inbuildTime = newTime;
      timeChanged = true;
    }
  }
  if (timeChanged) {
    this.boundingBoxUpdateRequired = true;
    this._lod.updateMorphColorAttribute(true);
    if (this.timeEnabled)
      this.markerUpdateRequired = true;
  }
}

/**
 * Check if the geometry is time varying.
 * 
 * @return {Boolean}
 */
ZincObject.prototype.isTimeVarying = function() {
  if (this.timeEnabled || this.morphColour)
    return true;
  return false;
}

/**
 * Get the visibility of this Geometry.
 * 
 */
ZincObject.prototype.getVisibility = function() {
  return this.visible;
}

/**
 * Set the visibility of this Geometry.
 * 
 * @param {Boolean} visible - a boolean flag indicate the visibility to be set 
 */
ZincObject.prototype.setVisibility = function(visible) {
  if (visible !== this.visible) {
    this.visible = visible;
    this.group.visible = visible;
    if (this.region) this.region.pickableUpdateRequired = true;
  }
}

/**
 * Set the opacity of this Geometry. This function will also set the isTransparent
 * flag according to the provided alpha value.
 * 
 * @param {Number} alpah - Alpha value to set for this geometry, 
 * can be any value between from 0 to 1.0.
 */
ZincObject.prototype.setAlpha = function(alpha) {
  const material = this._lod._material;
  let isTransparent = false;
  if (alpha  < 1.0)
    isTransparent = true;
  material.opacity = alpha;
  material.transparent = isTransparent;
  this.checkTransparentMesh();
}

/**
 * The rendering will be culled if it is outside of the frustrum
 * when this flag is set to true, it should be set to false if
 * morphing is enabled.
 * 
 * @param {Boolean} flag - Set frustrum culling on/off based on this flag.
 */
ZincObject.prototype.setFrustumCulled = function(flag) {
  //multilayers - set for all layers
  this._lod.setFrustumCulled(flag);
}

/**
 * Set rather a zinc object should be displayed using per vertex colour or
 * not.
 * 
 * @param {Boolean} vertexColors - Set display with vertex color on/off.
 */
ZincObject.prototype.setVertexColors = function(vertexColors) {
  //multilayers - set for all
  this._lod.setVertexColors(vertexColors);

}

/**
 * Get the colour of the mesh.
 * 
 * @return {THREE.Color}
 */
ZincObject.prototype.getColour = function() {
  if (this._lod._material)
    return this._lod._material.color;
	return undefined;
}
  
/**
 * Set the colour of the mesh.
 * 
 * @param {THREE.Color} colour - Colour to be set for this geometry.
 */
ZincObject.prototype.setColour = function(colour) {
  this._lod.setColour(colour);
}

/**
 * Get the colour of the mesh in hex string form.
 * 
 * @return {String}
 */
ZincObject.prototype.getColourHex = function() {
  if (!this.morphColour) {
    if (this._lod._material && this._lod._material.color)
      return this._lod._material.color.getHexString();
  }
  return undefined;
}

/**
 * Set the colour of the mesh using hex in string form.
 * 
 * @param {String} hex - The colour value in hex form.
 */
ZincObject.prototype.setColourHex = function(hex) {
  this._lod._material.color.setHex(hex);
  if (this._lod._secondaryMaterial) {
    this._lod._secondaryMaterial.color.setHex(hex);
  }
}

/**
 * Set the emissive rgb of the mesh using rgb.
 * 
 * @param {String} colour - The colour value in rgb form.
 */
ZincObject.prototype.setEmissiveRGB = function(colour) {
  if (this._lod._material && this._lod._material.emissive) {
    this._lod._material.emissive.setRGB(...colour);
  }
  if (this._lod._secondaryMaterial) {
    this._lod._secondaryMaterial.emissive.setRGB(...colour);
  }
}


/**
 * Set the material of the geometry.
 * 
 * @param {THREE.Material} material - Material to be set for this geometry.
 */
ZincObject.prototype.setMaterial = function(material) {
  this._lod.setMaterial(material);
}

/**
 * Get the index of the closest vertex to centroid.
 * 
 * @return {Number} - integer index in the array
 */
ZincObject.prototype.getClosestVertexIndex = function() {
  let closestIndex = -1;
  const morph = this.getMorph();
  if (morph && morph.geoemtry) {
    let position = morph.geometry.attributes.position;
    this._b1.setFromBufferAttribute(position);
    this._b1.getCenter(this._v1);
    if (position) {
      let distance = -1;
      let currentDistance = 0;
      for (let i = 0; i < position.count; i++) {
        this._v2.fromArray(position.array, i * 3);
        currentDistance = this._v2.distanceTo(this._v1);
        if (distance == -1)
          distance = currentDistance;
        else if (distance > (currentDistance)) {
          distance = currentDistance;
          closestIndex = i;
        }
      }
    }
  }
  return closestIndex;
}

/**
 * Get the  closest vertex to centroid.
 * 
 * @return {THREE.Vector3}
 */
ZincObject.prototype.getClosestVertex = function(applyMatrixWorld) {
  let position = new THREE.Vector3();
  if (this.closestVertexIndex == -1) {
    this.closestVertexIndex = this.getClosestVertexIndex();
  }
  const morph = this.getMorph();
  if (morph && morph.geometry && this.closestVertexIndex >= 0) {
    let influences = morph.morphTargetInfluences;
    let attributes = morph.geometry.morphAttributes;
    if (influences && attributes && attributes.position) {
      let found = false;
      for (let i = 0; i < influences.length; i++) {
        if (influences[i] > 0) {
          found = true;
          this._v1.fromArray(
            attributes.position[i].array, this.closestVertexIndex * 3);
          position.add(this._v1.multiplyScalar(influences[i]));
        }
      }
      if (found) {
        return applyMatrixWorld ? position.applyMatrix4(morph.matrixWorld) : position;
      }
    } else {
      position.fromArray(morph.geometry.attributes.position.array,
        this.closestVertexIndex * 3);
      return applyMatrixWorld ? position.applyMatrix4(morph.matrixWorld) : position;
    }
  }
  this.getBoundingBox();
  position.copy(this.center);
  return applyMatrixWorld ? position.applyMatrix4(this.morph.matrixWorld) : position;
}

/**
 * Get the bounding box of this geometry.
 * 
 * @return {THREE.Box3}.
 */
ZincObject.prototype.getBoundingBox = function() {
  if (this.visible) {
    let morph = this._lod.getCurrentMorph();
    if (morph && morph.visible) {
      if (this.boundingBoxUpdateRequired) {
        require("../utilities").getBoundingBox(morph, this.cachedBoundingBox,
          this._b1, this._v1, this._v2);
        this.cachedBoundingBox.getCenter(this.center);
        this.radius = this.center.distanceTo(this.cachedBoundingBox.max);
        this.boundingBoxUpdateRequired = false;
      }
      return this.cachedBoundingBox;
    }
  }
  return undefined;
}

/**
 * Clear this geometry and free the memory.
 */
ZincObject.prototype.dispose = function() {
  //multilayyers
  this._lod.dispose();
  this.animationGroup = undefined;
  this.mixer = undefined;
  this.morph = undefined;
  this.group = undefined;
  this.clipAction = undefined;
  this.groupName = undefined;
}

/**
 * Check if marker is enabled based on the objects settings with 
 * the provided scene options.
 * 
 * @return {Boolean} 
 */
ZincObject.prototype.markerIsRequired = function(options) {
  if (this.visible && 
    (this.markerMode === "on" || (options && options.displayMarkers &&
    (this.markerMode === "inherited")))) {
      return true;
  }
  return false;
}

/**
 * Update the marker's position and size based on current viewport. 
 */
ZincObject.prototype.updateMarker = function(playAnimation, options) {
  if ((playAnimation == false) &&
    (this.markerIsRequired(options)))
  {
    let ndcToBeUpdated = options.ndcToBeUpdated;
    if (this.groupName) {
      if (!this.marker) {
        this.marker = new (require("./marker").Marker)(this);
        this.markerUpdateRequired = true;
      }
      if (this.markerUpdateRequired) {
        let position = this.getClosestVertex(false);
        if (position) {
          this.marker.setPosition(position.x, position.y, position.z);
          this.markerUpdateRequired = false;
        }
      }
      if (!this.marker.isEnabled()) {
        if (options.markersList &&
          (!(this.marker.uuid in options.markersList))) {    
            ndcToBeUpdated = true;
          options.markersList[this.marker.uuid] = this.marker;
        }
        this.marker.enable();
        this.group.add(this.marker.morph);
      }
      this.marker.setNumber(this.markerNumber);
      if (options && options.camera && (ndcToBeUpdated ||
        options.markerCluster.markerUpdateRequired)) {
        this.marker.updateNDC(options.camera.cameraObject);
        options.markerCluster.markerUpdateRequired = true;
      }
    }
  } else {
    if (this.marker && this.marker.isEnabled()) {
      this.marker.disable();
      this.group.remove(this.marker.morph);
      if (options.markersList &&
        (this.marker.uuid in options.markersList)) {
        options.markerCluster.markerUpdateRequired = true;
        delete options.markersList[this.marker.uuid];
      }
    }
    this.markerUpdateRequired = true;
  }
}

ZincObject.prototype.processMarkerVisual = function(min, max) {
  if (this.marker && this.marker.isEnabled()) {
    this.marker.updateVisual(min, max);
  }
}

ZincObject.prototype.initiateMorphColor = function() {
  //Multilayers - set all
  if (this.morphColour == 1) {
    this._lod.updateMorphColorAttribute(false);
  }
}

ZincObject.prototype.setRenderOrder = function(renderOrder) {
  //multiilayers
  this._lod.setRenderOrder(renderOrder);
}

/**
 * Get the windows coordinates.
 * 
 * @return {Object} - position and rather the closest vertex is on screen.
 */
ZincObject.prototype.getClosestVertexDOMElementCoords = function(scene) {
  if (scene && scene.camera) {
    let inView = true;
    const position = this.getClosestVertex(true);
    position.project(scene.camera);
    position.z = Math.min(Math.max(position.z, 0), 1);
    if (position.x > 1 || position.x < -1 || position.y > 1 || position.y < -1) {
      inView = false;
    }
    scene.getZincCameraControls().getRelativeCoordsFromNDC(position.x, position.y, position);
    return {position, inView};
  } else {
    return undefined;
  }
}

/**
 * Set marker mode for this zinc object which determine rather the
 * markers should be displayed or not.
 *
 * @param {string} mode - There are three options:
 * "on" - marker is enabled regardless of settings of scene
 * "off" - marker is disabled regardless of settings of scene
 * "inherited" - Marker settings on scene will determine the visibility
 *  of the marker.
 * 
 * @return {Boolean} 
 */
 ZincObject.prototype.setMarkerMode = function(mode, options) {
  if (mode !== this.markerMode) {
    if (mode === "on" || mode === "off") {
      this.markerMode = mode;
    } else {
      this.markerMode = "inherited";
    }
    if (this.region) {
      this.region.pickableUpdateRequired = true;
    }
  }
  if (options) {
    this.markerNumber = options.number;
  }
}

//Update the geometry and colours depending on the morph.
ZincObject.prototype.render = function(delta, playAnimation,
  cameraControls, options) {
  if (this.visible && !(this.timeEnabled && playAnimation)) {
    this._lod.update(cameraControls, this.center);
  }
  if (playAnimation == true)
  {
    if ((this.clipAction) && this.isTimeVarying()) {
      this.mixer.update( delta );
    }
    else {
      let targetTime = this.inbuildTime + delta;
      if (targetTime > this.duration)
        targetTime = targetTime - this.duration;
      this.inbuildTime = targetTime;
    }
    //multilayers
    if (this.visible && delta != 0) {
      this.boundingBoxUpdateRequired = true;
      if (this.morphColour == 1) {
        this._lod.updateMorphColorAttribute(true);
      }
    }
  }
  this.updateMarker(playAnimation, options);
}


/**
 * Add lod from an url into the lod object.
 */
ZincObject.prototype.addLOD = function(loader, level, url, preload) {
  this._lod.addLevelFromURL(loader, level, url, preload);
}

/**
 * Add lod from an url into the lod object.
 */
ZincObject.prototype.addVertices = function(coords) {
  let mesh = this.getMorph();
  let geometry = undefined;
  if (!mesh) {
    geometry = createBufferGeometry((coords.length + 500), coords);
    this.drawRange = coords.length;
  } else {
    if (this.drawRange > -1) {
      const positionAttribute = mesh.geometry.getAttribute( 'position' );
      coords.forEach(coord => {
        positionAttribute.setXYZ(this.drawRange, coord[0], coord[1], coord[2])
        ++this.drawRange;
      });
      positionAttribute.needsUpdate = true;
      mesh.geometry.setDrawRange(0, this.drawRange);
      mesh.geometry.computeBoundingBox();
      mesh.geometry.computeBoundingSphere();
      geometry = mesh.geoemtry;
      this.boundingBoxUpdateRequired = true;
    }
  }
  return geometry;
}

/**
 * Set the objects position.
 * 
 * @return {THREE.Box3}.
 */
ZincObject.prototype.setPosition = function(x, y, z) {
  const group = this.getGroup();
  if (group) {
    group.position.set(x, y, z);
    group.updateMatrix();
    this.boundingBoxUpdateRequired = true;
  }
}

/**
 * Set the objects scale.
 * 
 * @return {THREE.Box3}.
 */
ZincObject.prototype.setScaleAll = function(scale) {
  const group = this.getGroup();
  if (group) {
    group.scale.set(scale, scale, scale);
    group.updateMatrix();
    this.boundingBoxUpdateRequired = true;
  }
}


exports.ZincObject = ZincObject;