const THREE = require('three'); const createBufferGeometry = require('../utilities').createBufferGeometry; const resolveURL = require('../utilities').resolveURL; 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 (this.markerImgURL) { this.marker.loadUserSprite(this.markerImgURL); } else { this.marker.setDefaultSprite(); } 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; this.markerImgURL = options.imgURL; } } //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, index, preload) { this._lod.addLevelFromURL(loader, level, url, index, 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(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; } } ZincObject.prototype.loadAdditionalSources = function(primitivesLoader, sources) { primitivesLoader.load(resolveURL(filename), meshloader(region, colour, opacity, localTimeEnabled, localMorphColour, undefined, undefined, undefined, undefined, finishCallback), this.onProgress(filename), this.onError(finishCallback)); } /** * 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;