const THREE = require('three'); const resolveURL = require('./utilities').resolveURL; /** * Object with containg viewport information used in ZincJS. * * @class * @author Alan Wu * @return {Viewport} */ const Viewport = function () { /** @property {Number} */ this.nearPlane = 0.168248; /** @property {Number} */ this.farPlane = 6.82906; /**@property {Array} */ this.eyePosition = [0.5, -2.86496, 0.5]; /** @property {Array} */ this.targetPosition = [0.5, 0.5, 0.5]; /** @property {Array} */ this.upVector = [ 0.0, 0.0, 1.0]; const _this = this; this.setFromObject = ({ nearPlane, farPlane, eyePosition, targetPosition, upVector }) => { _this.nearPlane = nearPlane; _this.farPlane = farPlane; _this.eyePosition = eyePosition; _this.targetPosition = targetPosition; _this.upVector = upVector; } }; /** * Provides the basic controls for a scene. * * @class * @author Alan Wu * @return {CameraControls} */ const CameraControls = function ( object, domElement, renderer, scene ) { const MODE = { NONE: -1, DEFAULT: 0, PATH: 1, SMOOTH_CAMERA_TRANSITION: 2, AUTO_TUMBLE: 3, ROTATE_TRANSITION: 4, MINIMAP: 5, SYNC_CONTROL: 6 }; /** * Actions states. * Available states are NONE, ROTATE, ZOOM, PAN, TOUCH_ROTATE, TOUCH_ZOOM, TOUCH_PAN and SCROLL. * @property {Object} */ const STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM: 4, TOUCH_PAN: 5, SCROLL: 6 }; const ROTATE_DIRECTION = { NONE: -1, FREE: 1, HORIZONTAL: 2, VERTICAL: 3 }; /** * Available click actions are MAIN, AUXILIARY and SECONARY. * @property {Object} */ const CLICK_ACTION = {}; CLICK_ACTION.MAIN = STATE.ROTATE; CLICK_ACTION.AUXILIARY = STATE.ZOOM; CLICK_ACTION.SECONDARY = STATE.PAN; this.cameraObject = object; this.domElement = ( domElement !== undefined ) ? domElement : document; this.renderer = renderer; this.scene = scene ; this.tumble_rate = 1.5; this.pointer_x = 0; this.pointer_y = 0; this.pointer_x_start = 0; this.pointer_y_start = 0; this.previous_pointer_x = 0; this.previous_pointer_y = 0; this.near_plane_fly_debt = 0.0; this.touchZoomDistanceStart = 0; this.touchZoomDistanceEnd = 0; this.directionalLight = 0; this.scrollRate = 50; this.pixelHeight = 1; let duration = 6000; let enabled = true; let inbuildTime = 0; let cameraPath = undefined; let numberOfCameraPoint = undefined; let updateLightWithPathFlag = false; let playRate = 500; let deviceOrientationControl = undefined; let defaultViewport = "default"; let currentMode = MODE.DEFAULT; let smoothCameraTransitionObject = undefined; let rotateCameraTransitionObject = undefined; let cameraAutoTumbleObject = undefined; let mouseScroll = 0; let rotateMode = ROTATE_DIRECTION.FREE; this._state = STATE.NONE; let zincRayCaster = undefined; this.targetTouchId = -1; let rect = undefined; const _a = new THREE.Vector3(); const _b = new THREE.Vector3(); const _c = new THREE.Vector3(); const _new_b = new THREE.Vector3(); const _new_c = new THREE.Vector3(); const _axis = new THREE.Vector3(); const _v = new THREE.Vector3(); const _rel_eye = new THREE.Vector3(); const sceneSphere = new THREE.Sphere(); const _tempEye = new THREE.Vector3(); let hasUpdated = false; let ndcControl = undefined; let maxDist = 0; const viewports = { "default" : new Viewport() }; viewports.default.nearPlane = 0.1; viewports.default.farPlane = 2000; viewports.default.eyePosition = [0, 0, 0]; viewports.default.targetPosition = [0, 0, -1.0]; viewports.default.upVector = [ 0.0, 1.0, 0.0]; //Add the target property if (this.cameraObject.target === undefined) this.cameraObject.target = new THREE.Vector3( ...viewports.default.targetPosition ); //Calculate the max distanc allowed, it is the longer //of 6 times the radius of the current scene and //the current distance between scene centroid and the postion //of the camera. this.calculateMaxAllowedDistance = (scene) => { const box = scene.getBoundingBox(); if (box) { box.getBoundingSphere(sceneSphere); maxDist = sceneSphere.radius * 6; let currentDist = 0; if (this.cameraObject) { currentDist = this.cameraObject.position.distanceTo(sceneSphere.center); } maxDist = currentDist > maxDist ? currentDist : maxDist; } else { maxDist = 0; } } /** * Add a viewport to the list of available named viewports. * * @param {String} name - Name of the viewport * @param {Viewport} viewportName - Viewport to be added */ this.addViewport = (viewportName, viewport) => { if (viewportName && viewport) viewports[viewportName] = viewport; } /** * Set the default viewport for this {@link CameraControls}. * * @param {String} defaultName - Name of the viewport * * @return {Boolean} true if set successfully, false otherwise. */ this.setDefaultViewport = defaultName => { if (defaultName && (defaultName in viewports)) { defaultViewport = defaultName; return true; } return false } /** * Get the name of the default viewport. * * * @return {String} */ this.getDefaultViewport = () => { return defaultViewport; } /** * Get the viewport with the provied name stored in this object. * @param {String} name - Name of the viewport * * @return {Viewport} */ this.getViewportOfName = name => { return viewports[name]; } /** * Set the viewport with a name if it is found in the list. * @param {String} name - Name of the viewport * * @return {Boolean} if viewport is found and set, otherwise false. */ this.setCurrentViewport = name => { if (name in viewports) { this.setCurrentCameraSettings(viewports[name]) return true; } return false; } /** * Set the direction of rotation allowed with this control. * * @param {String} mode - available options are none, horizontal, * vertical and free. */ this.setRotationMode = mode => { switch (mode) { case "none": rotateMode = ROTATE_DIRECTION.NONE; break; case "horizontal": rotateMode = ROTATE_DIRECTION.HORIZONTAL; break; case "vertical": rotateMode = ROTATE_DIRECTION.VERTICAL; break; case "free": default: rotateMode = ROTATE_DIRECTION.FREE; } } this.onResize = () => { if (rect) rect = undefined; if (ndcControl) ndcControl.setCurrentCameraSettings(this.cameraObject, viewports[defaultViewport]); } this.getVisibleHeightAtZDepth = ( depth ) => { // compensate for cameras not positioned at z=0 const cameraOffset = this.cameraObject.position.distanceTo(this.cameraObject.target); if ( depth < cameraOffset ) depth -= cameraOffset; else depth += cameraOffset; // vertical fov in radians const vFOV = this.cameraObject.fov * Math.PI / 180; // Math.abs to ensure the result is always positive return 2 * Math.tan( vFOV / 2 ) * Math.abs( depth ); }; this.calculateHeightPerPixelAtZeroDepth = ( wHeight ) => { const height = this.getVisibleHeightAtZDepth(0); this.pixelHeight = height / wHeight; return this.pixelHeight; } /** * Get normalised coordinates from windows coordinates. * * @param {String} x * @param {String} y * @param {THREE.Vector2} positionIn - Optional, write the value into * this object if it is provided, otherwise a new object will * be created and returned. * * @return {THREE.Vector2} containing the normalised x and y coordinates. */ this.getNDCFromDocumentCoords = (x, y, positionIn) => { updateRect(false); const position = positionIn ? positionIn : new THREE.Vector2(); const out_x = ((x - rect.left) / rect.width) * 2 - 1; const out_y = -((y - rect.top) / rect.height) * 2 + 1; return position.set(out_x, out_y); } /** * Get the relative windows coordinates from normalised coordiantes. * * @param {String} x * @param {String} y * @param {THREE.Vector2} positionIn - Optional, write the value into * this object if it is provided, otherwise a new object will * be created and returned. * * @return {THREE.Vector2} containing the relative x and y coordinates. */ this.getRelativeCoordsFromNDC = (x, y, positionIn) => { updateRect(false); const position = positionIn ? positionIn : new THREE.Vector2(); position.x = (x + 1) * rect.width / 2.0; position.y = (1 - y) * rect.height / 2.0; return position; } /** * Map a mouse click to the specified action. * * @param {String} buttonName - please see {@link CLICK_ACTION} * @param {String} actionName - please see {@link STATE} */ this.setMouseButtonAction = (buttonName, actionName) => { CLICK_ACTION[buttonName] = STATE[actionName]; } //Make sure the camera does not travel beyond limit const checkTravelDistance = () => { if (maxDist > 0) { const newDist = _tempEye.distanceTo(sceneSphere.center); return (maxDist > newDist || this.cameraObject.position.distanceTo(sceneSphere.center) > newDist ); } return true; } const translateViewport = translation => { _tempEye.copy(this.cameraObject.position).add(translation); if (checkTravelDistance()) { this.cameraObject.target.add(translation); this.cameraObject.position.add(translation); this.updateDirectionalLight(); } } const onDocumentMouseDown = event => { updateRect(false); // Check if mouse event hapens inside the minimap let minimapCoordinates = undefined; if (currentMode === MODE.DEFAULT) minimapCoordinates = this.scene.getNormalisedMinimapCoordinates( this.renderer, event); if (!minimapCoordinates) { if (event.button == 0) { if (event.ctrlKey) this._state = CLICK_ACTION.AUXILIARY; else if (event.shiftKey) this._state = CLICK_ACTION.SECONDARY; else this._state = CLICK_ACTION.MAIN; } else if (event.button == 1) { event.preventDefault(); this._state = CLICK_ACTION.AUXILIARY; } else if (event.button == 2) { this._state = CLICK_ACTION.SECONDARY; } this.pointer_x = event.clientX - rect.left; this.pointer_y = event.clientY - rect.top; this.pointer_x_start = this.pointer_x; this.pointer_y_start = this.pointer_y; this.previous_pointer_x = this.pointer_x; this.previous_pointer_y= this.pointer_y; } else { currentMode = MODE.MINIMAP; let translation = this.scene.getMinimapDiffFromNormalised( minimapCoordinates.x, minimapCoordinates.y); translateViewport(translation); } } const onDocumentMouseMove = event => { updateRect(false); if (rect) { this.pointer_x = event.clientX - rect.left; this.pointer_y = event.clientY - rect.top; if (currentMode === MODE.MINIMAP) { let minimapCoordinates = this.scene.getNormalisedMinimapCoordinates(this.renderer, event); if (minimapCoordinates) { let translation = this.scene.getMinimapDiffFromNormalised( minimapCoordinates.x, minimapCoordinates.y); translateViewport(translation); } } else { if ((this._state === STATE.NONE) && (zincRayCaster !== undefined)) { zincRayCaster.move(this, event.clientX, event.clientY, this.renderer); } } } } const onDocumentMouseUp = event => { this._state = STATE.NONE; if (currentMode == MODE.MINIMAP) currentMode = MODE.DEFAULT; if (zincRayCaster !== undefined) { if (this.pointer_x_start==(event.clientX - rect.left) && this.pointer_y_start==(event.clientY- rect.top)) { zincRayCaster.pick(this, event.clientX, event.clientY, this.renderer); } } } const onDocumentMouseLeave = event => { this._state = STATE.NONE; } const onDocumentTouchStart = event => { updateRect(false); const len = event.touches.length; if (len == 1) { this._state = STATE.TOUCH_ROTATE; this.pointer_x = event.touches[0].clientX - rect.left; this.pointer_y = event.touches[0].clientY - rect.top; this.pointer_x_start = this.pointer_x; this.pointer_y_start = this.pointer_y; this.previous_pointer_x = this.pointer_x; this.previous_pointer_y= this.pointer_y; } else if (len == 2) { this._state = STATE.TOUCH_ZOOM; const dx = event.touches[ 0 ].clientX - event.touches[ 1 ].clientX; const dy = event.touches[ 0 ].clientY - event.touches[ 1 ].clientY; this.touchZoomDistanceEnd = this.touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); } else if (len == 3) { this._state = STATE.TOUCH_PAN; this.targetTouchId = event.touches[0].identifier; this.pointer_x = event.touches[0].clientX - rect.left; this.pointer_y = event.touches[0].clientY - rect.top; this.previous_pointer_x = this.pointer_x; this.previous_pointer_y= this.pointer_y; } } const onDocumentTouchMove = event => { event.preventDefault(); event.stopPropagation(); const len = event.touches.length; if (len == 1) { this.pointer_x = event.touches[0].clientX - rect.left; this.pointer_y = event.touches[0].clientY - rect.top; } else if (len == 2) { if (this._state === STATE.TOUCH_ZOOM) { const dx = event.touches[ 0 ].clientX - event.touches[ 1 ].clientX; const dy = event.touches[ 0 ].clientY - event.touches[ 1 ].clientY; this.touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); } } else if (len == 3) { if (this._state === STATE.TOUCH_PAN) { for (let i = 0; i < 3; i++) { if (event.touches[i].identifier == this.targetTouchId) { this.pointer_x = event.touches[0].clientX - rect.left; this.pointer_y = event.touches[0].clientY - rect.top; } } } } } const onDocumentTouchEnd = event => { const len = event.touches.length; this.touchZoomDistanceStart = this.touchZoomDistanceEnd = 0; this.targetTouchId = -1; this._state = STATE.NONE; if (len == 1) { if (zincRayCaster !== undefined) { if (this.pointer_x_start==(event.touches[0].clientX- rect.left) && this.pointer_y_start==(event.touches[0].clientY- rect.top)) { zincRayCaster.pick(this.cameraObject, event.touches[0].clientX, event.touches[0].clientY, this.renderer); } } } } const onDocumentEnter = () => { updateRect(true); } const updateRect = forced => { //Use intersectionObserver to reset the rect for ray tracing. if (forced || rect === undefined) { const observer = new IntersectionObserver((entries) => { for (const entry of entries) { rect = entry.boundingClientRect; } observer.disconnect(); }); observer.observe(this.domElement); } } const onDocumentWheelEvent = event => { updateRect(false); this._state = STATE.SCROLL; let changes = 0; if (event.deltaY > 0) changes = this.scrollRate; else if (event.deltaY < 0) changes = this.scrollRate * -1; mouseScroll = mouseScroll + changes; event.preventDefault(); event.stopImmediatePropagation(); } const translate = () => { if (typeof this.cameraObject !== "undefined") { const height = rect.height; const distance = this.cameraObject.position.distanceTo(this.cameraObject.target); let fact = 0.0; if ((this.cameraObject.far > this.cameraObject.near) && (distance >= this.cameraObject.near) && (distance <= this.cameraObject.far)) { fact = (distance-this.cameraObject.near)/(this.cameraObject.far-this.cameraObject.near); } //_b == old_near, _c = old_far, _new_b = new_near, _new_c = new_far _b.set(this.previous_pointer_x,height - this.previous_pointer_y,0.0); _c.set(this.previous_pointer_x, height - this.previous_pointer_y,1.0); _new_b.set(this.pointer_x,height - this.pointer_y,0.0); _new_c.set(this.pointer_x,height - this.pointer_y,1.0); _b.unproject(this.cameraObject); _c.unproject(this.cameraObject); _new_b.unproject(this.cameraObject); _new_c.unproject( this.cameraObject); const translate_rate = -0.002; _new_b.sub(_b).multiplyScalar(1.0-fact); _new_c.sub(_c).multiplyScalar(fact); _new_b.add(_new_c).multiplyScalar(translate_rate); translateViewport(_new_b); } this.previous_pointer_x = this.pointer_x; this.previous_pointer_y = this.pointer_y; } this.getVectorsFromRotateAboutLookAtPoints = (axis, angle) => { axis.normalize(); _v.copy(this.cameraObject.position).sub(this.cameraObject.target); _rel_eye.copy(_v); _v.normalize() if (0.8 < Math.abs(_v.dot(axis))) { _v.copy(this.cameraObject.up); } _b.crossVectors(axis, _v).normalize(); _c.crossVectors(axis, _b); const rel_eyea = axis.dot(_rel_eye); const rel_eyeb = _b.dot(_rel_eye); const rel_eyec = _c.dot(_rel_eye); const upa = axis.dot(this.cameraObject.up); const upb = _b.dot(this.cameraObject.up); const upc = _c.dot(this.cameraObject.up); const cos_angle = Math.cos(angle); const sin_angle = Math.sin(angle); _new_b.set(cos_angle*_b.x+sin_angle*_c.x, cos_angle*_b.y+sin_angle*_c.y, cos_angle*_b.z+sin_angle*_c.z); _new_c.set(cos_angle*_c.x-sin_angle*_b.x, cos_angle*_c.y-sin_angle*_b.y, cos_angle*_c.z-sin_angle*_b.z); _v.copy(this.cameraObject.target); _v.x = _v.x + axis.x*rel_eyea + _new_b.x*rel_eyeb+_new_c.x*rel_eyec; _v.y = _v.y + axis.y*rel_eyea + _new_b.y*rel_eyeb+_new_c.y*rel_eyec; _v.z = _v.z + axis.z*rel_eyea + _new_b.z*rel_eyeb+_new_c.z*rel_eyec; _a.set(axis.x*upa+_new_b.x*upb+_new_c.x*upc, axis.y*upa+_new_b.y*upb+_new_c.y*upc, axis.z*upa+_new_b.z*upb+_new_c.z*upc); return {position: _v, up: _a}; } /** * Rotate around the axis with the amount specified by angle. * * @param {THREE.Vector3} axis - The rotational axis. * @param {Number} Angle - Specify how much the camera shoudl rotate by. */ this.rotateAboutLookAtpoint = (axis, angle) => { const returned_values = this.getVectorsFromRotateAboutLookAtPoints(axis, angle); this.cameraObject.position.copy(returned_values.position); this.updateDirectionalLight(); this.cameraObject.up.copy(returned_values.up); } const tumble = () => { if (typeof this.cameraObject !== "undefined") { const width = rect.width; const height = rect.height; if ((0 0) { const dx=-delta_y*1.0/tangent_dist; const dy=delta_x*1.0/tangent_dist; let d = 0; // Do not allow rotation on other direction around the origin if rotateMode is not free if (rotateMode === ROTATE_DIRECTION.FREE) { let d=dx*(this.pointer_x-0.5*(width-1))+dy*(0.5*(height-1)-this.pointer_y); if (d > radius) { d = radius; } else { if (d < -radius) { d = -radius; } } } const phi=Math.acos(d/radius)-0.5*Math.PI; const angle=this.tumble_rate*tangent_dist/radius; _a.copy(this.cameraObject.position).sub(this.cameraObject.target).normalize(); _b.copy(this.cameraObject.up).normalize(); _c.copy(_b).cross(_a).normalize().multiplyScalar(dx); _b.multiplyScalar(dy); _axis.addVectors(_c, _b).multiplyScalar(Math.cos(phi)); _a.multiplyScalar(Math.sin(phi)); _axis.add(_a); this.rotateAboutLookAtpoint(_axis, -angle); } } } this.previous_pointer_x = this.pointer_x; this.previous_pointer_y = this.pointer_y; } const calculateZoomDelta = () => { let delta = 0; if (this._state === STATE.ZOOM) { delta = this.previous_pointer_y-this.pointer_y; } else if (this._state === STATE.SCROLL) { delta = mouseScroll; } else { delta = -1.0 * (this.touchZoomDistanceEnd - this.touchZoomDistanceStart); this.touchZoomDistanceStart = this.touchZoomDistanceEnd; } return delta; } this.changeZoomByScrollRateUnit = unit => { const delta_y = unit * this.scrollRate; this.changeZoomByValue(delta_y); } this.changeZoomByValue = delta_y => { if (typeof this.cameraObject !== "undefined") { const height = rect.height; const a = this.cameraObject.position.clone(); a.sub(this.cameraObject.target); const dist = a.length(); const dy = 1.5 * delta_y/height; if ((dist + dy*dist) > 0.01) { a.normalize() _tempEye.copy(this.cameraObject.position); _tempEye.x += a.x*dy*dist; _tempEye.y += a.y*dy*dist; _tempEye.z += a.z*dy*dist; if (checkTravelDistance()) { this.cameraObject.position.copy(_tempEye); this.updateDirectionalLight(); const near_far_minimum_ratio = 0.00001; if ((near_far_minimum_ratio * this.cameraObject.far) < (this.cameraObject.near + dy*dist + this.near_plane_fly_debt)) { if (this.near_plane_fly_debt != 0.0) { this.near_plane_fly_debt += dy*dist; if (this.near_plane_fly_debt > 0.0) { this.cameraObject.near += this.near_plane_fly_debt; this.cameraObject.far += this.near_plane_fly_debt; this.near_plane_fly_debt = 0.0; } else { this.cameraObject.near += dy*dist; this.cameraObject.far += dy*dist; } } } else { if (this.near_plane_fly_debt == 0.0) { const diff = this.cameraObject.near - near_far_minimum_ratio * this.cameraObject.far; this.cameraObject.near = near_far_minimum_ratio * this.cameraObject.far; this.cameraObject.far -= diff; this.near_plane_fly_debt -= near_far_minimum_ratio * this.cameraObject.far; } this.near_plane_fly_debt += dy*dist; } hasUpdated = true; } } } } const flyZoom = () => { const delta_y = calculateZoomDelta(); this.changeZoomByValue(delta_y); if (this._state === STATE.ZOOM) { this.previous_pointer_x = this.pointer_x; this.previous_pointer_y = this.pointer_y; } if (this._state === STATE.SCROLL) { mouseScroll = 0; this._state = STATE.NONE; } } this.setDirectionalLight = directionalLightIn => { this.directionalLight = directionalLightIn; }; /** * Force an update to the position of the directional light. */ this.updateDirectionalLight = () => { if (this.directionalLight != 0) { this.directionalLight.position.set(this.cameraObject.position.x, this.cameraObject.position.y, this.cameraObject.position.z); } } /** * Enable the camera control. */ this.enable = function () { enabled = true; if (this.domElement && this.domElement.addEventListener) { this.domElement.addEventListener( 'mousedown', onDocumentMouseDown, false ); this.domElement.addEventListener( 'mousemove', onDocumentMouseMove, false ); this.domElement.addEventListener( 'mouseup', onDocumentMouseUp, false ); this.domElement.addEventListener( 'mouseleave', onDocumentMouseLeave, false ); this.domElement.addEventListener( 'touchstart', onDocumentTouchStart, false); this.domElement.addEventListener( 'touchmove', onDocumentTouchMove, false); this.domElement.addEventListener( 'touchend', onDocumentTouchEnd, false); this.domElement.addEventListener( 'wheel', onDocumentWheelEvent, false); this.domElement.addEventListener( 'contextmenu', event => { event.preventDefault(); }, false ); this.domElement.addEventListener( 'mouseenter', onDocumentEnter, false ); } } /** * Disable the camera control. */ this.disable = function () { enabled = false; if (this.domElement && this.domElement.removeEventListener) { this.domElement.removeEventListener( 'mousedown', onDocumentMouseDown, false ); this.domElement.removeEventListener( 'mousemove', onDocumentMouseMove, false ); this.domElement.removeEventListener( 'mouseup', onDocumentMouseUp, false ); this.domElement.removeEventListener( 'mouseleave', onDocumentMouseLeave, false ); this.domElement.removeEventListener( 'touchstart', onDocumentTouchStart, false); this.domElement.removeEventListener( 'touchmove', onDocumentTouchMove, false); this.domElement.removeEventListener( 'touchend', onDocumentTouchEnd, false); this.domElement.removeEventListener( 'wheel', onDocumentWheelEvent, false); this.domElement.removeEventListener( 'mouseenter', onDocumentEnter, false ); this.domElement.removeEventListener( 'contextmenu', event => { event.preventDefault(); }, false ); } } this.loadPath = pathData => { cameraPath = pathData.CameraPath; numberOfCameraPoint = pathData.NumberOfPoints; } /** * This is an experimental feature. It loads a path - point to point which * the camera will travel. * * @param {String} path_url - The path. * @param {requestCallback} finishCallback - The callback once the path is load. */ this.loadPathURL = (path_url, finishCallback) => { const xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = () => { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { const pathData = JSON.parse(xmlhttp.responseText); this.loadPath(pathData); if (finishCallback != undefined && (typeof finishCallback == 'function')) finishCallback(); } } const requestURL = resolveURL(path_url); xmlhttp.open("GET", requestURL, true); xmlhttp.send(); } /** * Set the duration for the camera to travel along the path. * * @param {Number} durationIn - the duration for the path. */ this.setPathDuration = durationIn => { duration = durationIn; if (smoothCameraTransitionObject) smoothCameraTransitionObject.setDuration(duration); if (rotateCameraTransitionObject) rotateCameraTransitionObject.setDuration(duration); } /** * Get the playRate - this determines how fast it takes to * finish one duration. * * @return {Number} */ this.getPlayRate = () => { return playRate; } /** * Set the playRate - this determines how fast it takes to * finish one duration. * * @param {Number} playRateIn - The play rate speed. */ this.setPlayRate = playRateIn => { playRate = playRateIn; } /** * Update the internal timer by the set amount, this can * be used to force a time update by setting delta to zero. * * @param {Number} delta - The amount of time to increment * the time by. */ const updateTime = delta => { let targetTime = inbuildTime + delta; if (targetTime > duration) targetTime = targetTime - duration inbuildTime = targetTime; }; /** * Get the current inbuild time, * * @return {Number} */ this.getTime = () => { return inbuildTime; } /** * Set the current inbuild time, * * @param {Number} timeIn - this will be used as the current time, * it should be between the range of zero and the set duration. */ this.setTime = timeIn => { if (timeIn > duration) inbuildTime = duration; else if (timeIn < 0.0) inbuildTime = 0.0; else inbuildTime = timeIn; } /** * Get the number of frame which is determine by number of points * in the camera path. * * @return {Number} */ this.getNumberOfTimeFrame = () => { return numberOfCameraPoint; } /** * Get the current time frame and it will return three values in * an array. * * @return {Array} - bottom frame, top frame and the proportion. */ this.getCurrentTimeFrame = () => { if (numberOfCameraPoint > 2) { const current_time = inbuildTime/duration * (numberOfCameraPoint - 1); const bottom_frame = Math.floor(current_time); const proportion = 1 - (current_time - bottom_frame); const top_frame = Math.ceil(current_time); if (bottom_frame == top_frame) { if (bottom_frame == numberOfCameraPoint - 1) { return [bottom_frame - 1, top_frame, 0]; } else { return [bottom_frame, top_frame + 1, 1.0]; } } return [bottom_frame, top_frame, proportion]; } else if (numberOfCameraPoint == 1) { return [0, 0, 0]; } return undefined; } /** * Set the current time frame. * * @param {Number} targetTimeFrame - bottom frame, top frame and the proportion. */ this.setCurrentTimeFrame = targetTimeFrame => { if (numberOfCameraPoint > 2) { inbuildTime = duration * targetTimeFrame / (numberOfCameraPoint - 1); if (inbuildTime < 0.0) inbuildTime = 0.0; if (inbuildTime > duration) inbuildTime = duration; } } /** * Update the progress on the path by the specified amount - delta. * * @param {Number} delta - The amount of time to increment */ const updatePath = delta => { if (currentMode === MODE.PATH) { updateTime(delta); if (cameraPath) { const time_frame = this.getCurrentTimeFrame(); const bottom_frame = time_frame[0]; const top_frame = time_frame[1]; const proportion = time_frame[2]; const bot_pos = [cameraPath[bottom_frame*3], cameraPath[bottom_frame*3+1], cameraPath[bottom_frame*3+2]]; const top_pos = [cameraPath[top_frame*3], cameraPath[top_frame*3+1], cameraPath[top_frame*3+2]]; const current_positions = []; for (let i = 0; i < bot_pos.length; i++) { current_positions.push(proportion * bot_pos[i] + (1.0 - proportion) * top_pos[i]); } this.cameraObject.position.set(current_positions[0], current_positions[1], current_positions[2]); this.cameraObject.target.set(top_pos[0], top_pos[1], top_pos[2]); if (deviceOrientationControl) this.cameraObject.lookAt( this.cameraObject.target ); if (updateLightWithPathFlag) { this.directionalLight.position.set(current_positions[0], current_positions[1], current_positions[2]); this.directionalLight.target.position.set(top_pos[0], top_pos[1], top_pos[2]); } } } }; /** * Force recalculation of the current path. */ this.calculatePathNow = () => { updatePath(0.0); } // handle synchronised control based on information in the idc const handleSyncControl = () => { if ((this._state === STATE.ROTATE) || (this._state === STATE.TOUCH_ROTATE)){ //rotateion does not trigger callback tumble(); } else if ((this._state === STATE.PAN) || (this._state === STATE.TOUCH_PAN)){ translate(); ndcControl.triggerCallback(); } else if ((this._state === STATE.ZOOM) || (this._state === STATE.TOUCH_ZOOM) || (this._state === STATE.SCROLL)){ ndcControl.zoom(calculateZoomDelta()); this.previous_pointer_x = this.pointer_x; this.previous_pointer_y = this.pointer_y; if (this._state === STATE.SCROLL) { this._state = STATE.NONE; } mouseScroll = 0; ndcControl.triggerCallback(); } } /** * Update all controls related changes - including calculation of the viewport. * * @param {Number} timeChanged - Time eclipse since last called. */ this.update = timeChanged => { const delta = timeChanged * playRate; let controlEnabled = enabled; let updated = true; if (currentMode === MODE.PATH) { updatePath(delta); } else if (currentMode === MODE.SMOOTH_CAMERA_TRANSITION && smoothCameraTransitionObject) { smoothCameraTransitionObject.update(delta); if (smoothCameraTransitionObject.isTransitionCompleted()) { smoothCameraTransitionObject == undefined; currentMode = MODE.DEFAULT; } controlEnabled = false; } else if (currentMode === MODE.ROTATE_CAMERA_TRANSITION && rotateCameraTransitionObject) { rotateCameraTransitionObject.update(delta); if (rotateCameraTransitionObject.isTransitionCompleted()) { rotateCameraTransitionObject == undefined; currentMode = MODE.DEFAULT; } controlEnabled = false; } else if (currentMode === MODE.AUTO_TUMBLE && cameraAutoTumbleObject) { cameraAutoTumbleObject.update(delta); } else if (currentMode === MODE.SYNC_CONTROL && ndcControl) { handleSyncControl(); controlEnabled = false; } else { updated = false; } if (controlEnabled) { if (this._state !== STATE.NONE) { updated = true; } if ((this._state === STATE.ROTATE) || (this._state === STATE.TOUCH_ROTATE)){ tumble(); } else if ((this._state === STATE.PAN) || (this._state === STATE.TOUCH_PAN)){ translate(); } else if ((this._state === STATE.ZOOM) || (this._state === STATE.TOUCH_ZOOM) || (this._state === STATE.SCROLL)){ flyZoom(); } if (this._state !== STATE.NONE) { if (currentMode === MODE.AUTO_TUMBLE && cameraAutoTumbleObject && cameraAutoTumbleObject.stopOnCameraInput) { } } if (this._state === STATE.SCROLL) this._state = STATE.NONE; } if (deviceOrientationControl) { updated = true; deviceOrientationControl.update(); //this.directionalLight.target.position.set(this.cameraObject.target.x, // this.cameraObject.target.y, this.cameraObject.target.z); } else { this.cameraObject.lookAt( this.cameraObject.target ); } updated = updated || hasUpdated; hasUpdated = false; return updated; }; /** * Switch to path mode and begin traveling through the camera path. */ this.playPath = () => { currentMode = MODE.PATH; } /** * Stop playing path and switch back to normal control. */ this.stopPath = () => { currentMode = MODE.DEFAULT; } /** * Check rather the control is currently in path mode. * * @return {Boolean} */ this.isPlayingPath = () => { return (currentMode === MODE.PATH); } /** * Enable directional light update as the camera * is traveling through path. * * @param {Boolean} flag */ this.enableDirectionalLightUpdateWithPath = flag => { updateLightWithPathFlag = flag; } /** * Enable rotation using the devices's accelerometer. */ this.enableDeviceOrientation = () => { if (!deviceOrientationControl) deviceOrientationControl = new ModifiedDeviceOrientationControls(this.cameraObject); } /** * Disable rotation using the devices's accelerometer. */ this.disableDeviceOrientation = () => { if (deviceOrientationControl) { deviceOrientationControl.dispose(); deviceOrientationControl = undefined; } } /** * Check rather device orientation based on accelerometer is on. */ this.isDeviceOrientationEnabled = () => { if (deviceOrientationControl) { return true; } return false; } /** * Reset the viewport settings to the one provided by default viewport. */ this.resetView = () => { const viewport = viewports[defaultViewport]; this.cameraObject.near = viewport.nearPlane; this.cameraObject.far = viewport.farPlane; this.cameraObject.position.set( viewport.eyePosition[0], viewport.eyePosition[1], viewport.eyePosition[2]); this.cameraObject.target.set( viewport.targetPosition[0], viewport.targetPosition[1], viewport.targetPosition[2] ); this.cameraObject.up.set( viewport.upVector[0], viewport.upVector[1], viewport.upVector[2]); this.cameraObject.updateProjectionMatrix(); this.updateDirectionalLight(); } /** * Set the current camera settings with the provided viewport. * * @param {Viewport} newViewport - viewport settings. */ this.setCurrentCameraSettings = newViewport => { if (newViewport.nearPlane) this.cameraObject.near = newViewport.nearPlane; if (newViewport.farPlane) this.cameraObject.far = newViewport.farPlane; if (newViewport.eyePosition) this.cameraObject.position.set( newViewport.eyePosition[0], newViewport.eyePosition[1], newViewport.eyePosition[2]); if (newViewport.targetPosition) this.cameraObject.target.set( newViewport.targetPosition[0], newViewport.targetPosition[1], newViewport.targetPosition[2] ); if (newViewport.upVector) this.cameraObject.up.set( newViewport.upVector[0], newViewport.upVector[1], newViewport.upVector[2]); this.cameraObject.updateProjectionMatrix(); this.updateDirectionalLight(); hasUpdated = true; } /** * Get the viewport based on centre, radius, view_angle and clip distance. * * @param {Number} centreX - x coordinate of the centre. * @param {Number} centreY - y coordinate of the centre. * @param {Number} centreZ - z coordinate of the centre. * @param {Number} radius - radius if the viewport. * @param {Number} view_angle - view angle. * @param {Number} clip_distance - clip_distance between the near and far plane. * * @return {Viewport} */ this.getViewportFromCentreAndRadius = (centreX, centreY, centreZ, radius, view_angle, clip_distance) => { let eyex = this.cameraObject.position.x-this.cameraObject.target.x; let eyey = this.cameraObject.position.y-this.cameraObject.target.y; let eyez = this.cameraObject.position.z-this.cameraObject.target.z; const fact = 1.0/Math.sqrt(eyex*eyex+eyey*eyey+eyez*eyez); eyex = eyex * fact; eyey = eyey * fact; eyez = eyez * fact; /* look at the centre of the sphere */ const localTargetPosition = [centreX, centreY, centreZ]; /* shift the eye position to achieve the desired view_angle */ const eye_distance = radius/Math.tan(view_angle*Math.PI/360.0); const localEyePosition = [ centreX + eyex*eye_distance, centreY + eyey*eye_distance, centreZ + eyez*eye_distance]; const localFarPlane = eye_distance+clip_distance; let localNearPlane = 0.0; const nearClippingFactor = 0.95; if (clip_distance > nearClippingFactor*eye_distance) { localNearPlane = (1.0 - nearClippingFactor)*eye_distance; } else { localNearPlane = eye_distance - clip_distance; } const newViewport = new Viewport(); newViewport.nearPlane = localNearPlane; newViewport.farPlane = localFarPlane; newViewport.eyePosition = localEyePosition; newViewport.targetPosition = localTargetPosition; newViewport.upVector = [this.cameraObject.up.x, this.cameraObject.up.y, this.cameraObject.up.z]; return newViewport; } /** * Get the viewport for the boudning box * * @param {Number} boundingBox - y coordinate of the centre. * @return {Viewport} */ this.getViewportFromBoundingBox = (boundingBox, radiusScale) => { const radius = boundingBox.min.distanceTo(boundingBox.max) / 2.0 * radiusScale; const centreX = (boundingBox.min.x + boundingBox.max.x) / 2.0; const centreY = (boundingBox.min.y + boundingBox.max.y) / 2.0; const centreZ = (boundingBox.min.z + boundingBox.max.z) / 2.0; const clip_factor = 4.0; const viewport = this.getViewportFromCentreAndRadius( centreX, centreY, centreZ, radius, 40, radius * clip_factor); return viewport; } /** * Get the current camera viewport. * * @return {Viewport} */ this.getCurrentViewport = () => { const currentViewport = new Viewport(); currentViewport.nearPlane = this.cameraObject.near; currentViewport.farPlane = this.cameraObject.far; currentViewport.eyePosition[0] = this.cameraObject.position.x; currentViewport.eyePosition[1] = this.cameraObject.position.y; currentViewport.eyePosition[2] = this.cameraObject.position.z; currentViewport.targetPosition[0] = this.cameraObject.target.x; currentViewport.targetPosition[1] = this.cameraObject.target.y; currentViewport.targetPosition[2] = this.cameraObject.target.z; currentViewport.upVector[0] = this.cameraObject.up.x; currentViewport.upVector[1] = this.cameraObject.up.y; currentViewport.upVector[2] = this.cameraObject.up.z; return currentViewport; } this.getDefaultEyePosition = () => { return eyePosition; } this.getDefaultTargetPosition = () => { return targetPosition; } /** * Setup a smooth transition object which transition the camera from one * viewport to the other in the specified duration. This will not work if * {@link rotateCameraTransition} is active. * To use this object, the transition must be enabled using * {@link enableCameraTransition}. * * @param {Viewport} startingViewport - the starting viewport * @param {Viewport} endingViewport - the viewport ti end the transistion with. * @param {Number} durationIn - duration of the smooth transition. */ this.cameraTransition = (startingViewport, endingViewport, durationIn) => { if (rotateCameraTransitionObject == undefined) smoothCameraTransitionObject = new SmoothCameraTransition(startingViewport, endingViewport, this, durationIn); } /** * Setup a rotate camera transition object which rotate the * camera by the specified the angle in the specified * duration. This will not work if {@link cameraTransition} * is active. * To use this object, the transition must be enabled using * {@link enableCameraTransition}. * * @param {THREE.Vector3} axis - the starting viewport * @param {Number} angle - the viewport ti end the transistion with. * @param {Number} duration - duration of the smooth transition. */ this.rotateCameraTransition = (axis, angle, duration) => { if (smoothCameraTransitionObject == undefined) rotateCameraTransitionObject = new RotateCameraTransition(axis, angle, this, duration); } /** * Enable camera transition, {@link rotateCameraTransition} amd * {@link cameraTransition} must be called before camera transition can * be enabled. */ this.enableCameraTransition = () => { if (smoothCameraTransitionObject) currentMode = MODE.SMOOTH_CAMERA_TRANSITION; if (rotateCameraTransitionObject) currentMode = MODE.ROTATE_CAMERA_TRANSITION; } /** * Pause the camera transition. */ this.pauseCameraTransition = () => { currentMode = MODE.DEFAULT; } /** * Stop the camera transition and remove camera transition * and rotate camera transition. */ this.stopCameraTransition = () => { currentMode = MODE.DEFAULT; smoothCameraTransitionObject = undefined; rotateCameraTransitionObject = undefined; } /** * Check if camera transition is active. */ this.isTransitioningCamera = () => { return (currentMode === MODE.SMOOTH_CAMERA_TRANSITION || currentMode === MODE.ROTATE_CAMERA_TRANSITION); } /** * Setup auto tumble object of the camera which will rotate the camera * around the target as if the user is rotating the camera by mouse/touch * interaction. * The tumbling will only be enabled with {@link enabelAutoTumble}. * * @param {Array} tumbleDirectionIn - direction of the mouse/touch. * @param {Number} tumbleRateIn - Speed of the tumbling. * @param {Boolean} stopOnCameraInputIn - Disable the tumbling once the user * start interacting with the scene. */ this.autoTumble = (tumbleDirectionIn, tumbleRateIn, stopOnCameraInputIn) => { cameraAutoTumbleObject = new CameraAutoTumble(tumbleDirectionIn, tumbleRateIn, stopOnCameraInputIn, this); } /** * Enable autotumble. */ this.enableAutoTumble = () => { currentMode = MODE.AUTO_TUMBLE; } /** * Disable the autotumble. */ this.stopAutoTumble = () => { currentMode = MODE.DEFAULT; cameraAutoTumbleObject = undefined; } /** * Update the autotumble object. */ this.updateAutoTumble = () => { if (cameraAutoTumbleObject) cameraAutoTumbleObject.requireUpdate = true; } /** * Check rather autotumble is active. * * @return {Boolean} */ this.isAutoTumble = () => { return (currentMode === MODE.AUTO_TUMBLE); } /** * Create an internal raycaster object and enable it for picking. * * @param {Scene} sceneIn - The scene to pick from, it can be different from the * camera's scene. * @param {requestCallback} callbackFunctionIn - The callback for pick event. * @param {requestCallback} hoverCallbackFunctionIn - The callback for hover * over event. */ this.enableRaycaster = (sceneIn, callbackFunctionIn, hoverCallbackFunctionIn) => { if (zincRayCaster == undefined) zincRayCaster = new RayCaster(sceneIn, this.scene, callbackFunctionIn, hoverCallbackFunctionIn, this.renderer); } /** * Disable raycaster and remove the internal ray caster object. */ this.disableRaycaster = () => { zincRayCaster.disable(); zincRayCaster = undefined; } /** * Check rather the camera is in syncControl mode. * * @return {Boolean} */ this.isSyncControl = () => { return currentMpde === MODE.SYNC_CONTROL; } /** * Enable syncControl. */ this.enableSyncControl = () => { currentMode = MODE.SYNC_CONTROL; if (!ndcControl) ndcControl = new NDCCameraControl(); ndcControl.setCurrentCameraSettings(this.cameraObject, viewports[defaultViewport]); return ndcControl; } /** * Disable syncControl. */ this.disableSyncControl = () => { currentMode = MODE.DEFAULT; this.cameraObject.zoom = 1; this.cameraObject.updateProjectionMatrix(); } this.enable(); }; const SmoothCameraTransition = function(startingViewport, endingViewport, targetCameraIn, durationIn) { const startingEyePosition = startingViewport.eyePosition; const startingTargetPosition = startingViewport.targetPosition; const startingUp = startingViewport.upVector; const endingEyePosition = endingViewport.eyePosition; const endingTargetPosition = endingViewport.targetPosition; const endingUp = endingViewport.upVector; const targetCamera = targetCameraIn; let duration = durationIn; let inbuildTime = 0; const enabled = true; const updateLightWithPathFlag = true; let completed = false; targetCamera.near = Math.min(startingViewport.nearPlane, endingViewport.nearPlane); targetCamera.far = Math.max(startingViewport.farPlane, endingViewport.farPlane); targetCamera.cameraObject.up.set( endingViewport.upVector[0], endingViewport.upVector[1], endingViewport.upVector[2]); this.setDuration = newDuration => { duration = newDuration; } const updateTime = delta => { let targetTime = inbuildTime + delta; if (targetTime > duration) targetTime = duration; inbuildTime = targetTime; }; const updateCameraSettings = () => { const ratio = inbuildTime / duration; const eyePosition = [startingEyePosition[0] * (1.0 - ratio) + endingEyePosition[0] * ratio, startingEyePosition[1] * (1.0 - ratio) + endingEyePosition[1] * ratio, startingEyePosition[2] * (1.0 - ratio) + endingEyePosition[2] * ratio]; const targetPosition = [startingTargetPosition[0] * (1.0 - ratio) + endingTargetPosition[0] * ratio, startingTargetPosition[1] * (1.0 - ratio) + endingTargetPosition[1] * ratio, startingTargetPosition[2] * (1.0 - ratio) + endingTargetPosition[2] * ratio]; const upVector = [startingUp[0] * (1.0 - ratio) + endingUp[0] * ratio, startingUp[1] * (1.0 - ratio) + endingUp[1] * ratio, startingUp[2] * (1.0 - ratio) + endingUp[2] * ratio]; targetCamera.cameraObject.position.set( eyePosition[0], eyePosition[1], eyePosition[2]); targetCamera.cameraObject.target.set( targetPosition[0], targetPosition[1], targetPosition[2] ); }; this.update = delta => { if ( this.enabled === false ) return; updateTime(delta); updateCameraSettings(); if (inbuildTime == duration) { completed = true; } } this.isTransitionCompleted = () => { return completed; } }; const RotateCameraTransition = function(axisIn, angleIn, targetCameraIn, durationIn) { const axis = axisIn; const angle = angleIn; const targetCamera = targetCameraIn; let duration = durationIn; let inbuildTime = 0; const enabled = true; const ratio = inbuildTime / duration; let completed = false; this.setDuration = newDuration => { duration = newDuration; } const updateCameraSettings = delta => { const previousTime = inbuildTime; let targetTime = inbuildTime + delta; if (targetTime > duration) targetTime = duration; inbuildTime = targetTime; const actualDelta = inbuildTime - previousTime; const ratio = actualDelta / duration; const alpha = ratio * angle; targetCamera.rotateAboutLookAtpoint(axis, alpha); }; this.update = delta => { if ( this.enabled === false ) return; updateCameraSettings(delta); if (inbuildTime == duration) { completed = true; } } this.isTransitionCompleted = () => { return completed; } } const RayCaster = function (sceneIn, hostSceneIn, callbackFunctionIn, hoverCallbackFunctionIn, rendererIn) { const scene = sceneIn; const hostScene = hostSceneIn; const renderer = rendererIn; const callbackFunction = callbackFunctionIn; const hoverCallbackFunction = hoverCallbackFunctionIn; const enabled = true; const raycaster = new THREE.Raycaster(); raycaster.params.Line.threshold = 0.1; raycaster.params.Points.threshold = 1; const mouse = new THREE.Vector2(); let awaiting = false; let lastHoveredDate = new Date(); let lastHoveredEmpty = false; let timeDiff = 0; let pickedObjects = new Array(); let lastPosition = { zincCamera: undefined, x: -1 ,y: -1}; let pickableObjects = undefined; this.enable = () => { enable = true; } this.disable = () => { enable = false; } this.getIntersectsObject = (zincCamera) => { if (hostScene !== scene) { const threejsScene = scene.getThreeJSScene(); renderer.render(threejsScene, zincCamera.cameraObject); } let objects = pickableObjects ? pickableObjects : scene.getPickableThreeJSObjects(); //Reset pickedObjects array pickedObjects.length = 0; return raycaster.intersectObjects( objects, true, pickedObjects ); } this.setPickableObjects = (zincObjects) => { if (zincObjects === undefined) { pickableObjects = undefined; } else { pickableObjects = []; zincObjects.forEach(zincObject => { if (zincObject.getGroup() && zincObject.getGroup().visible) { pickableObjects.push(zincObject.getGroup()); } }); } } this.getIntersectsObjectWithOrigin = (zincCamera, origin, direction) => { raycaster.set(origin, direction); return this.getIntersectsObject(zincCamera); } this.getIntersectsObjectWithCamera = (zincCamera, x, y) => { zincCamera.getNDCFromDocumentCoords(x, y, mouse); raycaster.setFromCamera(mouse, zincCamera.cameraObject); return this.getIntersectsObject(zincCamera); }; this.pick = (zincCamera, x, y) => { if (enabled && renderer && scene && zincCamera && callbackFunction) { this.getIntersectsObjectWithCamera(zincCamera, x, y); const length = pickedObjects.length; for (let i = 0; i < length; i++) { let zincObject = pickedObjects[i].object ? pickedObjects[i].object.userData : undefined; if (zincObject && zincObject.isMarkerCluster && zincObject.visible && zincObject.clusterIsVisible(pickedObjects[i].object.clusterIndex)) { //Can zoom into cluster if (zincObject.zoomToCluster(pickedObjects[i].object.clusterIndex)) { return; } } } callbackFunction(pickedObjects, x, y); } } let hovered = (zincCamera, x, y) => { if (enabled && renderer && scene && zincCamera && hoverCallbackFunction) { this.getIntersectsObjectWithCamera(zincCamera, x, y); lastHoveredDate.setTime(Date.now()); if (pickedObjects.length === 0) { //skip hovered callback if the previous one is empty if (lastHoveredEmpty) return lastHoveredEmpty = true; } else { lastHoveredEmpty = false; } hoverCallbackFunction(pickedObjects, x, y); } } this.move = (zincCamera, x, y) => { if (enabled && renderer && scene && zincCamera && hoverCallbackFunction) { if (scene.displayMarkers) { hovered(zincCamera, x, y); } else { lastPosition.zincCamera = zincCamera; lastPosition.x = x; lastPosition.y = y; if (!awaiting) { timeDiff = lastHoveredDate ? Date.now() - lastHoveredDate.getTime() : 250; if (timeDiff >= 250) { hovered(zincCamera, x, y); } else { awaiting = true; setTimeout(awaitMove(lastPosition), timeDiff); } } } } } let awaitMove = (lastPosition) => { return function() { awaiting = false; hovered(lastPosition.zincCamera, lastPosition.x, lastPosition.y); } } }; const CameraAutoTumble = function (tumbleDirectionIn, tumbleRateIn, stopOnCameraInputIn, targetCameraIn) { const tumbleAxis = new THREE.Vector3(); const angle = -tumbleRateIn; const targetCamera = targetCameraIn; const enabled = true; const updateLightWithPathFlag = true; const tumbleDirection = tumbleDirectionIn; this.stopOnCameraInput = stopOnCameraInputIn; this.requireUpdate = true; const b = new THREE.Vector3(); const c = new THREE.Vector3(); const computeTumbleAxisAngle = tumbleDirection => { const tangent_dist = Math.sqrt(tumbleDirection[0]*tumbleDirection[0] + tumbleDirection[1]*tumbleDirection[1]); const width = Math.abs(tumbleDirection[0]) * 4.0; const height = Math.abs(tumbleDirection[1]) * 4.0; const radius = 0.25 * (width + height); const dx = -tumbleDirection[1]/tangent_dist; const dy = tumbleDirection[0]/tangent_dist; let d = dx*(tumbleDirection[0])+dy*(-tumbleDirection[1]); if (d > radius) { d = radius; } else { if (d < -radius) { d = -radius; } } const phi=Math.acos(d/radius)-0.5*Math.PI; /* get axis to rotate about */ tumbleAxis.copy(targetCamera.cameraObject.position).sub( targetCamera.cameraObject.target).normalize(); b.copy(targetCamera.cameraObject.up).normalize(); c.crossVectors(b, tumbleAxis).normalize().multiplyScalar(dx); b.multiplyScalar(dy); b.add(c).multiplyScalar(Math.cos(phi)); tumbleAxis.multiplyScalar(Math.sin(phi)).add(b); }; this.update = delta => { if ( this.enabled === false ) return; if (this.requireUpdate) { computeTumbleAxisAngle(tumbleDirection); this.requireUpdate = false; } targetCamera.rotateAboutLookAtpoint(tumbleAxis, angle * delta/1000); } }; /** * @author mrdoob / http://mrdoob.com/ */ const StereoCameraZoomFixed = function () { this.type = 'StereoCamera'; this.aspect = 1; this.cameraL = new THREE.PerspectiveCamera(); this.cameraL.layers.enable( 1 ); this.cameraL.matrixAutoUpdate = false; this.cameraR = new THREE.PerspectiveCamera(); this.cameraR.layers.enable( 2 ); this.cameraR.matrixAutoUpdate = false; }; Object.assign( StereoCameraZoomFixed.prototype, { update: (() => { let focus, fov, aspect, near, far, zoom; const eyeRight = new THREE.Matrix4(); const eyeLeft = new THREE.Matrix4(); return function update( camera ) { const needsUpdate = focus !== camera.focus || fov !== camera.fov || aspect !== camera.aspect * this.aspect || near !== camera.near || far !== camera.far || zoom !== camera.zoom; if ( needsUpdate ) { focus = camera.focus; fov = camera.fov; aspect = camera.aspect * this.aspect; near = camera.near; far = camera.far; zoom = camera.zoom; // Off-axis stereoscopic effect based on // http://paulbourke.net/stereographics/stereorender/ const projectionMatrix = camera.projectionMatrix.clone(); const eyeSep = 0.064 / 2; const eyeSepOnProjection = eyeSep * near / focus; const ymax = near * Math.tan( THREE.Math.DEG2RAD * fov * 0.5 ) / camera.zoom; let xmin, xmax; // translate xOffset eyeLeft.elements[ 12 ] = - eyeSep; eyeRight.elements[ 12 ] = eyeSep; // for left eye xmin = - ymax * aspect + eyeSepOnProjection; xmax = ymax * aspect + eyeSepOnProjection; projectionMatrix.elements[ 0 ] = 2 * near / ( xmax - xmin ); projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin ); this.cameraL.projectionMatrix.copy( projectionMatrix ); // for right eye xmin = - ymax * aspect - eyeSepOnProjection; xmax = ymax * aspect - eyeSepOnProjection; projectionMatrix.elements[ 0 ] = 2 * near / ( xmax - xmin ); projectionMatrix.elements[ 8 ] = ( xmax + xmin ) / ( xmax - xmin ); this.cameraR.projectionMatrix.copy( projectionMatrix ); } this.cameraL.matrixWorld.copy( camera.matrixWorld ).multiply( eyeLeft ); this.cameraR.matrixWorld.copy( camera.matrixWorld ).multiply( eyeRight ); }; })() } ); /** the following StereoEffect is written by third party */ /** * @author alteredq / http://alteredqualia.com/ * @authod mrdoob / http://mrdoob.com/ * @authod arodic / http://aleksandarrodic.com/ * @authod fonserbc / http://fonserbc.github.io/ */ const StereoEffect = function ( renderer ) { const _stereo = new StereoCameraZoomFixed(); _stereo.aspect = 0.5; this.setSize = (width, height) => { renderer.setSize( width, height ); }; this.render = (scene, camera) => { scene.updateMatrixWorld(); if ( camera.parent === null ) camera.updateMatrixWorld(); _stereo.update( camera ); const size = renderer.getSize(); renderer.setScissorTest( true ); renderer.clear(); renderer.setScissor( 0, 0, size.width / 2, size.height ); renderer.setViewport( 0, 0, size.width / 2, size.height ); renderer.render( scene, _stereo.cameraL ); renderer.setScissor( size.width / 2, 0, size.width / 2, size.height ); renderer.setViewport( size.width / 2, 0, size.width / 2, size.height ); renderer.render( scene, _stereo.cameraR ); renderer.setScissorTest( false ); }; }; /** * @author richt / http://richt.me * @author WestLangley / http://github.com/WestLangley * * W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html) */ const ModifiedDeviceOrientationControls = function ( object ) { const scope = this; this.object = object; this.object.rotation.reorder( "YXZ" ); this.enabled = true; this.deviceOrientation = {}; this.screenOrientation = 0; const onDeviceOrientationChangeEvent = event => { scope.deviceOrientation = event; }; const onScreenOrientationChangeEvent = () => { if (typeof(window) !== 'undefined') scope.screenOrientation = window.orientation || 0; }; // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' const setObjectQuaternion = (() => { const zee = new THREE.Vector3( 0, 0, 1 ); const euler = new THREE.Euler(); const q0 = new THREE.Quaternion(); const q1 = new THREE.Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis return (cameraObject, alpha, beta, gamma, orient) => { const vector = new THREE.Vector3(0, 0, 1); vector.subVectors(cameraObject.target, cameraObject.position); euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us const quaternion = new THREE.Quaternion(); quaternion.setFromEuler( euler ); // orient the device quaternion.multiply( q1 ); // camera looks out the back of the device, not the top quaternion.multiply( q0.setFromAxisAngle( zee, - orient ) ); // adjust for screen orientation vector.applyQuaternion(quaternion); vector.addVectors(cameraObject.position, vector); cameraObject.lookAt(vector); }; })(); this.connect = () => { onScreenOrientationChangeEvent(); // run once on load if (typeof(window) !== 'undefined') { window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent, false ); window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false ); } scope.enabled = true; }; this.disconnect = () => { if (typeof(window) !== 'undefined') { window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent, false ); window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent, false ); } scope.enabled = false; }; this.update = () => { if ( scope.enabled === false ) return; const alpha = scope.deviceOrientation.alpha ? THREE.Math.degToRad( scope.deviceOrientation.alpha ) : 0; // Z const beta = scope.deviceOrientation.beta ? THREE.Math.degToRad( scope.deviceOrientation.beta ) : 0; // X' const gamma = scope.deviceOrientation.gamma ? THREE.Math.degToRad( scope.deviceOrientation.gamma ) : 0; // Y'' const orient = scope.screenOrientation ? THREE.Math.degToRad( scope.screenOrientation ) : 0; // O setObjectQuaternion( scope.object, alpha, beta, gamma, orient ); }; this.dispose = function () { this.disconnect(); }; this.connect(); }; const NDCCameraControl = function () { let camera = undefined; let targetCamera = undefined; let defaultViewport = undefined; const position = new THREE.Vector3(); const target = new THREE.Vector3(); const v1 = new THREE.Vector3(); const v2 = new THREE.Vector3(); let eventCallback = undefined; this.setCurrentCameraSettings = (cameraIn, defaultViewportIn) => { camera = cameraIn.clone(); targetCamera = cameraIn; defaultViewport = defaultViewportIn; camera.near = defaultViewport.nearPlane; if (defaultViewport.farPlane) camera.far = defaultViewport.farPlane; if (defaultViewport.eyePosition) camera.position.set(defaultViewport.eyePosition[0], defaultViewport.eyePosition[1], defaultViewport.eyePosition[2]); if (defaultViewport.upVector) camera.up.set(defaultViewport.upVector[0], defaultViewport.upVector[1], defaultViewport.upVector[2]); if (defaultViewport.targetPosition) { camera.target = new THREE.Vector3(defaultViewport.targetPosition[0], defaultViewport.targetPosition[1], defaultViewport.targetPosition[2]); camera.lookAt(camera.target); } camera.updateProjectionMatrix(); position.copy(camera.position).project(camera); target.copy(camera.target).project(camera); } this.getCurrentPosition = () => { target.copy(targetCamera.target).project(camera); return [target.x, target.y]; } this.zoom = delta => { let scaledDelta = delta * 0.002; let zoom = Math.max(targetCamera.zoom - scaledDelta, 1.0); targetCamera.zoom = zoom; targetCamera.updateProjectionMatrix(); } this.zoomToBox = (box, zoom) => { box.getCenter(v1); v1.project(camera); this.setCenterZoom([v1.x, v1.y], zoom); } //return top left and size this.getPanZoom = () => { return {target: this.getCurrentPosition(), zoom: targetCamera.zoom }; } this.setCenterZoom = (center, zoom) => { v1.set(center[0], center[1], target.z).unproject(camera); v2.copy(v1).sub(targetCamera.target); targetCamera.target.copy(v1); targetCamera.lookAt(targetCamera.target); targetCamera.position.add(v2); targetCamera.zoom = zoom; targetCamera.updateProjectionMatrix(); } this.setEventCallback = (callback) => { if (callback === undefined || (typeof callback == 'function')) eventCallback = callback; } this.triggerCallback = () => { if (eventCallback !== undefined && (typeof eventCallback == 'function')) eventCallback(); } }; exports.Viewport = Viewport exports.CameraControls = CameraControls exports.SmoothCameraTransition = SmoothCameraTransition exports.RotateCameraTransition = RotateCameraTransition exports.RayCaster = RayCaster exports.CameraAutoTumble = CameraAutoTumble exports.StereoEffect = StereoEffect exports.NDCCameraControl = NDCCameraControl