const THREE = require('three'); const MarkerCluster = require('./primitives/markerCluster').MarkerCluster; const SceneLoader = require('./sceneLoader').SceneLoader; const SceneExporter = require('./sceneExporter').SceneExporter; const Viewport = require('./controls').Viewport; const createBufferGeometry = require('./utilities').createBufferGeometry; const getCircularTexture = require('./utilities').getCircularTexture; let uniqueiId = 0; const getUniqueId = function () { return "sc" + uniqueiId++; } const defaultMetadata = function() { return { Duration: "6 secs", OriginalDuration: "-", TimeStamps: {} } }; const defaultDuration = 6000; /** * A Scene contains {@link Region},and * {@link CameraControls} which controls the viewport and additional features. * It is the main object used for controlling what is and what is not displayed * on the renderer. * * @class * @param {Object} containerIn - Container to create the renderer on. * @author Alan Wu * @return {Scene} */ exports.Scene = function (containerIn, rendererIn) { const container = containerIn; let videoHandler = undefined; let sceneLoader = new SceneLoader(this); let minimap = undefined; let zincObjectAddedCallbacks = {}; let zincObjectAddedCallbacks_id = 0; let zincObjectRemovedCallbacks = {}; let zincObjectRemovedCallbacks_id = 0; const scene = new THREE.Scene(); const rootRegion = new (require('./region').Region)(undefined, this); scene.add(rootRegion.getGroup()); const tempGroup = new THREE.Group(); scene.add(tempGroup); /** * A {@link THREE.DirectionalLight} object for controlling lighting of this scene. */ this.directionalLight = undefined; /** * a {@link THREE.AmbientLight} for controlling the ambient lighting of this scene. */ this.ambient = undefined; this.camera = undefined; let duration = 6000; let zincCameraControls = undefined; this.sceneName = undefined; let stereoEffectFlag = false; let stereoEffect = undefined; this.autoClearFlag = true; this.displayMarkers = false; this.displayMinimap = false; this.minimapScissor = { x_offset: 16, y_offset: 16, width: 128, height: 128, align: "top-left", updateRequired: true }; let scissor = {x: 0, y: 0}; let metadata = defaultMetadata(); let _markerTarget = new THREE.Vector2(); let pickableObjectsList = []; this.forcePickableObjectsUpdate = false; this.uuid = getUniqueId(); let markerCluster = new MarkerCluster(this); markerCluster.disable(); scene.add(markerCluster.group); const getDrawingWidth = () => { if (container) if (typeof container.clientWidth !== "undefined") return container.clientWidth; else return container.width; return 0; } const getDrawingHeight = () => { if (container) if (typeof container.clientHeight !== "undefined") return container.clientHeight; else return container.height; return 0; } /** * This function returns a three component array, which contains * [totalsize, totalLoaded and errorDownload] of all the downloads happening * in this scene. * @returns {Array} */ this.getDownloadProgress = () => { return sceneLoader.getDownloadProgress(); } //called from Renderer when panel has been resized this.onWindowResize = () => { const wHeight = getDrawingHeight(); this.camera.aspect = getDrawingWidth() / wHeight; this.camera.updateProjectionMatrix(); this.minimapScissor.updateRequired = true; zincCameraControls.onResize(); zincCameraControls.calculateHeightPerPixelAtZeroDepth(wHeight); } /** * Reset the viewport of this scene to its original state. */ this.resetView = () => { this.onWindowResize(); zincCameraControls.resetView(); } /** * Set the zoom level by unit scroll rate */ this.changeZoomByScrollRateUnit = unit => { zincCameraControls.changeZoomByScrollRateUnit(unit); } //Setup the camera for this scene, it also initialise the lighting const setupCamera = () => { this.camera = new THREE.PerspectiveCamera(40, getDrawingWidth() / getDrawingHeight(), 0.0, 10.0); this.ambient = new THREE.AmbientLight(0xffffff, 0.2); scene.add(this.ambient); this.directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); scene.add(this.directionalLight); zincCameraControls = new (require('./controls').CameraControls)(this.camera, rendererIn.domElement, rendererIn, this); zincCameraControls.setDirectionalLight(this.directionalLight); zincCameraControls.resetView(); minimap = new (require('./minimap').Minimap)(this); }; setupCamera(); /** * Load the viewport Data from the argument {@link Zinc.Viewport} and set it as * the default viewport of this scene. * * @param {Zinc.Viewport} viewData - Viewport data to be loaded. */ this.loadView = settings => { const viewPort = new Viewport(); viewPort.setFromObject(settings); zincCameraControls.setCurrentCameraSettings(viewPort); return true; } /** * Set up multiple views. * * @param {Zinc.Viewport} viewData - Viewport data to be loaded. */ this.setupMultipleViews = (defaultView, entries) => { for (const [name, settings] of Object.entries(entries)) { const viewport = new Viewport(); viewport.setFromObject(settings); zincCameraControls.addViewport(name, viewport); } zincCameraControls.setDefaultViewport(defaultView); } /** * Get the bounding box of all the object in this scene only. * * @returns {THREE.Box3} */ this.getBoundingBox = () => { return rootRegion.getBoundingBox(true); } /** * Adjust the viewport to display the desired volume provided by the bounding box. * * @param {THREE.Box3} boundingBox - The bounding box which describes the volume of * which we the viewport should be displaying. */ this.viewAllWithBoundingBox = boundingBox => { if (boundingBox) { const viewport = zincCameraControls.getViewportFromBoundingBox(boundingBox, 1.0); zincCameraControls.setCurrentCameraSettings(viewport); zincCameraControls.calculateHeightPerPixelAtZeroDepth(getDrawingHeight()); markerCluster.markerUpdateRequired = true; } } /** * Adjust zoom distance to include all primitives in scene only. */ this.viewAll = () => { const boundingBox = this.getBoundingBox(); this.viewAllWithBoundingBox(boundingBox); markerCluster.markerUpdateRequired = true; } /** * A function which iterates through the list of geometries and call the callback * function with the geometries as the argument. * @param {Function} callbackFunction - Callback function with the geometry * as an argument. */ this.forEachGeometry = callbackFunction => { rootRegion.forEachGeometry(callbackFunction, true); } /** * A function which iterates through the list of glyphsets and call the callback * function with the glyphset as the argument. * @param {Function} callbackFunction - Callback function with the glyphset * as an argument. */ this.forEachGlyphset = callbackFunction => { rootRegion.forEachGlyphset(callbackFunction, true); } /** * A function which iterates through the list of pointsets and call the callback * function with the pointset as the argument. * @param {Function} callbackFunction - Callback function with the pointset * as an argument. */ this.forEachPointset = callbackFunction => { rootRegion.forEachPointset(callbackFunction, true); } /** * A function which iterates through the list of lines and call the callback * function with the lines as the argument. * @param {Function} callbackFunction - Callback function with the lines * as an argument. */ this.forEachLine = callbackFunction => { rootRegion.forEachLine(callbackFunction, true); } /** * Find and return all geometries in this scene with the matching GroupName. * * @param {String} GroupName - Groupname to match with. * @returns {Array} */ this.findGeometriesWithGroupName = GroupName => { return rootRegion.findGeometriesWithGroupName(GroupName, true); } /** * Find and return all pointsets in this scene with the matching GroupName. * * @param {String} GroupName - Groupname to match with. * @returns {Array} */ this.findPointsetsWithGroupName = GroupName => { return rootRegion.findPointsetsWithGroupName(GroupName, true); } /** * Find and return all glyphsets in this scene with the matching GroupName. * * @param {String} GroupName - Groupname to match with. * @returns {Array} */ this.findGlyphsetsWithGroupName = GroupName => { return rootRegion.findGlyphsetsWithGroupName(GroupName, true); } /** * Find and return all lines in this scene with the matching GroupName. * * @param {String} GroupName - Groupname to match with. * @returns {Array} */ this.findLinesWithGroupName = GroupName => { return rootRegion.findLinesWithGroupName(GroupName, true); } /** * Find a list of objects with the specified name, this will * tranverse through the region tree to find all child objects * with matching name. * * @param {String} GroupName - Groupname to match with. * @returns {Array} */ this.findObjectsWithGroupName = GroupName => { return rootRegion.findObjectsWithGroupName(GroupName, true); } this.findObjectsWithAnatomicalId = anatomicalId => { return rootRegion.findObjectsWithAnatomicalId(anatomicalId, true); } /** * Get the bounding box of all zinc objects in list. * * @param {Array} objectsArray - Groupname to match with. * @returns {THREE.Box3} */ this.getBoundingBoxOfZincObjects = objectsArray => { let boundingBox = undefined; for (let i = 0; i < objectsArray.length; i++) { let box = objectsArray[i].getBoundingBox(); if (box) { if (!boundingBox) boundingBox = box; else boundingBox.union(box); } } return boundingBox; } /** * Convert the vector3 into screen coordinates. * * @param {THREE.Vector3} point - Vector 3 containing the point to convert, * this vector will be overwritten with the returned value. * @param {Array} objectsArray - Groupname to match with. * @returns {THREE.Vector3} */ this.vectorToScreenXY = point => { point.project(this.camera); let width = getDrawingWidth(); let height = getDrawingHeight(); let widthHalf = (width / 2); let heightHalf = (height / 2); point.x = (point.x * widthHalf) + widthHalf; point.y = - (point.y * heightHalf) + heightHalf; return point; } /** * Get the screen coordinate of the centroid of provided list of objects. * * @param {Array} zincObjects - List of {@link ZincObject}. * @returns {THREE.Vector3} */ this.getObjectsScreenXY = zincObjects => { if (zincObjects && zincObjects.length > 0) { let boundingBox = this.getBoundingBoxOfZincObjects(zincObjects); const center = new THREE.Vector3(); boundingBox.getCenter(center); return this.vectorToScreenXY(center); } return undefined; } /** * Get the screen coordinate of the centroid of all objects * in scene with the provided name. * * @param {String} name - List of {@link ZincObject}. * @returns {THREE.Vector3} */ this.getNamedObjectsScreenXY = name => { let zincObjects = this.findObjectsWithGroupName(name); return this.getObjectsScreenXY(zincObjects); }; /** * Add zinc object into the root {@link Region} of sfcene. * * @param {ZincObject} - zinc object ot be added. * @returns {THREE.Vector3} */ this.addZincObject = zincObject => { if (zincObject) { rootRegion.addZincObject(zincObject); if (zincCameraControls) zincCameraControls.calculateMaxAllowedDistance(this); } } /** * Load a glyphset into this scene object. * * @param {String} metaurl - Provide informations such as transformations, colours * and others for each of the glyph in the glyphsset. * @param {String} glyphurl - regular json model file providing geometry of the glyph. * @param {String} groupName - name to assign the glyphset's groupname to. * @param {Function} finishCallback - Callback function which will be called * once the glyphset is succssfully load in. */ this.loadGlyphsetURL = (metaurl, glyphurl, groupName, finishCallback) => { sceneLoader.loadGlyphsetURL(rootRegion, metaurl, glyphurl, groupName, finishCallback); } /** * Load a pointset into this scene object. * * @param {String} metaurl - Provide informations such as transformations, colours * and others for each of the glyph in the glyphsset. * @param {Boolean} timeEnabled - Indicate if morphing is enabled. * @param {Boolean} morphColour - Indicate if color morphing is enabled. * @param {STRING} groupName - name to assign the pointset's groupname to. * @param {Function} finishCallback - Callback function which will be called * once the glyphset is succssfully load in. */ this.loadPointsetURL = (url, timeEnabled, morphColour, groupName, finishCallback) => { sceneLoader.loadPointsetURL(rootRegion, url, timeEnabled, morphColour, groupName, finishCallback); } /** * Load lines into this scene object. * * @param {String} metaurl - Provide informations such as transformations, colours * and others for each of the glyph in the glyphsset. * @param {Boolean} timeEnabled - Indicate if morphing is enabled. * @param {Boolean} morphColour - Indicate if color morphing is enabled. * @param {STRING} groupName - name to assign the pointset's groupname to. * @param {Function} finishCallback - Callback function which will be called * once the glyphset is succssfully load in. */ this.loadLinesURL = (url, timeEnabled, morphColour, groupName, finishCallback) => { sceneLoader.loadLinesURL(rootRegion, url, timeEnabled, morphColour, groupName, finishCallback); } /** * Read a STL file into this scene, the geometry will be presented as * {@link Zinc.Geometry}. * * @param {STRING} url - location to the STL file. * @param {STRING} groupName - name to assign the geometry's groupname to. * @param {Function} finishCallback - Callback function which will be called * once the STL geometry is succssfully loaded. */ this.loadSTL = (url, groupName, finishCallback) => { sceneLoader.loadSTL(rootRegion, url, groupName, finishCallback); } /** * Read a OBJ file into this scene, the geometry will be presented as * {@link Zinc.Geometry}. * * @param {STRING} url - location to the STL file. * @param {STRING} groupName - name to assign the geometry's groupname to. * @param {Function} finishCallback - Callback function which will be called * once the OBJ geometry is succssfully loaded. */ this.loadOBJ = (url, groupName, finishCallback) => { sceneLoader.loadOBJ(rootRegion, url, groupName, finishCallback); } /** * Load a metadata file from the provided URL into this scene. Once * succssful scene proceeds to read each items into scene for visualisations. * * @param {String} url - Location of the metafile * @param {Function} finishCallback - Callback function which will be called * for each glyphset and geometry that has been written in. */ this.loadMetadataURL = (url, finishCallback, allCompletedCallback) => { sceneLoader.loadMetadataURL(rootRegion, url, finishCallback, allCompletedCallback); } /** * Load a legacy model(s) format with the provided URLs and parameters. This only loads the geometry * without any of the metadata. Therefore, extra parameters should be provided. * * @deprecated */ this.loadModelsURL = (urls, colours, opacities, timeEnabled, morphColour, finishCallback) => { sceneLoader.loadModelsURL(rootRegion. urls, colours, opacities, timeEnabled, morphColour, finishCallback); } /** * Load the viewport from an external location provided by the url. * @param {String} URL - address to the file containing viewport information. */ this.loadViewURL = url => { sceneLoader.loadViewURL(url); } /** * Load a legacy file format containing the viewport and its meta file from an external * location provided by the url. Use the new metadata format with * {@link Zinc.Scene#loadMetadataURL} instead. * * @param {String} URL - address to the file containing viewport and model information. * @deprecated */ this.loadFromViewURL = (jsonFilePrefix, finishCallback) => { sceneLoader.loadFromViewURL(jsonFilePrefix, finishCallback); } /** * Load GLTF into this scene object. */ this.loadGLTF = (url, finishCallback, allCompletedCallback, options) => { sceneLoader.loadGLTF(rootRegion, url, finishCallback, allCompletedCallback, options); } //Update the directional light for this scene. this.updateDirectionalLight = () => { zincCameraControls.updateDirectionalLight(); } /** * Add any {THREE.Object} into this scene. * @param {THREE.Object} object - to be addded into this scene. */ this.addObject = object => { scene.add(object); } /** * Remove any {THREE.Object} from this scene. * @param {THREE.Object} object - to be removed from this scene. */ this.removeObject = object => { scene.remove(object); } /** * Get the current time of the scene. * @return {Number} */ this.getCurrentTime = () => { if (videoHandler != undefined) { return videoHandler.getCurrentTime(duration); } const time = rootRegion.getCurrentTime(); if (time !== -1) return time; return 0; } /** * Set the current time of all the geometries and glyphsets of this scene. * @param {Number} time - Value to set the time to. */ this.setMorphsTime = (time) => { if (videoHandler != undefined) { videoHandler.setMorphTime(time, duration); } rootRegion.setMorphTime(time, true); } /** * Check if any object in this scene is time varying. * * @return {Boolean} */ this.isTimeVarying = () => { if (videoHandler && videoHandler.video && !videoHandler.video.error) { return true; } return rootRegion.isTimeVarying(); } /** * Update geometries and glyphsets based on the calculated time. * @private */ this.renderGeometries = (playRate, delta, playAnimation) => { // Let video dictates the progress if one is present let options = {}; options.camera = zincCameraControls; //Global markers flag, marker can be set at individual zinc object level //overriding this flag. options.displayMarkers = this.displayMarkers; options.markerCluster = markerCluster; options.markersList = markerCluster.markers; options.ndcToBeUpdated = false; //Always set marker cluster update required when playAnimation is true //to make sure it is updated when it stops if (playAnimation) { options.markerCluster.markerUpdateRequired = true; } if (videoHandler) { if (videoHandler.isReadyToPlay()) { if (playAnimation) { videoHandler.video.play(); } else { videoHandler.video.pause(); } const currentTime = videoHandler.video.currentTime / videoHandler.getVideoDuration() * duration; if (0 == sceneLoader.toBeDownloaded) { zincCameraControls.setTime(currentTime); options.ndcToBeUpdated = zincCameraControls.update(0); if (options.ndcToBeUpdated) { zincCameraControls.calculateHeightPerPixelAtZeroDepth(getDrawingHeight()); } rootRegion.setMorphTime(currentTime, true); rootRegion.renderGeometries(0, 0, playAnimation, zincCameraControls, options, true); } else { zincCameraControls.update(0); } //console.log(videoHandler.video.currentTime / videoHandler.getVideoDuration() * 6000); } else { myPlayRate = 0; } } else { if (0 == sceneLoader.toBeDownloaded) { options.ndcToBeUpdated = zincCameraControls.update(delta); if (options.ndcToBeUpdated) { zincCameraControls.calculateHeightPerPixelAtZeroDepth(getDrawingHeight()); } rootRegion.renderGeometries(playRate, delta, playAnimation, zincCameraControls, options, true); } else { zincCameraControls.update(0); } } } /** * Return the internal {THREE.Scene}. * @return {THREE.Scene} */ this.getThreeJSScene = () => { return scene; } this.setVideoHandler = (videoHandlerIn) => { if (!videoHandler) videoHandler = videoHandlerIn; } /** * Set a group of scenes into this parent scene. This group of * scenes will also be rendered when this scene is rendered. * @private */ this.setAdditionalScenesGroup = scenesGroup => { scene.add(scenesGroup); } let getWindowsPosition = (align, x_offset, y_offset, width, height, renderer_width, renderer_height) => { let x = 0; let y = 0; if (align.includes("top")) { y = renderer_height - height - y_offset; } else if (align.includes("bottom")) { y = y_offset; } else { y = Math.floor((renderer_height - height) / 2.0); } if (align.includes("left")) { x = x_offset; } else if (align.includes("right")) { x = renderer_width - x_offset- width; } else { x = Math.floor((renderer_width - width) / 2.0); } return {x: x, y: y}; } const renderMinimap = renderer => { if (this.displayMinimap === true) { renderer.setScissorTest(true); renderer.getSize(_markerTarget); if (this.minimapScissor.updateRequired) { scissor = getWindowsPosition(this.minimapScissor.align, this.minimapScissor.x_offset, this.minimapScissor.y_offset, this.minimapScissor.width, this.minimapScissor.height, _markerTarget.x, _markerTarget.y); this.minimapScissor.updateRequired = false; } renderer.setScissor( scissor.x, scissor.y, this.minimapScissor.width, this.minimapScissor.height); renderer.setViewport( scissor.x, scissor.y, this.minimapScissor.width, this.minimapScissor.height); minimap.updateCamera(); scene.add(minimap.mask); renderer.render(scene, minimap.camera); scene.remove(minimap.mask); renderer.setScissorTest(false); renderer.setViewport(0, 0, _markerTarget.x, _markerTarget.y); } } /** * Render the scene. * @private */ this.render = renderer => { if (this.autoClearFlag) renderer.clear(); if (stereoEffectFlag && stereoEffect) { stereoEffect.render(scene, this.camera); } else { renderer.render(scene, this.camera); renderMinimap(renderer); } } /** * Enable or disable interactive control, this is on by default. * * @param {Boolean} flag - Indicate either interactive control * should be enabled or disabled. */ this.setInteractiveControlEnable = flag => { if (flag == true) zincCameraControls.enable(); else zincCameraControls.disable(); } /** * Get the camera control of this scene. * @return {Zinc.CameraControls} */ this.getZincCameraControls = () => { return zincCameraControls; } /** * Get the internal {THREE.Scene}. * @return {THREE.Scene} */ this.getThreeJSScene = () => { return scene; } /** * Set the default duration value for geometries and glyphsets * that are to be loaded into this scene. * @param {Number} durationIn - duration of the scene. */ this.setDuration = durationIn => { rootRegion.setDuration(durationIn); duration = durationIn; zincCameraControls.setPathDuration(durationIn); sceneLoader.duration = durationIn; } /** * Get the default duration value. * @return {Number} */ this.getDuration = () => { return duration; } /** * Enable or disable stereo effect of this scene. * @param {Boolean} stereoFlag - Indicate either stereo effect control * should be enabled or disabled. */ this.setStereoEffectEnable = stereoFlag => { if (stereoFlag == true) { if (!stereoEffect) { stereoEffect = new require('./controls').StereoEffect(rendererIn); } } rendererIn.setSize(getDrawingWidth(), getDrawingHeight()); this.camera.updateProjectionMatrix(); stereoEffectFlag = stereoFlag; } /** * Check rather object is in scene. * * @return {Boolean} */ this.objectIsInScene = zincObject => { return rootRegion.objectIsInRegion(zincObject, true); } /** * Rotate the camera view to view the entirety of the * bounding box with a smooth transition within the providied * transitionTime. * * @param {THREE.Box3} boundingBox - the bounding box to target * @param {Number} transitionTime - Duration to perform the transition. */ this.alignBoundingBoxToCameraView = (boundingBox, transitionTime) => { if (boundingBox) { const center = new THREE.Vector3(); boundingBox.getCenter(center); const viewport = this.getZincCameraControls().getCurrentViewport(); const target = new THREE.Vector3(viewport.targetPosition[0], viewport.targetPosition[1], viewport.targetPosition[2]); const eyePosition = new THREE.Vector3(viewport.eyePosition[0], viewport.eyePosition[1], viewport.eyePosition[2]); const newVec1 = new THREE.Vector3(); const newVec2 = new THREE.Vector3(); newVec1.subVectors(target, eyePosition).normalize(); newVec2.subVectors(target, center).normalize(); const newVec3 = new THREE.Vector3(); newVec3.crossVectors(newVec1, newVec2); const angle = newVec1.angleTo(newVec2); if (transitionTime > 0) { this.getZincCameraControls().rotateCameraTransition(newVec3, angle, transitionTime); this.getZincCameraControls().enableCameraTransition(); } else { this.getZincCameraControls().rotateAboutLookAtpoint(newVec3, angle); } markerCluster.markerUpdateRequired = true; } } /** * Translate the camera view to the center of the * bounding box with a smooth transition within the providied * transitionTime. * * @param {THREE.Box3} boundingBox - the bounding box to target * @param {Number} transitionTime - Duration to perform the transition. */ this.translateBoundingBoxToCameraView = (boundingBox, scaleRadius, transitionTime) => { if (boundingBox) { const oldViewport = this.getZincCameraControls().getCurrentViewport(); const viewport = this.getZincCameraControls().getViewportFromBoundingBox(boundingBox, scaleRadius); if (transitionTime > 0) { this.getZincCameraControls().cameraTransition(oldViewport, viewport, transitionTime); this.getZincCameraControls().enableCameraTransition(); } markerCluster.markerUpdateRequired = true; } } /** * Transition the camera into viewing the zinc object with a * smooth transition within the providied transitionTime. * * @param {ZincObject} zincObject - the bounding box to target * @param {Number} transitionTime - Duration to perform the transition. */ this.alignObjectToCameraView = (zincObject, transitionTime) => { if (this.objectIsInScene(zincObject)) { const boundingBox = zincObject.getBoundingBox(); this.alignBoundingBoxToCameraView(boundingBox, transitionTime); } } /** * Set the camera to point to the centroid of the zinc object. * * @param {ZincObject} zincObject - the bounding box to target */ this.setCameraTargetToObject = zincObject => { if (this.objectIsInScene(zincObject)) { const center = new THREE.Vector3(); const boundingBox = zincObject.getBoundingBox(); const viewport = this.getZincCameraControls().getCurrentViewport(); boundingBox.getCenter(center); const target = new THREE.Vector3(viewport.targetPosition[0], viewport.targetPosition[1], viewport.targetPosition[2]); const eyePosition = new THREE.Vector3(viewport.eyePosition[0], viewport.eyePosition[1], viewport.eyePosition[2]); const newVec1 = new THREE.Vector3(); const newVec2 = new THREE.Vector3(); newVec1.subVectors(eyePosition, target); newVec2.addVectors(center, newVec1); viewport.eyePosition[0] = newVec2.x; viewport.eyePosition[1] = newVec2.y; viewport.eyePosition[2] = newVec2.z; viewport.targetPosition[0] = center.x; viewport.targetPosition[1] = center.y; viewport.targetPosition[2] = center.z; this.getZincCameraControls().setCurrentCameraSettings(viewport); markerCluster.markerUpdateRequired = true; } } /** * Check if stereo effect is enabled. * @returns {Boolean} */ this.isStereoEffectEnable = () => { return stereoEffectFlag; } /** * Remove a ZincObject from this scene if it presents. This will eventually * destroy the object and free up the memory. * @param {Zinc.Object} zincObject - object to be removed from this scene. */ this.removeZincObject = zincObject => { rootRegion.removeZincObject(zincObject); if (zincCameraControls) { zincCameraControls.calculateMaxAllowedDistance(this); } markerCluster.markerUpdateRequired = true; } /** * Update pickable objects list */ this.updatePickableThreeJSObjects = () => { pickableObjectsList.length = 0; if (markerCluster.isEnabled) { pickableObjectsList.push(markerCluster.group); } rootRegion.getPickableThreeJSObjects(pickableObjectsList, true); this.forcePickableObjectsUpdate = false; } /** * Get all pickable objects. */ this.getPickableThreeJSObjects = () => { //The list will only be updated if changes have been made //in region or a flag has been raise if (this.forcePickableObjectsUpdate || rootRegion.checkPickableUpdateRequred(true)) { this.updatePickableThreeJSObjects(); } return pickableObjectsList; } /** * Get the Normalised coordinates on minimap if mouse event is * inside the minimap */ this.getNormalisedMinimapCoordinates = (renderer, event) => { if (this.displayMinimap) { const target = new THREE.Vector2(); renderer.getSize(target); let offsetY = target.y - event.clientY; if (((scissor.x + this.minimapScissor.width) > event.clientX) && (event.clientX > scissor.x) && ((scissor.y + this.minimapScissor.height) > offsetY) && (offsetY > scissor.y)) { let x = ((event.clientX - scissor.x) / this.minimapScissor.width) * 2.0 - 1.0; let y = ((offsetY - scissor.y) / this.minimapScissor.height) * 2.0 - 1.0; return {"x": x, "y": y}; } } return undefined; } /** * Get the coordinates difference of the current viewing * point and projected coordinates. */ this.getMinimapDiffFromNormalised = (x, y) => { if (minimap) return minimap.getDiffFromNormalised(x, y); return undefined; } this.isWebGL2 = () => { return rendererIn.isWebGL2(); } /** * Remove all objects that are created with ZincJS APIs and it will free the memory allocated. * This does not remove obejcts that are added using the addObject APIs. */ this.clearAll = () => { markerCluster.clear(); rootRegion.clear(true); this.clearZincObjectAddedCallbacks(); this.clearZincObjectRemovedCallbacks(); sceneLoader.toBeDwonloaded = 0; if (zincCameraControls) { zincCameraControls.calculateMaxAllowedDistance(this); } markerCluster.markerUpdateRequired = true; } /** * All time stamp to the metadata TimeStamps field. */ this.addMetadataTimeStamp = (key, time) => { metadata["TimeStamps"][key] = convertDurationObjectTomSec(time); } /** * Get a specific metadata field. */ this.getMetadataTag = key => { return metadata[key]; } /** * Get all metadata set for the scene. */ this.getMetadata = () => { return metadata; } /** * Set a specific metadata field. */ this.setMetadataTag = (key, value) => { metadata[key] = value; } /** * Remove a specific metadata field. */ this.removeMetadataTag = key => { delete metadata[key]; } /** * Reset all metadata fields to original value. */ this.resetMetadata = () => { metadata = defaultMetadata(); } /** * Reset duration of scene to default value. */ this.resetDuration = () => { this.setDuration(defaultDuration); } // Turn the object into a readable string {years: years,months: months, // weeks: weeks, days: days, hours: hours, mins: mins, secs: secs } const convertDurationObjectToString = duration => { return [ ...(duration.years ? [`${duration.years}years`] : []), ...(duration.months ? [`${duration.months}months`] : []), ...(duration.weeks ? [`${duration.weeks}weeks`] : []), ...(duration.days ? [`${duration.days}days`] : []), ...(duration.hours ? [`${duration.hours}hours`] : []), ...(duration.mins ? [`${duration.mins}mins`] : []), ...(duration.secs ? [`${duration.secs}secs`] : []), ].join(' '); } // Turn the object into a number representing milliesecond {years: years,months: months, // weeks: weeks, days: days, hours: hours, mins: mins, secs: secs } const convertDurationObjectTomSec = duration => { return duration.years ? duration.years * 31536000000 : 0 + duration.months ? duration.months * 2592000000 : 0 + duration.weeks ? duration.weeks * 604800000 : 0 + duration.days ? duration.days * 86400000 : 0 + duration.hours ? duration.hours * 3600000 : 0 + duration.mins ? duration.mins * 60000 : 0 + duration.secs ? duration.secs * 1000 : 0; } // Set the readable duration and timer using an object // with the following format {years: years,months: months, weeks: weeks, days: days, // hours: hours, mins: mins, secs: secs } this.setDurationFromObject = duration => { const string = convertDurationObjectToString(duration); const millisec = convertDurationObjectTomSec(duration); this.setMetadataTag("Duration", string); this.setDuration(millisec); } // Set the readable original duration using an object // with the following format {years: years,months: months, weeks: weeks, days: days, // hours: hours, mins: mins, secs: secs } this.setOriginalDurationFromObject = duration => { const string = convertDurationObjectToString(duration); this.setMetadataTag("OriginalDuration", string); } /** * Export the scene in GLTF format, it can either return it in * string or binary form. * * @param {Boolean} binary - Indicate it should be exported as binary or * text. * * @return {Promise} The exported data if the promise resolve successfully */ this.exportGLTF = (binary) => { const exporter = new SceneExporter(this); return exporter.exportGLTF(binary); } /** * Get the root region of the scene. * * @return {Region} Return the root region of the scene */ this.getRootRegion = () => { return rootRegion; } /** * Create points in region specified in the path * */ this.createLines = ( regionPath, groupName, coords, colour ) => { let region = rootRegion.findChildFromPath(regionPath); if (region === undefined) { region = rootRegion.createChildFromPath(regionPath); } return region.createLines(groupName, coords, colour); } /** * Create points in region specified in the path * */ this.createPoints = ( regionPath, groupName, coords, labels, colour ) => { let region = rootRegion.findChildFromPath(regionPath); if (region === undefined) { region = rootRegion.createChildFromPath(regionPath); } return region.createPoints(groupName, coords, labels, colour); } /** * Add a callback function which will be called everytime zinc object is added. * @param {Function} callbackFunction - callbackFunction to be added. * * @return {Number} */ this.addZincObjectAddedCallbacks = callbackFunction => { zincObjectAddedCallbacks_id = zincObjectAddedCallbacks_id + 1; zincObjectAddedCallbacks[zincObjectAddedCallbacks_id] = callbackFunction; return zincObjectAddedCallbacks_id; } /** * Add a callback function which will be called everytime zinc object is removed. * @param {Function} callbackFunction - callbackFunction to be added. * * @return {Number} */ this.addZincObjectRemovedCallbacks = callbackFunction => { zincObjectRemovedCallbacks_id = zincObjectRemovedCallbacks_id + 1; zincObjectRemovedCallbacks[zincObjectRemovedCallbacks_id] = callbackFunction; return zincObjectRemovedCallbacks_id; } /** * Remove a callback function that is previously added to the scene. * @param {Number} id - identifier of the previously added callback function. */ this.removeZincObjectAddedCallbacks = id => { if (id in zincObjectAddedCallbacks_id) { delete zincObjectAddedCallbacks[id]; } } /** * Remove a callback function that is previously added to the scene. * @param {Number} id - identifier of the previously added callback function. */ this.removeZincObjectRemovedCallbacks = id => { if (id in zincObjectRemovedCallbacks_id) { delete zincObjectRemovedCallbacks[id]; } } /** * Clear all zinc object callback function */ this.clearZincObjectAddedCallbacks = () => { zincObjectAddedCallbacks = {}; zincObjectAddedCallbacks_id = 0; } /** * Clear all zinc object callback function */ this.clearZincObjectRemovedCallbacks = () => { zincObjectRemovedCallbacks = {}; zincObjectRemovedCallbacks_id = 0; } /** * Used to trigger zinc object added callback */ this.triggerObjectAddedCallback = (zincObject) => { for (let key in zincObjectAddedCallbacks) { if (zincObjectAddedCallbacks.hasOwnProperty(key)) { zincObjectAddedCallbacks[key](zincObject); } } } /** * Used to trigger zinc object removed callback */ this.triggerObjectRemovedCallback= (zincObject) => { for (let key in zincObjectRemovedCallbacks) { if (zincObjectRemovedCallbacks.hasOwnProperty(key)) { zincObjectRemovedCallbacks[key](zincObject); } } } /* * Add temporary points to the scene which can be removed * with clearTemporaryPrimitives method. */ this.addTemporaryPoints = (coords, colour) => { const geometry = createBufferGeometry(coords.length, coords); let material = new THREE.PointsMaterial({ alphaTest: 0.5, size: 15, color: colour, sizeAttenuation: false }); const texture = getCircularTexture(); material.map = texture; let point = new (require('./three/Points').Points)(geometry, material); tempGroup.add(point); return point; } /* * Add temporary lines to the scene which can be removed * with clearTemporaryPrimitives method. */ this.addTemporaryLines = (coords, colour) => { const geometry = createBufferGeometry(coords.length, coords); const material = new THREE.LineBasicMaterial({color:colour}); const line = new (require("./three/line/LineSegments").LineSegments)(geometry, material); tempGroup.add(line); return line; } /* * Remove object from temporary objects list */ this.removeTemporaryPrimitive = (object) => { tempGroup.remove(object); object.geometry.dispose(); object.material.dispose(); } /* * Remove all temporary primitives. * Return number of primitives removed; */ this.clearTemporaryPrimitives = () => { let i = 0; const children = tempGroup.children; children.forEach(child => { child.geometry.dispose(); child.material.dispose(); i++; }); tempGroup.clear(); return i; } /* * Create primitive based on the bounding box of scene and * add to specify region and group name. */ this.addBoundingBoxPrimitive = (regionPath, group, colour, opacity, visibility, boundingBox = undefined) => { let region = rootRegion.findChildFromPath(regionPath); if (region === undefined) { region = rootRegion.createChildFromPath(regionPath); } const box = boundingBox ? boundingBox : this.getBoundingBox(); const dim = new THREE.Vector3().subVectors(box.max, box.min); const boxGeo = new THREE.BoxGeometry(dim.x, dim.y, dim.z); const primitive = region.createGeometryFromThreeJSGeometry( group, boxGeo, colour, opacity, visibility, 10000); dim.addVectors(box.min, box.max).multiplyScalar( 0.5 ); primitive.setPosition(dim.x, dim.y, dim.z); return primitive; } /* * Create primitive based on the bounding box of scene and * add to specify region and group name. */ this.addSlicesPrimitive = (regionPath, groups, colours, opacity, visibility, boundingBox = undefined) => { if (groups && groups.length >= 3 && colours && colours.length >= 3) { let region = rootRegion.findChildFromPath(regionPath); if (region === undefined) { region = rootRegion.createChildFromPath(regionPath); } const box = boundingBox ? boundingBox : this.getBoundingBox(); const dim = new THREE.Vector3().subVectors(box.max, box.min); const directions = ["x", "y", "z"]; const primitives = []; let index = 0; directions.forEach((direction) => { let planeGeo = undefined; switch(direction) { //YZ plane case "x": planeGeo = new THREE.PlaneGeometry(dim.z, dim.y); planeGeo.rotateY(Math.PI / 2); // code block break; //XZ plane case "y": planeGeo = new THREE.PlaneGeometry(dim.x, dim.z); planeGeo.rotateX(Math.PI / 2); // code block break; //XY plane case "z": planeGeo = new THREE.PlaneGeometry(dim.x, dim.y); // code block break; default: break; } const primitive = region.createGeometryFromThreeJSGeometry( groups[index], planeGeo, colours[index], opacity, visibility, 10001); primitives.push(primitive); index++; }); dim.addVectors(box.min, box.max).multiplyScalar( 0.5 ); primitives.forEach((primitive) => { primitive.setPosition(dim.x, dim.y, dim.z); }); return primitives; } } /* * Enable marker cluster to work with markers */ this.enableMarkerCluster = (flag) => { if (flag) { markerCluster.markerUpdateRequired = true; markerCluster.enable(); } else { markerCluster.markerUpdateRequired = false; markerCluster.disable(); } this.forcePickableObjectsUpdate = true; } }