├── .gitignore ├── Gulpfile.js ├── types ├── tslint.json ├── index.d.ts ├── tsconfig.json ├── test.ts └── three-map-controls.d.ts ├── test ├── stub_dom.js └── test.js ├── index.html ├── package.json ├── gulp └── webpack.js ├── dist ├── vendor-manifest.json ├── three-map-controls.js ├── demo.js └── three-map-controls.js.map ├── LICENSE ├── README.md └── src ├── demo.js └── three-map-controls.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .idea -------------------------------------------------------------------------------- /Gulpfile.js: -------------------------------------------------------------------------------- 1 | require('./gulp/webpack'); 2 | 3 | -------------------------------------------------------------------------------- /types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules": { 4 | "eofline": false 5 | } 6 | } -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 3.5 2 | 3 | import MapControls from './three-map-controls'; 4 | export default MapControls; 5 | -------------------------------------------------------------------------------- /test/stub_dom.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | console: { 3 | log: function () {} 4 | }, 5 | document: { 6 | body: { 7 | clientWidth: 1920, 8 | clientHeight: 1080, 9 | addEventListener: function () {}, 10 | removeEventListener: function () {} 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6", "dom"], 5 | "noImplicitAny": true, 6 | "noImplicitThis": true, 7 | "strictNullChecks": true, 8 | "strictFunctionTypes": true, 9 | "noEmit": true, 10 | "baseUrl": ".", 11 | "paths": { "three-map-controls": ["."] } 12 | } 13 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 |
23 | Toggle Sphere Mode 24 | Toggle Plane Mode 25 |
26 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /types/test.ts: -------------------------------------------------------------------------------- 1 | import MapControls from 'three-map-controls'; 2 | import { PerspectiveCamera, Sphere, Plane, Vector3, Box2, MOUSE, Box3 } from "three"; 3 | 4 | const camera = new PerspectiveCamera(); 5 | const element = new Element(); 6 | 7 | let controls: MapControls = new MapControls(camera, element, { mode: "foo" }); // $ExpectError 8 | controls = new MapControls(camera, element, { foo: "bar" }); // $ExpectError 9 | 10 | controls = new MapControls(camera, element, { target: new Box3()}); // $ExpectError 11 | controls = new MapControls(camera, element, { target: new Plane(new Vector3(0, 0, 0), 0)}); // $ExpectError 12 | 13 | // $ExpectType MapControls 14 | controls = new MapControls(camera, element, { mode: "plane", target: new Sphere(new Vector3(0, 0, 0), 5) }); 15 | 16 | // $ExpectType MapControls 17 | controls = new MapControls(camera, element, { 18 | mode: "plane", 19 | target: new Sphere(new Vector3(0, 0, 0), 5), 20 | maxDistance: 5 21 | }); 22 | 23 | controls.getZoomAlpha(); // $ExpectType number 24 | controls.targetAreaVisible(); // $ExpectType Box2 25 | 26 | controls.mouseButtons = {PAN: MOUSE.RIGHT}; 27 | 28 | controls.mouseButtons = {FOO: "BAR"}; // $ExpectError 29 | controls.mouseButtons = {FOO: MOUSE.LEFT}; // $ExpectError 30 | 31 | controls.keys = {PAN_UP: "ArrowUp"}; 32 | controls.keys = {FOO: 1}; // $ExpectError -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@windfish-studio/three-map-controls", 3 | "version": "1.1.10", 4 | "types": "types", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/sikanrong/three-map-controls.git" 8 | }, 9 | "scripts": { 10 | "test": "ava --tap test/test.js", 11 | "dtslint": "dtslint types" 12 | }, 13 | "main": "src/three-map-controls.js", 14 | "author": { 15 | "name": "Alex Pilafian", 16 | "email": "sikanrong@gmail.com", 17 | "url": "http://github.com/sikanrong" 18 | }, 19 | "dependencies": { 20 | "three": "^0.113.2" 21 | }, 22 | "devDependencies": { 23 | "ava": "^2.0.0", 24 | "webpack": "4.6.0", 25 | "@babel/core": "^7.4.3", 26 | "@babel/register": "^7.4.0", 27 | "@babel/preset-env": "^7.4.3", 28 | "babel-loader": "^8.0.6", 29 | "webpack-bundle-analyzer": "^2.11.1", 30 | "chalk-log": "1.2.0", 31 | "chalk": "^2.4.1", 32 | "dom-stub": "1.0.1", 33 | "gulp-live-server": "0.0.30", 34 | "browserify-row-flow": "0.1.0", 35 | "watchify": "3.7.0", 36 | "babelify": "7.3.0", 37 | "gulp": "^4.0.2", 38 | "gulp-util": "3.0.7", 39 | "del": "2.2.2", 40 | "path": "0.12.7", 41 | "resolve-file": "0.2.1", 42 | "dtslint": "^0.8.0", 43 | "typescript": "^3.5.3" 44 | }, 45 | "ava": { 46 | "require": [ 47 | "@babel/register" 48 | ], 49 | "babel": { 50 | "testOptions": { 51 | "presets": [ 52 | "@babel/preset-env" 53 | ] 54 | } 55 | } 56 | }, 57 | "babel": { 58 | "presets": [ 59 | [ 60 | "@babel/preset-env", 61 | { 62 | "targets": { 63 | "node": "current" 64 | } 65 | } 66 | ] 67 | ] 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /types/three-map-controls.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 3.5 2 | 3 | import { 4 | Box2, 5 | EventDispatcher, 6 | MOUSE, 7 | Object3D, 8 | PerspectiveCamera, 9 | Plane, 10 | Sphere, 11 | Vector2, 12 | Vector3 13 | } from "three"; 14 | 15 | export enum KeyboardActions { 16 | PAN_UP, 17 | PAN_DOWN, 18 | PAN_LEFT, 19 | PAN_RIGHT, 20 | ZOOM_OUT, 21 | ZOOM_IN 22 | } 23 | 24 | export enum MouseActions { 25 | ZOOM, 26 | PAN 27 | } 28 | 29 | export enum MapMode { 30 | plane, 31 | sphere 32 | } 33 | 34 | export interface MapControlsRequiredOpts { 35 | target: MapControls['target']; 36 | mode: MapControls['mode']; 37 | } 38 | 39 | export default class MapControls extends EventDispatcher { 40 | constructor( 41 | camera: PerspectiveCamera, 42 | domElement: Element, 43 | options: MapControlsRequiredOpts & { [key in keyof MapControls]?: MapControls[key] } 44 | ); 45 | 46 | camera: PerspectiveCamera; 47 | domElement: Element; 48 | enabled: boolean; 49 | target: Plane | Sphere; 50 | mode: keyof typeof MapMode; 51 | 52 | minDistance: number; 53 | maxDistance: number; 54 | enableZoom: boolean; 55 | zoomSpeed: number; 56 | zoomDampingAlpha: number; 57 | initialZoom: number; 58 | 59 | enablePan: boolean; 60 | panDampingAlpha: number; 61 | 62 | keyPanSpeed: number; 63 | keyZoomSpeed: number; 64 | enableKeys: boolean; 65 | 66 | keys: {[key in keyof typeof KeyboardActions]?: string}; 67 | mouseButtons: {[key in keyof typeof MouseActions]?: MOUSE}; 68 | 69 | getZoomAlpha(): number; 70 | reset(): void; 71 | update(): void; 72 | dispose(): void; 73 | 74 | zoomToFit( 75 | mesh: Object3D, 76 | center: Vector3, 77 | dims: Vector2 78 | ): void; 79 | 80 | targetAreaVisible(): Box2; 81 | targetAreaVisibleDeg(): Box2; 82 | 83 | } 84 | -------------------------------------------------------------------------------- /gulp/webpack.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | var gulp = require('gulp'); 4 | 5 | var dist_path = path.resolve(__dirname, '../dist'); 6 | var app_entrypoint = path.resolve(__dirname, '../src/three-map-controls.js'); 7 | var demo_entrypoint = path.resolve(__dirname, '../src/demo.js'); 8 | 9 | var vendor_deps = Object.keys(require('../package.json').dependencies); 10 | var extra_plugins = []; 11 | var appConf; 12 | 13 | var doPack = async function(conf, watch){ 14 | await new Promise((resolve, reject) => { 15 | const reportStats = function (err, stats) { 16 | console.log(stats.toString({ 17 | colors: true 18 | })); 19 | 20 | if (err) { 21 | reject(); 22 | return; 23 | } 24 | 25 | resolve(stats); 26 | }; 27 | 28 | var compiler = webpack(conf); 29 | 30 | if(watch){ 31 | compiler.watch({ 32 | aggregateTimeout: 300, 33 | ignored: [/[\\/]node_modules[\\/]/, /[\\/]assets[\\/]generated[\\/]/] 34 | }, reportStats); 35 | }else{ 36 | compiler.run(reportStats); 37 | } 38 | }); 39 | }; 40 | 41 | gulp.task('webpack-vendor', async function webpackVendor () { 42 | return doPack({ 43 | entry: { 44 | vendor: vendor_deps 45 | }, 46 | 47 | output: { 48 | path: dist_path, 49 | filename: '[name].js', 50 | library: '[name]', 51 | publicPath: '/' 52 | }, 53 | 54 | node: { 55 | fs: "empty" 56 | }, 57 | 58 | plugins: [ 59 | new webpack.DllPlugin({ 60 | name: '[name]', 61 | path: path.join( dist_path, '[name]-manifest.json' ), 62 | }) 63 | ] 64 | }, false); 65 | }); 66 | 67 | 68 | gulp.task('webpack', gulp.series('webpack-vendor', async function webpackWatch () { 69 | appConf = { 70 | entry: { 71 | "three-map-controls": app_entrypoint, 72 | "demo": demo_entrypoint 73 | }, 74 | output: { 75 | path: dist_path, 76 | filename: '[name].js', 77 | publicPath: '/' 78 | }, 79 | module: { 80 | rules: [ 81 | { 82 | test: /\.js$/, 83 | exclude: /node_modules/, 84 | use: { 85 | loader: "babel-loader" 86 | } 87 | } 88 | ] 89 | }, 90 | 91 | plugins: [ 92 | new webpack.DllReferencePlugin({ 93 | manifest: require(path.join(dist_path, 'vendor-manifest.json')), 94 | }), 95 | 96 | new webpack.SourceMapDevToolPlugin({ 97 | exclude: ['vendor'], 98 | filename: '[name].js.map' 99 | }) 100 | ].concat(extra_plugins) 101 | }; 102 | 103 | return doPack(appConf, true); 104 | })); -------------------------------------------------------------------------------- /dist/vendor-manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"vendor","content":{"./node_modules/three/build/three.module.js":{"id":0,"buildMeta":{"exportsType":"namespace","providedExports":["ACESFilmicToneMapping","AddEquation","AddOperation","AdditiveBlending","AlphaFormat","AlwaysDepth","AmbientLight","AmbientLightProbe","AnimationClip","AnimationLoader","AnimationMixer","AnimationObjectGroup","AnimationUtils","ArcCurve","ArrayCamera","ArrowHelper","Audio","AudioAnalyser","AudioContext","AudioListener","AudioLoader","AxesHelper","AxisHelper","BackSide","BasicDepthPacking","BasicShadowMap","BinaryTextureLoader","Bone","BooleanKeyframeTrack","BoundingBoxHelper","Box2","Box3","Box3Helper","BoxBufferGeometry","BoxGeometry","BoxHelper","BufferAttribute","BufferGeometry","BufferGeometryLoader","ByteType","Cache","Camera","CameraHelper","CanvasRenderer","CanvasTexture","CatmullRomCurve3","CineonToneMapping","CircleBufferGeometry","CircleGeometry","ClampToEdgeWrapping","Clock","ClosedSplineCurve3","Color","ColorKeyframeTrack","CompressedTexture","CompressedTextureLoader","ConeBufferGeometry","ConeGeometry","CubeCamera","CubeGeometry","CubeReflectionMapping","CubeRefractionMapping","CubeTexture","CubeTextureLoader","CubeUVReflectionMapping","CubeUVRefractionMapping","CubicBezierCurve","CubicBezierCurve3","CubicInterpolant","CullFaceBack","CullFaceFront","CullFaceFrontBack","CullFaceNone","Curve","CurvePath","CustomBlending","CylinderBufferGeometry","CylinderGeometry","Cylindrical","DataTexture","DataTexture2DArray","DataTexture3D","DataTextureLoader","DefaultLoadingManager","DepthFormat","DepthStencilFormat","DepthTexture","DirectionalLight","DirectionalLightHelper","DirectionalLightShadow","DiscreteInterpolant","DodecahedronBufferGeometry","DodecahedronGeometry","DoubleSide","DstAlphaFactor","DstColorFactor","DynamicBufferAttribute","EdgesGeometry","EdgesHelper","EllipseCurve","EqualDepth","EquirectangularReflectionMapping","EquirectangularRefractionMapping","Euler","EventDispatcher","ExtrudeBufferGeometry","ExtrudeGeometry","Face3","Face4","FaceColors","FaceNormalsHelper","FileLoader","FlatShading","Float32Attribute","Float32BufferAttribute","Float64Attribute","Float64BufferAttribute","FloatType","Fog","FogExp2","Font","FontLoader","FrontFaceDirectionCCW","FrontFaceDirectionCW","FrontSide","Frustum","GammaEncoding","Geometry","GeometryUtils","GreaterDepth","GreaterEqualDepth","GridHelper","Group","HalfFloatType","HemisphereLight","HemisphereLightHelper","HemisphereLightProbe","IcosahedronBufferGeometry","IcosahedronGeometry","ImageBitmapLoader","ImageLoader","ImageUtils","ImmediateRenderObject","InstancedBufferAttribute","InstancedBufferGeometry","InstancedInterleavedBuffer","Int16Attribute","Int16BufferAttribute","Int32Attribute","Int32BufferAttribute","Int8Attribute","Int8BufferAttribute","IntType","InterleavedBuffer","InterleavedBufferAttribute","Interpolant","InterpolateDiscrete","InterpolateLinear","InterpolateSmooth","JSONLoader","KeyframeTrack","LOD","LatheBufferGeometry","LatheGeometry","Layers","LensFlare","LessDepth","LessEqualDepth","Light","LightProbe","LightProbeHelper","LightShadow","Line","Line3","LineBasicMaterial","LineCurve","LineCurve3","LineDashedMaterial","LineLoop","LinePieces","LineSegments","LineStrip","LinearEncoding","LinearFilter","LinearInterpolant","LinearMipMapLinearFilter","LinearMipMapNearestFilter","LinearToneMapping","Loader","LoaderUtils","LoadingManager","LogLuvEncoding","LoopOnce","LoopPingPong","LoopRepeat","LuminanceAlphaFormat","LuminanceFormat","MOUSE","Material","MaterialLoader","Math","Matrix3","Matrix4","MaxEquation","Mesh","MeshBasicMaterial","MeshDepthMaterial","MeshDistanceMaterial","MeshFaceMaterial","MeshLambertMaterial","MeshMatcapMaterial","MeshNormalMaterial","MeshPhongMaterial","MeshPhysicalMaterial","MeshStandardMaterial","MeshToonMaterial","MinEquation","MirroredRepeatWrapping","MixOperation","MultiMaterial","MultiplyBlending","MultiplyOperation","NearestFilter","NearestMipMapLinearFilter","NearestMipMapNearestFilter","NeverDepth","NoBlending","NoColors","NoToneMapping","NormalBlending","NotEqualDepth","NumberKeyframeTrack","Object3D","ObjectLoader","ObjectSpaceNormalMap","OctahedronBufferGeometry","OctahedronGeometry","OneFactor","OneMinusDstAlphaFactor","OneMinusDstColorFactor","OneMinusSrcAlphaFactor","OneMinusSrcColorFactor","OrthographicCamera","PCFShadowMap","PCFSoftShadowMap","ParametricBufferGeometry","ParametricGeometry","Particle","ParticleBasicMaterial","ParticleSystem","ParticleSystemMaterial","Path","PerspectiveCamera","Plane","PlaneBufferGeometry","PlaneGeometry","PlaneHelper","PointCloud","PointCloudMaterial","PointLight","PointLightHelper","Points","PointsMaterial","PolarGridHelper","PolyhedronBufferGeometry","PolyhedronGeometry","PositionalAudio","PositionalAudioHelper","PropertyBinding","PropertyMixer","QuadraticBezierCurve","QuadraticBezierCurve3","Quaternion","QuaternionKeyframeTrack","QuaternionLinearInterpolant","REVISION","RGBADepthPacking","RGBAFormat","RGBA_ASTC_10x10_Format","RGBA_ASTC_10x5_Format","RGBA_ASTC_10x6_Format","RGBA_ASTC_10x8_Format","RGBA_ASTC_12x10_Format","RGBA_ASTC_12x12_Format","RGBA_ASTC_4x4_Format","RGBA_ASTC_5x4_Format","RGBA_ASTC_5x5_Format","RGBA_ASTC_6x5_Format","RGBA_ASTC_6x6_Format","RGBA_ASTC_8x5_Format","RGBA_ASTC_8x6_Format","RGBA_ASTC_8x8_Format","RGBA_PVRTC_2BPPV1_Format","RGBA_PVRTC_4BPPV1_Format","RGBA_S3TC_DXT1_Format","RGBA_S3TC_DXT3_Format","RGBA_S3TC_DXT5_Format","RGBDEncoding","RGBEEncoding","RGBEFormat","RGBFormat","RGBM16Encoding","RGBM7Encoding","RGB_ETC1_Format","RGB_PVRTC_2BPPV1_Format","RGB_PVRTC_4BPPV1_Format","RGB_S3TC_DXT1_Format","RawShaderMaterial","Ray","Raycaster","RectAreaLight","RectAreaLightHelper","RedFormat","ReinhardToneMapping","RepeatWrapping","ReverseSubtractEquation","RingBufferGeometry","RingGeometry","Scene","SceneUtils","ShaderChunk","ShaderLib","ShaderMaterial","ShadowMaterial","Shape","ShapeBufferGeometry","ShapeGeometry","ShapePath","ShapeUtils","ShortType","Skeleton","SkeletonHelper","SkinnedMesh","SmoothShading","Sphere","SphereBufferGeometry","SphereGeometry","Spherical","SphericalHarmonics3","SphericalReflectionMapping","Spline","SplineCurve","SplineCurve3","SpotLight","SpotLightHelper","SpotLightShadow","Sprite","SpriteMaterial","SrcAlphaFactor","SrcAlphaSaturateFactor","SrcColorFactor","StereoCamera","StringKeyframeTrack","SubtractEquation","SubtractiveBlending","TangentSpaceNormalMap","TetrahedronBufferGeometry","TetrahedronGeometry","TextBufferGeometry","TextGeometry","Texture","TextureLoader","TorusBufferGeometry","TorusGeometry","TorusKnotBufferGeometry","TorusKnotGeometry","Triangle","TriangleFanDrawMode","TriangleStripDrawMode","TrianglesDrawMode","TubeBufferGeometry","TubeGeometry","UVMapping","Uint16Attribute","Uint16BufferAttribute","Uint32Attribute","Uint32BufferAttribute","Uint8Attribute","Uint8BufferAttribute","Uint8ClampedAttribute","Uint8ClampedBufferAttribute","Uncharted2ToneMapping","Uniform","UniformsLib","UniformsUtils","UnsignedByteType","UnsignedInt248Type","UnsignedIntType","UnsignedShort4444Type","UnsignedShort5551Type","UnsignedShort565Type","UnsignedShortType","Vector2","Vector3","Vector4","VectorKeyframeTrack","Vertex","VertexColors","VertexNormalsHelper","VideoTexture","WebGLMultisampleRenderTarget","WebGLRenderTarget","WebGLRenderTargetCube","WebGLRenderer","WebGLUtils","WireframeGeometry","WireframeHelper","WrapAroundEnding","XHRLoader","ZeroCurvatureEnding","ZeroFactor","ZeroSlopeEnding","sRGBEncoding"]}}}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # three-map-controls 2 | Map Controls class for ThreeJS; pan and zoom with respect to a ThreeJS [_Plane_](https://threejs.org/docs/#api/en/math/Plane) or [_Sphere_](https://threejs.org/docs/#api/en/math/Sphere). 3 | 4 | The aim of this library is to provide a ThreeJS-compatible interface by which a user can interact with a map, either two-dimensonal (a plane) or three-dimensonal (a sphere). The controls provided are meant to behave in the most _natural_ way possible for cartographic navigation; e.g. panning the map by dragging the mouse should keep the same point under the cursor as the map moves. 5 | ## Usage 6 | 7 | ```javascript 8 | import MapControls from '@windfish-studio/three-map-controls' 9 | 10 | const radius = 6.0; 11 | new MapControls( camera, renderer.domElement, { 12 | mode: 'sphere', 13 | target: new Sphere(new THREE.Vector3(0,0,0), radius), 14 | minDistance: 2.0, 15 | maxDistance: 20 16 | }); 17 | ``` 18 | 19 | Here's a [JSFiddle demo](https://jsfiddle.net/sikanrong/m8c250o2/). 20 | 21 | 22 | ## Change Log 23 | 24 | #### v1.1.3 - Jun 07 2019 25 | 26 | Lots of big changes in the latest version, namely supporting a spherical target mode (e.g. a globe instead of a 2D map). 27 | 28 | As well, v1.1.3 introduces a _targetAreaVisible()_ function which returns the currently-visible portion of the map, in world coordinates. 29 | 30 | In spherical mode, _targetAreaVisible()_ returns a bounding box in spherical coordinates (theta and phi, in radians). Translating these coordinates to degrees will yield a latitude-longitude bounding box. 31 | 32 | These changes are reflected in the tests, which now use the Ava testing framework. As well, the [jsfiddle demo](https://jsfiddle.net/sikanrong/m8c250o2/) has been updated to show off the new functionality. 33 | 34 | #### v1.0.1 - May 19 2018 35 | 36 | Update the project to use ES6-style classes and function scoping features. Removes previous ES6 compatability hacks. Switches out browserify for webpack. Packages demo and test bundles with webpack, moving test 37 | suite to the client. 38 | 39 | Finally adding a universal zoomToFit(mesh) function which optimally fills the screen with a given geometry by dollying the camera towards or away from it. 40 | 41 | Adjust the relationship of pan/dolly Vector math within update(). 42 | 43 | ## API 44 | 45 | ### Member Variables 46 | 47 | 48 | #### target: Plane | Sphere 49 | Must be set to instance of threejs Plane or Sphere. *required* 50 | ```javascript 51 | mapControls.target = new Sphere(new Vector3(0,0,0), 5); 52 | ``` 53 | 54 | #### mode: string 55 | Must either be set to 'sphere' or 'plane'. *required* 56 | ```javascript 57 | mapControls.mode = 'sphere'; 58 | ``` 59 | 60 | #### enabled: boolean 61 | Set to false to disable all input events. 62 | ```javascript 63 | mapControls.enabled = true; 64 | ``` 65 | 66 | #### min/maxDistance: number 67 | How far you can dolly the camera in and out from the target geometry. 68 | ```javascript 69 | mapControls.minDistance = 1; //probably should never be 0 70 | mapControls.maxDistance = 100; 71 | ``` 72 | 73 | #### enableZoom: boolean 74 | Set to false to disable all camera-dolly events. 75 | ```javascript 76 | mapControls.enableZoom = true; 77 | ``` 78 | 79 | #### zoomSpeed: number 80 | Set speed of camera dolly; how fast the camera will move towards the target geometry on mousewheel events 81 | ```javascript 82 | mapControls.zoomSpeed = 3.0; 83 | ``` 84 | 85 | #### zoomDampingAlpha: number 86 | Set the damping of the dolly movement; makes the camera dolly movement feel smoother. 87 | ```javascript 88 | mapControls.zoomDampingAlpha = 0.1; 89 | ``` 90 | 91 | #### enablePan: boolean 92 | Set to false to disable camera pan inputs. In 'sphere' mode, this disables rotation of the camera about the sphere. 93 | ```javascript 94 | mapControls.enablePan = true; 95 | ``` 96 | 97 | #### panDampingAlpha: number 98 | Sets the damping of the pan movement; makes camera pan movement feel smoother. 99 | ```javascript 100 | mapControls.panDampingAlpha = 0.2; 101 | ``` 102 | 103 | #### enableKeys: boolean 104 | Enable/disable keyboard input 105 | ```javascript 106 | mapControls.enableKeys = true; 107 | ``` 108 | 109 | #### keyPanSpeed: number 110 | Define how fast the camera should pan for each keypress. Everything on the screen should move this many pixels per kepress. 111 | ```javascript 112 | mapControls.keyPanSpeed = 12.0; 113 | ``` 114 | 115 | #### keys: object 116 | Define the keyboard char-codes which map to each pan movement. 117 | ```javascript 118 | mapControls.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 119 | ``` 120 | #### mouseButtons: object 121 | Define the mouse buttons and what action each one is mapped to. (Note: all values are from the threejs MOUSE enumeration) 122 | ```javascript 123 | mapControls.mouseButtons = { ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.LEFT }; 124 | ``` 125 | 126 | #### Member Functions 127 | 128 | #### getZoomAlpha(void): number 129 | returns current zoom value as a range between 0 and 1; zero represents the camera at mapControls.maxDistance from the target geometry (plane or sphere), and 1 is the camera at mapControls.maxDistance. 130 | ```javascript 131 | mapControls.getZoomAlpha(); 132 | ``` 133 | 134 | #### update(void): void 135 | Called on each animation frame, updates all of the internal calculations and the camera position/lookAt vectors. 136 | ```javascript 137 | mapControls.update(); 138 | ``` 139 | 140 | #### targetAreaVisible(void): [Box2](https://threejs.org/docs/#api/en/math/Box2) 141 | Returns the bounding box which defines the currently-visible area of the map, in world coordinates. 142 | 143 | In spherical mode, returns a bounding box in spherical coordinates (Θ and φ; in radians). Translating these coordinates to degrees will yield a latitude-longitude bounding box. 144 | ```javascript 145 | mapControls.update(); 146 | ``` 147 | 148 | #### zoomToFit(mesh: [Object3D](https://threejs.org/docs/#api/en/core/Object3D), center?: [Vector3](https://threejs.org/docs/#api/en/core/Vector3), dims?: [Vector2](https://threejs.org/docs/#api/en/core/Vector2)): void 149 | Will make the camera reposition itself to best fit **mesh** to the visible screen area. 150 | 151 | The **center** and **dims** parameters refer to the center and dimensions of **mesh** _relative to the projection_. By default the function will attempt to infer these values from the boundingSphere of the mesh. However, this will result in a less exact fit of the object to the visible screen area; e.g. the algorithm will leave bigger margins between the edge of the screen and the mesh. 152 | 153 | ## TODO 154 | 155 | - Add typescript type definitions. 156 | - Add JSDoc documentation 157 | 158 | ## Testing 159 | ```bash 160 | npm run test 161 | ``` 162 | ``` 163 | TAP version 13 164 | # shouldn't allow initialization if camera intersects plane 165 | ok 1 - shouldn't allow initialization if camera intersects plane 166 | # should correctly determine the camera orientation to the target plane 167 | ok 2 - should correctly determine the camera orientation to the target plane 168 | # should initialize with cam at controls.maxDistance by default 169 | ok 3 - should initialize with cam at controls.maxDistance by default 170 | # shouldn't move from initial position if no input received 171 | ok 4 - shouldn't move from initial position if no input received 172 | # should automatically orient camera towards plane based on starting position 173 | ok 5 - should automatically orient camera towards plane based on starting position 174 | # should lerp camera towards target plane on mousewheel 175 | ok 6 - should lerp camera towards target plane on mousewheel 176 | # should stop zooming at minDistance from target plane 177 | ok 7 - should stop zooming at minDistance from target plane 178 | # reset should revert camera to correct initial position 179 | ok 8 - reset should revert camera to correct initial position 180 | # should zoom into mouse pointer 181 | ok 9 - should zoom into mouse pointer 182 | # mouse should keep same world coordinates under it during camera pan (pan calibration) 183 | ok 10 - mouse should keep same world coordinates under it during camera pan (pan calibration) 184 | # initialZoom parameter should set the default cam position correctly 185 | ok 11 - initialZoom parameter should set the default cam position correctly 186 | # pan calibration should hold true when zoomed in 187 | ok 12 - pan calibration should hold true when zoomed in 188 | # sphere camera should return correct targetVisibleArea 189 | ok 13 - sphere camera should return correct targetVisibleArea 190 | # sphere camera should return correct targetVisibleArea on zoom 191 | ok 14 - sphere camera should return correct targetVisibleArea on zoom 192 | # sphere camera should maintain distance from sphere as it rotates around 193 | ok 15 - sphere camera should maintain distance from sphere as it rotates around 194 | # sphere test rotation calibration; when rotated the point on the sphere should stay under the cursor 195 | ok 16 - sphere test rotation calibration; when rotated the point on the sphere should stay under the cursor 196 | # sphere test zoom out stops at correct distance from sphere 197 | ok 17 - sphere test zoom out stops at correct distance from sphere 198 | 199 | 1..17 200 | # tests 17 201 | # pass 17 202 | # fail 0 203 | ``` 204 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import test from 'ava'; 3 | import { 4 | Plane, 5 | Sphere, 6 | Raycaster, 7 | Vector3, 8 | Vector2, 9 | PerspectiveCamera 10 | } from 'three'; 11 | import MapControls from '../src/three-map-controls.js'; 12 | 13 | //test stubs 14 | if(typeof window == 'undefined'){ 15 | global.window = require('./stub_dom'); 16 | } 17 | 18 | const aspect = window.document.body.clientWidth / window.document.body.clientHeight; 19 | const camera = new PerspectiveCamera(45, aspect, 1, 1000); 20 | 21 | global.inputEvents = {}; 22 | 23 | const addEventListenerStub = (key, listener) => { 24 | inputEvents[key] = listener; 25 | }; 26 | 27 | window.document.body.addEventListener = addEventListenerStub; 28 | 29 | const defaultOpts = { 30 | target: new Plane(new Vector3(0,0,1), 0), 31 | mode: 'plane', 32 | minDistance: 2.0, 33 | maxDistance: 20 34 | }; 35 | 36 | let controls; 37 | 38 | function advanceFrames(frames){ 39 | (Array.apply(null, Array(frames))).forEach(function(){ 40 | controls.update(); 41 | controls.camera.updateMatrixWorld(); 42 | }); 43 | }; 44 | 45 | function currentDistance(){ 46 | return Math.abs(controls.target.distanceToPoint(controls.camera.position)); 47 | }; 48 | 49 | const stub = function(){}; 50 | 51 | function EventStub(data){ 52 | 53 | this.preventDefault = stub; 54 | this.stopPropagation = stub; 55 | 56 | return Object.assign(this, data); 57 | }; 58 | 59 | const sigfigs = 3; 60 | const fastRound = (_n) => { 61 | const p = Math.pow(10, sigfigs); 62 | return (Math.round(_n*p)/p); 63 | }; 64 | 65 | const screenCenter = new Vector2( 66 | window.document.body.clientWidth / 2, 67 | window.document.body.clientHeight / 2 68 | ); 69 | 70 | var initial_cam_pos = new Vector3(3,2,-20); //what it should be, used for comparisons 71 | 72 | test("shouldn't allow initialization if camera intersects plane", (t) => { 73 | try{ 74 | controls = new MapControls( camera, window.document.body, defaultOpts ); 75 | t.fail('controls created where camera intersects target plane'); 76 | }catch(e){ 77 | t.pass('camera cannot intersect target plane on init'); 78 | } 79 | 80 | camera.position.copy(initial_cam_pos.clone()); 81 | camera.position.z = 1; 82 | 83 | try{ 84 | controls = new MapControls( camera, window.document.body, defaultOpts ); 85 | t.pass('controls created correctly'); 86 | }catch(e){ 87 | console.log(e); 88 | t.fail('controls not created successfully'); 89 | } 90 | 91 | 92 | }); 93 | 94 | test('should correctly determine the camera orientation to the target plane', (t) => { 95 | t.deepEqual(controls._camOrientation.toArray(), [0,0,-1]); 96 | camera.position.z = -1; 97 | controls = new MapControls( camera, window.document.body, defaultOpts ); 98 | t.deepEqual(controls._camOrientation.toArray(), [0,0,1]); 99 | 100 | }); 101 | 102 | test('should initialize with cam at controls.maxDistance by default', function(t){ 103 | var distance = currentDistance(); 104 | t.is(distance, controls.maxDistance); 105 | t.is(controls.getZoomAlpha(), controls.initialZoom); 106 | 107 | }); 108 | 109 | test("shouldn't move from initial position if no input received", function(t){ 110 | advanceFrames(10); 111 | var distance = currentDistance(); 112 | t.is(distance, controls.maxDistance); 113 | t.truthy(initial_cam_pos.equals(controls.camera.position)); 114 | }); 115 | 116 | test("should automatically orient camera towards plane based on starting position", function(t){ 117 | var cam_vec = new Vector3(); 118 | camera.getWorldDirection(cam_vec); 119 | t.truthy(cam_vec.equals(controls.target.normal)); 120 | 121 | }); 122 | 123 | test('should lerp camera towards target plane on mousewheel', (t) => { 124 | var lastDistance = currentDistance(); 125 | inputEvents.mousewheel(new EventStub({wheelDelta: 1})); 126 | advanceFrames(1000); 127 | var distance = currentDistance(); 128 | var expected = lastDistance * Math.pow(0.95, controls.zoomSpeed); 129 | t.is(fastRound(distance), fastRound(expected)); //round both to 3rd decimal place for comparison 130 | 131 | }); 132 | 133 | test('should stop zooming at minDistance from target plane', (t) => { 134 | controls.reset(); 135 | (Array.apply(null, Array(20))).forEach(function(){ 136 | inputEvents.mousewheel(new EventStub({ 137 | wheelDelta: 1, 138 | offsetX: window.document.body.clientWidth / 2, 139 | offsetY: window.document.body.clientHeight / 2 140 | })); 141 | }); 142 | 143 | advanceFrames(1000); 144 | var distance = currentDistance(); 145 | t.is(controls.minDistance, distance); 146 | t.is(controls.getZoomAlpha(), 1); 147 | 148 | 149 | }); 150 | 151 | test('reset should revert camera to correct initial position', function(t){ 152 | controls.reset(); 153 | t.truthy(initial_cam_pos.equals(controls.camera.position)); 154 | 155 | }); 156 | 157 | test('should zoom into mouse pointer', function(t){ //e.g. should act like maps controls. 158 | (Array.apply(null, Array(30))).forEach(function(){ 159 | inputEvents.mousewheel(new EventStub({ 160 | wheelDelta: 1, 161 | offsetX: 400, 162 | offsetY: 300 163 | })); 164 | }); 165 | 166 | advanceFrames(1000); 167 | var tolerance = Math.pow(10, -sigfigs); 168 | 169 | const desired = new Vector3( 170 | 10.812787997105476, 171 | 5.34833686125601, 172 | -1.8118972640060278 173 | ); 174 | 175 | var delta = Math.abs(new Vector3().subVectors(desired, controls.camera.position).length()); 176 | 177 | t.truthy( delta <= tolerance ); 178 | 179 | }); 180 | 181 | 182 | var testPanCalibration = function(t, move, tol){ 183 | 184 | move = move || new Vector2(10, 10); 185 | 186 | const intersectMouse = function(x, y){ 187 | var mouse_pos = new Vector2( 188 | ( x / window.document.body.clientWidth ) * 2 - 1, 189 | - ( y / window.document.body.clientHeight ) * 2 + 1); //NDC 190 | 191 | const raycaster = new Raycaster(); 192 | raycaster.setFromCamera(mouse_pos, controls.camera); 193 | 194 | const intersection = new Vector3(); 195 | 196 | switch(controls.mode){ 197 | case 'plane': 198 | raycaster.ray.intersectPlane(controls.target, intersection); 199 | break; 200 | case 'sphere': 201 | raycaster.ray.intersectSphere(controls.target, intersection); 202 | break; 203 | } 204 | 205 | return intersection; 206 | }; 207 | 208 | 209 | //push mouse button down.. 210 | inputEvents.mousedown(new EventStub({ 211 | offsetX: screenCenter.x, 212 | offsetY: screenCenter.y, 213 | button: controls.mouseButtons.PAN 214 | })); 215 | 216 | var first_campos = controls.camera.position.clone(); 217 | var first_intersect = intersectMouse(screenCenter.x, screenCenter.y); 218 | 219 | const newMouse = screenCenter.clone().add(move); 220 | 221 | inputEvents.mousemove(new EventStub({ 222 | offsetX: newMouse.x, 223 | offsetY: newMouse.y 224 | })); 225 | 226 | advanceFrames(1000); 227 | 228 | var second_campos = controls.camera.position.clone(); 229 | var second_intersect = intersectMouse(newMouse.x, newMouse.y); 230 | 231 | //second_intersect should be the same as first_intersect; e.g. the point in world-space under the mouse should not 232 | //have changed during pan operation 233 | const delta = Math.abs(new Vector3().subVectors(second_intersect, first_intersect).length()); 234 | t.truthy(delta <= (tol || 0.0001)); 235 | t.notDeepEqual(first_campos.toArray(), second_campos.toArray()); 236 | 237 | inputEvents.mouseup(); 238 | }; 239 | 240 | test('mouse should keep same world coordinates under it during camera pan (pan calibration)', function(t){ 241 | controls.reset(); 242 | testPanCalibration(t); 243 | }); 244 | 245 | 246 | test('initialZoom parameter should set the default cam position correctly', function(t){ 247 | controls.initialZoom = 0.5; 248 | controls.reset(); 249 | 250 | var correct_z = initial_cam_pos.z + ((controls.maxDistance - controls.minDistance) / 2); 251 | t.is(controls.camera.position.z, correct_z); 252 | 253 | //try max zoom 254 | controls.initialZoom = 1; 255 | controls.reset(); 256 | 257 | var correct_z = -controls.minDistance; 258 | t.is(controls.camera.position.z, correct_z); 259 | 260 | }); 261 | 262 | test('pan calibration should hold true when zoomed in', function(t){ 263 | controls.reset(); 264 | controls.camera.updateWorldMatrix(); 265 | testPanCalibration(t); 266 | }); 267 | 268 | test('sphere camera should return correct targetVisibleArea', (t) => { 269 | controls.dispose(); 270 | controls = undefined; 271 | 272 | camera.position.set(0,0,100); 273 | camera.lookAt(new Vector3(0,0,0)); 274 | camera.updateWorldMatrix(); 275 | controls = new MapControls( camera, window.document.body, { 276 | target: new Sphere(new Vector3(0,0,0), 10), 277 | mode: 'sphere', 278 | minDistance: 2, 279 | maxDistance: 100 280 | }); 281 | 282 | const bbox = controls.targetAreaVisible(); 283 | const bbox_ar = Array.prototype.concat.apply([], [bbox.min, bbox.max].map(_v => {return _v.toArray()})); 284 | t.deepEqual( 285 | bbox_ar, 286 | [-Math.PI/2, -Math.PI/2, Math.PI/2, Math.PI/2] 287 | ); 288 | }); 289 | 290 | test('sphere camera should return correct targetVisibleArea on zoom', (t) => { 291 | 292 | (Array.apply(null, Array(20))).forEach(function(){ 293 | inputEvents.mousewheel(new EventStub({ 294 | wheelDelta: 1, 295 | offsetX: window.document.body.clientWidth / 2, 296 | offsetY: window.document.body.clientHeight / 2 297 | })); 298 | }); 299 | 300 | advanceFrames(1000); 301 | 302 | const bbox = controls.targetAreaVisible(); 303 | const bbox_ar = Array.prototype.concat.apply([], [bbox.min, bbox.max].map(_v => {return _v.toArray()})); 304 | t.deepEqual( 305 | bbox_ar, 306 | [ 307 | -0.14727593328821142, 308 | -0.08284271247461894, 309 | 0.14727593328821142, 310 | 0.08284271247461894 311 | ] 312 | ); 313 | }); 314 | 315 | test('sphere camera should maintain distance from sphere as it rotates around', (t) => { 316 | 317 | t.is(currentDistance(), controls.minDistance); 318 | const first_campos = controls.camera.position.clone(); 319 | 320 | inputEvents.mousedown(new EventStub({ 321 | offsetX: screenCenter.x, 322 | offsetY: screenCenter.y, 323 | button: controls.mouseButtons.PAN 324 | })); 325 | 326 | inputEvents.mousemove(new EventStub({ 327 | offsetX: screenCenter.x + 10, 328 | offsetY: screenCenter.y + 10 329 | })); 330 | 331 | inputEvents.mouseup(); 332 | 333 | advanceFrames(1000); 334 | 335 | t.is(fastRound(currentDistance()), fastRound(controls.minDistance)); 336 | t.notDeepEqual(first_campos.toArray(), controls.camera.position.toArray()); 337 | 338 | }); 339 | 340 | test('sphere test rotation calibration; when rotated the point on the sphere should stay under the cursor', (t) => { 341 | controls.camera.updateWorldMatrix(); 342 | testPanCalibration(t, new Vector2(10, 10), 0.001); 343 | }); 344 | 345 | test('sphere test zoom out stops at correct distance from sphere', (t) => { 346 | (Array.apply(null, Array(20))).forEach(function(){ 347 | inputEvents.mousewheel(new EventStub({ 348 | wheelDelta: -1, 349 | offsetX: window.document.body.clientWidth / 2, 350 | offsetY: window.document.body.clientHeight / 2 351 | })); 352 | }); 353 | 354 | advanceFrames(1000); 355 | 356 | t.is(fastRound(controls.maxDistance), fastRound(currentDistance())); 357 | }); 358 | -------------------------------------------------------------------------------- /dist/three-map-controls.js: -------------------------------------------------------------------------------- 1 | !function(t){var e={};function s(i){if(e[i])return e[i].exports;var a=e[i]={i:i,l:!1,exports:{}};return t[i].call(a.exports,a,a.exports,s),a.l=!0,a.exports}s.m=t,s.c=e,s.d=function(t,e,i){s.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:i})},s.r=function(t){Object.defineProperty(t,"__esModule",{value:!0})},s.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return s.d(e,"a",e),e},s.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},s.p="/",s(s.s=1)}([function(t,e,s){t.exports=s(3)(0)},function(t,e,s){"use strict";s.r(e);var i=s(0);if("undefined"==typeof window){s(2)}e.default=class extends i.EventDispatcher{constructor(t,e,s){super(),this.camera=t,this.domElement=void 0!==e?e:window.document.body,this.enabled=!0,this.target,this.minDistance=1,this.maxDistance=100,this.enableZoom=!0,this.zoomSpeed=6,this.zoomDampingAlpha=.1,this.initialZoom=0,this.enablePan=!0,this.keyPanSpeed=50,this.keyZoomSpeed=this.zoomSpeed,this.panDampingAlpha=.1,this.enableKeys=!0,this.keys={PAN_LEFT:"ArrowLeft",PAN_UP:"ArrowUp",PAN_RIGHT:"ArrowRight",PAN_BOTTOM:"ArrowDown",ZOOM_IN:"]",ZOOM_OUT:"["},this.mouseButtons={ZOOM:i.MOUSE.MIDDLE,PAN:i.MOUSE.LEFT},Object.assign(this,s);let a=!1;if(void 0===this.mode)throw new Error("'mode' option must be set to either 'plane' or 'sphere'");switch(this.mode){case"plane":a=void 0!==this.target.normal&&void 0!==this.target.constant;break;case"sphere":a=void 0!==this.target.center&&void 0!==this.target.radius}if(!a)throw new Error("'target' option must be an instance of type THREE.Plane or THREE.Sphere");this._eventListeners={contextmenu:this._onContextMenu.bind(this),mousedown:this._onMouseDown.bind(this),mousewheel:this._onMouseWheel.bind(this),MozMousePixelScroll:this._onMouseWheel.bind(this),touchstart:this._onTouchStart.bind(this),touchend:this._onTouchEnd.bind(this),touchmove:this._onTouchMove.bind(this),keydown:this._onKeyDown.bind(this),mouseover:this._onMouseOver.bind(this),mouseout:this._onMouseOut.bind(this),mousemove:this._onMouseMove.bind(this),mouseup:this._onMouseUp.bind(this)},this._init()}_init(){if(this.target0=this.target.clone(),this.position0=this.camera.position.clone(),this.zoom0=this.camera.zoom,this._changeEvent={type:"change"},this._startEvent={type:"start"},this._endEvent={type:"end"},this._STATES={NONE:-1,DOLLY:1,PAN:2,TOUCH_DOLLY:4,TOUCH_PAN:5},0==this.target.distanceToPoint(this.camera.position))throw new Error("ORIENTATION_UNKNOWABLE: initial Camera position cannot intersect target plane.");this._state=this._STATES.NONE,this._mouse=new i.Vector2,this._finalTargetDistance=0,this._currentTargetDistance=0,this._panTarget=new i.Vector3(0,0,0),this._panCurrent=new i.Vector3(0,0,0),this._minZoomPosition=new i.Vector3,this._maxZoomPosition=new i.Vector3,this._panStart=new i.Vector2,this._panEnd=new i.Vector2,this._panDelta=new i.Vector2,this._dollyStart=new i.Vector2,this._dollyEnd=new i.Vector2,this._dollyDelta=new i.Vector2,this._camOrientation=new i.Vector2,this._zoomAlpha,this._screenWorldXform=Math.tan(this.camera.fov/2*Math.PI/180),this._straightDollyTrack(),this.camera.position.lerpVectors(this._minZoomPosition,this._maxZoomPosition,this.initialZoom),this._finalTargetDistance=this._currentTargetDistance=Math.abs(this.target.distanceToPoint(this.camera.position));const t=this._intersectCameraTarget();this.camera.lookAt(t.intersection),this._camOrientation=t.ray.direction.clone().normalize(),this._updateZoomAlpha(),["contextmenu","mousedown","mousewheel","MozMousePixelScroll","touchstart","touchend","touchmove","mouseover","mouseout","keydown"].forEach(t=>{this.domElement.addEventListener(t,this._eventListeners[t],!1)}),"CANVAS"!=this.domElement.tagName||this.domElement.getAttribute("tabindex")||this.domElement.setAttribute("tabindex","1"),this.update()}_intersectCameraTarget(){let t,e=new i.Vector3;switch(this.mode){case"plane":const s=new i.Vector3;this.target.projectPoint(this.camera.position,s),(t=new i.Ray(this.camera.position,(new i.Vector3).subVectors(s,this.camera.position).normalize())).intersectPlane(this.target,e);break;case"sphere":(t=new i.Ray(this.camera.position,(new i.Vector3).subVectors(this.target.center,this.camera.position).normalize())).intersectSphere(this.target,e)}return{intersection:e,ray:t}}_straightDollyTrack(){this._updateDollyTrack(this._intersectCameraTarget().ray)}getZoomAlpha(){return this._zoomAlpha}reset(){this.target.copy(this.target0),this.camera.position.copy(this.position0),this.camera.zoom=this.zoom0,this.camera.updateProjectionMatrix(),this._init(),this.dispatchEvent(this._changeEvent),this.update(),this._state=this._STATES.NONE}update(){const t=new i.Vector3,e=new i.Vector3,s=this.camera.position;switch(e.copy(this._panCurrent),this._panCurrent.lerp(this._panTarget,this.panDampingAlpha),t.subVectors(this._panCurrent,e),this.mode){case"plane":this._maxZoomPosition.add(t),this._minZoomPosition.add(t);break;case"sphere":const s=new i.Vector3,a=new i.Quaternion;a.setFromAxisAngle(s.setFromMatrixColumn(this.camera.matrix,1),t.x),this._maxZoomPosition.applyQuaternion(a),this._minZoomPosition.applyQuaternion(a),a.setFromAxisAngle(s.setFromMatrixColumn(this.camera.matrix,0),t.y),this._maxZoomPosition.applyQuaternion(a),this._minZoomPosition.applyQuaternion(a),a.setFromAxisAngle(new i.Vector3(0,1,0),t.z),this._maxZoomPosition.applyQuaternion(a),this._minZoomPosition.applyQuaternion(a)}s.lerpVectors(this._minZoomPosition,this._maxZoomPosition,this._updateZoomAlpha()),"sphere"==this.mode&&this.camera.lookAt(this.target.center)}dispose(){Object.keys(this._eventListeners).forEach(t=>{this.domElement.removeEventListener(t,this._eventListeners[t],!1)})}zoomToFit(t,e,s){if(void 0===e&&(e=t.geometry.boundingSphere.center.clone()),e=t.localToWorld(e.clone()),void 0===s){const e=2*t.geometry.boundingSphere.radius;s=new i.Vector2(e,e)}switch(this.mode){case"plane":this._panTarget.copy(e),this._panCurrent.copy(this._intersectCameraTarget().intersection);break;case"sphere":const t=this._sphericalCoordinatesFrom(e),s=this._sphericalCoordinatesFrom(this.camera.position),a=(new i.Vector2).subVectors(t,s);Math.abs(a.x)>Math.PI&&(a.x=-Math.abs(a.x)/a.x*(2*Math.PI-Math.abs(a.x))),this._panTarget.add(new i.Vector3(0,-a.y,a.x))}this._straightDollyTrack();const a=this.camera.fov*(Math.PI/180),o=2*Math.atan(Math.tan(a/2)*this.camera.aspect),n=s.x/s.y;this._finalTargetDistance=(n>this.camera.aspect?s.x:s.y)/2/Math.tan((n>this.camera.aspect?o:a)/2)}targetAreaVisible(){let t,e,s,a;switch(this.mode){case"plane":var o=new i.Ray(this.camera.position,this._camOrientation).distanceToPlane(this.target);a=this.camera.position.clone(),s=(e=this._screenWorldXform*o)*this.camera.aspect,t=new i.Box2(new i.Vector2(a.x-s,a.y-e),new i.Vector2(a.x+s,a.y+e));break;case"sphere":const n=(new i.Vector3).subVectors(this.target.center,this.camera.position);a=this._sphericalCoordinatesFrom(this.camera.position);const h=Math.PI/2,r=n.length();e=this._screenWorldXform*(r/this.target.radius-1),e=Math.min(e,h);const c=this.target.radius*Math.cos(a.y-h);s=e*this.camera.aspect*(this.target.radius/c),s=Math.min(s,h),t=new i.Box2(new i.Vector2(a.x-s-h,a.y-e-h),new i.Vector2(a.x+s-h,a.y+e-h)),["min","max"].forEach(e=>{t[e].x=t[e].x>Math.PI?-2*Math.PI+t[e].x:t[e].x})}return t}targetAreaVisibleDeg(){let t=this.targetAreaVisible();return"sphere"==this.mode&&(t.min.x=t.min.x*(180/Math.PI),t.min.y=t.min.y*(180/Math.PI),t.max.x=t.max.x*(180/Math.PI),t.max.y=t.max.y*(180/Math.PI)),t}_sphericalCoordinatesFrom(t){const e=(new i.Vector3).subVectors(this.target.center,t),s=new i.Vector3(e.x,0,e.z),a=(new i.Vector3,new i.Vector2(s.angleTo(new i.Vector3(1,0,0)),e.angleTo(new i.Vector3(0,1,0))));return a.x=e.z>0?2*Math.PI-a.x:a.x,a}_updateZoomAlpha(){this._finalTargetDistance=Math.max(this.minDistance,Math.min(this.maxDistance,this._finalTargetDistance));var t=this._currentTargetDistance-this._finalTargetDistance,e=this.zoomDampingAlpha;return this._currentTargetDistance-=t*e,this._zoomAlpha=Math.abs(Math.round(1e5*(1-(this._currentTargetDistance-this.minDistance)/(this.maxDistance-this.minDistance)))/1e5),this._zoomAlpha}_updateDollyTrack(t){let e=new i.Vector3;switch(this.mode){case"plane":t.intersectPlane(this.target,e);break;case"sphere":t.intersectSphere(this.target,e)}e&&(this._maxZoomPosition.addVectors(e,(new i.Vector3).subVectors(this.camera.position,e).normalize().multiplyScalar(this.minDistance)),this._minZoomPosition.copy(this._calculateMinZoom(this.camera.position,e)),this._finalTargetDistance=this._currentTargetDistance=e.clone().sub(this.camera.position).length())}_getZoomScale(t){return t=t||this.zoomSpeed,Math.pow(.95,t)}_panLeft(t,e){var s=new i.Vector3;switch(this.mode){case"sphere":s.set(-t,0,0);break;case"plane":s.setFromMatrixColumn(e,0),s.multiplyScalar(-t)}this._panTarget.add(s)}_panUp(t,e){var s=new i.Vector3;switch(this.mode){case"sphere":s.set(0,-t,0);break;case"plane":s.setFromMatrixColumn(e,1),s.multiplyScalar(t)}this._panTarget.add(s)}_pan(t,e){var s,a=this.domElement,o=new i.Ray(this.camera.position,this._camOrientation);switch(this.mode){case"plane":s=this._screenWorldXform*o.distanceToPlane(this.target);break;case"sphere":const t=(new i.Vector3).subVectors(this.camera.position,this.target.center);s=this._screenWorldXform*(t.length()/this.target.radius-1)}this._panLeft(2*t*s/a.clientHeight,this.camera.matrix),this._panUp(2*e*s/a.clientHeight,this.camera.matrix)}_dollyIn(t){this._cameraOfKnownType()?this._finalTargetDistance/=t:(console.warn("WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_dollyOut(t){this._cameraOfKnownType()?this._finalTargetDistance*=t:(console.warn("WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_cameraOfKnownType(){return"PerspectiveCamera"===this.camera.type}_handleUpdateDollyTrackMouse(t){var e=this._mouse.clone();if(this._mouse.set(t.offsetX/this.domElement.clientWidth*2-1,-t.offsetY/this.domElement.clientHeight*2+1),!e.equals(this._mouse)){var s=new i.Raycaster;s.setFromCamera(this._mouse,this.camera),this._updateDollyTrack(s.ray)}}_handleMouseDownDolly(t){this._handleUpdateDollyTrackMouse(t),this._dollyStart.set(t.offsetX,t.offsetY)}_handleMouseDownPan(t){this._panStart.set(t.offsetX,t.offsetY)}_handleMouseMoveDolly(t){this._handleUpdateDollyTrackMouse(t),this._dollyEnd.set(t.offsetX,t.offsetY),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyIn(this._getZoomScale()):this._dollyDelta.y<0&&this._dollyOut(this._getZoomScale()),this._dollyStart.copy(this._dollyEnd),this.update()}_handleMouseMovePan(t){this._panEnd.set(t.offsetX,t.offsetY),this._panDelta.subVectors(this._panEnd,this._panStart),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleMouseUp(t){}_calculateMinZoom(t,e){return e.clone().add(t.clone().sub(e).normalize().multiplyScalar(this.maxDistance))}_handleMouseWheel(t){this._handleUpdateDollyTrackMouse(t);var e=0;void 0!==t.wheelDelta?e=t.wheelDelta:void 0!==t.detail&&(e=-t.detail),e>0?this._dollyOut(this._getZoomScale()):e<0&&this._dollyIn(this._getZoomScale()),this.update()}_handleKeyDown(t){switch(t.key){case this.keys.PAN_UP:this._pan(0,this.keyPanSpeed),this.update();break;case this.keys.PAN_BOTTOM:this._pan(0,-this.keyPanSpeed),this.update();break;case this.keys.PAN_LEFT:this._pan(this.keyPanSpeed,0),this.update();break;case this.keys.PAN_RIGHT:this._pan(-this.keyPanSpeed,0),this.update();break;case this.keys.ZOOM_IN:this._dollyIn(this._getZoomScale(this.keyZoomSpeed)),this.update();break;case this.keys.ZOOM_OUT:this._dollyOut(this._getZoomScale(this.keyZoomSpeed)),this.update()}}_handleUpdateDollyTrackTouch(t){var e=new i.Vector2,s=t.touches[0].pageX-t.touches[1].pageX,a=t.touches[0].pageY-t.touches[1].pageY;e.x=t.touches[0].pageX+s/2,e.y=t.touches[0].pageY+a/2;var o=new i.Vector2;o.x=e.x/domElement.clientWidth*2-1,o.y=-e.y/domElement.clientHeight*2+1,this._updateDollyTrack(o)}_handleTouchStartDolly(t){this._handleUpdateDollyTrackTouch(t);var e=t.touches[0].pageX-t.touches[1].pageX,s=t.touches[0].pageY-t.touches[1].pageY,i=Math.sqrt(e*e+s*s);this._dollyStart.set(0,i)}_handleTouchStartPan(t){this._panStart.set(t.touches[0].pageX,t.touches[0].pageY)}_handleTouchMoveDolly(t){this._handleUpdateDollyTrackTouch(t);var e=t.touches[0].pageX-t.touches[1].pageX,s=t.touches[0].pageY-t.touches[1].pageY,i=Math.sqrt(e*e+s*s);this._dollyEnd.set(0,i),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyOut(this._getZoomScale()):this._dollyDelta.y<0&&this._dollyIn(this._getZoomScale()),this._dollyStart.copy(this._dollyEnd),this.update()}_handleTouchMovePan(t){this._panEnd.set(t.touches[0].pageX,t.touches[0].pageY),this._panDelta.subVectors(this._panEnd,this._panStart),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleTouchEnd(t){}_onMouseDown(t){if(!1!==this.enabled){if(t.preventDefault(),t.button===this.mouseButtons.ZOOM){if(!1===this.enableZoom)return;this._handleMouseDownDolly(t),this._state=this._STATES.DOLLY}else if(t.button===this.mouseButtons.PAN){if(!1===this.enablePan)return;this._handleMouseDownPan(t),this._state=this._STATES.PAN}this._state!==this._STATES.NONE&&(this.domElement.addEventListener("mousemove",this._eventListeners.mousemove,!1),this.domElement.addEventListener("mouseup",this._eventListeners.mouseup,!1),this.dispatchEvent(this._startEvent))}}_onMouseMove(t){if(!1!==this.enabled)if(t.preventDefault(),this._state===this._STATES.DOLLY){if(!1===this.enableZoom)return;this._handleMouseMoveDolly(t)}else if(this._state===this._STATES.PAN){if(!1===this.enablePan)return;this._handleMouseMovePan(t)}}_onMouseUp(t){!1!==this.enabled&&(this._handleMouseUp(t),this.domElement.removeEventListener("mousemove",this._eventListeners.mousemove,!1),this.domElement.removeEventListener("mouseup",this._eventListeners.mouseup,!1),this.dispatchEvent(this._endEvent),this._state=this._STATES.NONE)}_onMouseWheel(t){!1!==this.enabled&&!1!==this.enableZoom&&this._state===this._STATES.NONE&&(t.preventDefault(),t.stopPropagation(),this._handleMouseWheel(t),this.dispatchEvent(this._startEvent),this.dispatchEvent(this._endEvent))}_onKeyDown(t){!1!==this.enabled&&!1!==this.enableKeys&&!1!==this.enablePan&&this._handleKeyDown(t)}_onTouchStart(t){if(!1!==this.enabled){switch(t.touches.length){case 1:if(!1===this.enablePan)return;this._handleTouchStartPan(t),this._state=this._STATES.TOUCH_PAN;break;case 2:if(!1===this.enableZoom)return;this._handleTouchStartDolly(t),this._state=this._STATES.TOUCH_DOLLY;break;default:this._state=this._STATES.NONE}this._state!==this._STATES.NONE&&this.dispatchEvent(this._startEvent)}}_onTouchMove(t){if(!1!==this.enabled)switch(t.preventDefault(),t.stopPropagation(),t.touches.length){case 1:if(!1===this.enablePan)return;if(this._state!==this._STATES.TOUCH_PAN)return;this._handleTouchMovePan(t);break;case 2:if(!1===this.enableZoom)return;if(this._state!==this._STATES.TOUCH_DOLLY)return;this._handleTouchMoveDolly(t);break;default:this._state=this._STATES.NONE}}_onTouchEnd(t){!1!==this.enabled&&(this._handleTouchEnd(t),this.dispatchEvent(this._endEvent),this._state=this._STATES.NONE)}_onContextMenu(t){t.preventDefault()}_onMouseOver(t){return this.domElement.focus(),!1}_onMouseOut(t){return this.domElement.blur(),!1}}},function(t,e){t.exports={console:{log:function(){}},document:{body:{clientWidth:1920,clientHeight:1080,addEventListener:function(){},removeEventListener:function(){}}}}},function(t,e){t.exports=vendor}]); 2 | //# sourceMappingURL=three-map-controls.js.map -------------------------------------------------------------------------------- /src/demo.js: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | Vector3, 4 | Vector2, 5 | Quaternion, 6 | Box2, 7 | Box3, 8 | PerspectiveCamera, 9 | SphereBufferGeometry, 10 | PlaneBufferGeometry, 11 | BufferGeometry, 12 | CubeGeometry, 13 | PointsMaterial, 14 | MeshBasicMaterial, 15 | MeshNormalMaterial, 16 | Color, 17 | DoubleSide, 18 | VertexColors, 19 | Mesh, 20 | Points, 21 | Raycaster, 22 | Float32BufferAttribute, 23 | Plane, 24 | Sphere, 25 | WebGLRenderer 26 | } from 'three'; 27 | import MapControls from './three-map-controls.js'; 28 | 29 | const SPHERE_RADIUS = 10; 30 | 31 | class MapControlsDemo { 32 | constructor (mode) { 33 | this.container = document.body; 34 | this.scene = new Scene(); 35 | this.renderer = null; 36 | this.meshes = []; 37 | this.dims = 10; 38 | this.selectedObjectTween = 0; 39 | this.selectedObject = null; 40 | this.controls; 41 | this.mode; 42 | 43 | this.debugCamViewInterval; 44 | 45 | this.camViewMesh; 46 | this.camViewLines; 47 | 48 | this.init(); 49 | this.setMode(mode); 50 | this.animate(); 51 | } 52 | 53 | setMode(mode) { 54 | this.deselect(); 55 | this.mode = mode; 56 | const links = { 57 | sphere: document.getElementById('sphere-link'), 58 | plane: document.getElementById('plane-link') 59 | }; 60 | 61 | links[this.mode].style.display = 'none'; 62 | links[(this.mode == 'plane')? 'sphere' : 'plane'].style.display = 'inline-block'; 63 | 64 | this.meshes.concat([this.camViewLines, this.camViewMesh]).forEach((_m) => { 65 | if(_m === undefined){ 66 | return; 67 | } 68 | 69 | this.scene.remove(_m); 70 | _m.geometry.dispose(); 71 | }); 72 | 73 | this.camViewLines = this.camViewMesh = undefined; 74 | 75 | switch(this.mode){ 76 | case 'sphere': 77 | this.initSphere(); 78 | break; 79 | case 'plane': 80 | this.initPlane(); 81 | break; 82 | } 83 | } 84 | 85 | initSphere(){ 86 | 87 | var camera = new PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 1000 ); 88 | camera.position.z = 40; 89 | this.controls = new MapControls( camera, this.renderer.domElement, { 90 | target: new Sphere(new Vector3(0,0,0), SPHERE_RADIUS), 91 | mode: 'sphere', 92 | minDistance: 1, 93 | maxDistance: camera.position.z 94 | }); 95 | 96 | const colors = []; 97 | 98 | const geometry = new SphereBufferGeometry(SPHERE_RADIUS, this.dims, this.dims); 99 | geometry.computeBoundingSphere(); 100 | 101 | const vertices = geometry.getAttribute('position').array; 102 | for(var i = 0; i < vertices.length; i += 3){ 103 | var color = new Color(); 104 | var vert = new Vector3(vertices[i], vertices[i+1], vertices[i+2]); 105 | 106 | color.setRGB( 107 | ( vert.x / SPHERE_RADIUS ) + 0.5, 108 | ( vert.y / SPHERE_RADIUS ) + 0.5, 109 | ( vert.z / SPHERE_RADIUS ) + 0.5 110 | ); 111 | 112 | colors.push( color.r, color.g, color.b ); 113 | } 114 | 115 | geometry.addAttribute( 'color', new Float32BufferAttribute( Float32Array.from(colors), 3 ) ); 116 | 117 | const points = new Points( 118 | geometry, 119 | new PointsMaterial( { size: 1, vertexColors: VertexColors } ) 120 | ); 121 | 122 | this.scene.add( points ); 123 | this.meshes.push( points ); 124 | 125 | const polys = new Mesh( 126 | geometry, 127 | new MeshBasicMaterial({ 128 | vertexColors: VertexColors, 129 | transparent: true, 130 | opacity: 0.2 131 | 132 | }) 133 | ); 134 | 135 | polys.userData.selectable = true; 136 | 137 | this.meshes.push( polys ); 138 | this.scene.add( polys ); 139 | 140 | const lines = new Mesh( 141 | geometry, 142 | new MeshBasicMaterial({ 143 | vertexColors: VertexColors, 144 | wireframe: true 145 | }) 146 | ); 147 | 148 | this.meshes.push( lines ); 149 | this.scene.add( lines ); 150 | 151 | } 152 | 153 | toggleDebugCamView(e){ 154 | if(!e.target.checked){ 155 | clearInterval(this.debugCamViewInterval); 156 | this.scene.remove( this.camViewMesh ); 157 | this.scene.remove( this.camViewLines ); 158 | this.camViewMesh.geometry.dispose(); 159 | this.camViewLines.geometry.dispose(); 160 | this.camViewLines = this.camViewMesh = undefined; 161 | return true; 162 | } 163 | 164 | this.debugCamViewInterval = setInterval(() => { 165 | const bbox = this.controls.targetAreaVisible(); 166 | console.log(`${bbox.min.x}, ${bbox.min.y}, ${bbox.max.x}, ${bbox.max.y}`); 167 | 168 | let geometry, position; 169 | position = new Vector3(0,0,0); 170 | 171 | switch (this.mode) { 172 | case 'sphere': 173 | let phidelta = Math.abs(bbox.max.x - bbox.min.x); 174 | if(phidelta > Math.PI){ 175 | phidelta = Math.abs((bbox.max.x + Math.PI*2) - bbox.min.x); 176 | } 177 | geometry = new SphereBufferGeometry(SPHERE_RADIUS, this.dims, this.dims, 178 | bbox.min.x + Math.PI/2, //phistart 179 | phidelta, //philength 180 | -bbox.max.y + Math.PI/2, //thetastart 181 | Math.abs(bbox.max.y - bbox.min.y) //thetalength 182 | ); 183 | break; 184 | case 'plane': 185 | 186 | geometry = new PlaneBufferGeometry( 187 | (bbox.max.x - bbox.min.x), 188 | (bbox.max.y - bbox.min.y), 189 | this.dims, this.dims 190 | ); 191 | 192 | position.copy(this.controls.camera.position); 193 | position.z = 0; 194 | 195 | break; 196 | } 197 | 198 | if(this.camViewMesh == undefined){ 199 | this.camViewMesh = new Mesh( 200 | geometry, 201 | new MeshBasicMaterial({ 202 | color: new Color(1, 0, 0), 203 | side: DoubleSide, 204 | transparent: true, 205 | opacity: 0.5 206 | }) 207 | ); 208 | 209 | this.camViewLines = new Mesh( 210 | geometry, 211 | new MeshBasicMaterial({ 212 | color: new Color(1, 0, 0), 213 | wireframe: true 214 | }) 215 | ); 216 | 217 | this.scene.add( this.camViewMesh ); 218 | this.scene.add( this.camViewLines ); 219 | }else{ 220 | 221 | this.camViewMesh.geometry.copy(geometry); 222 | this.camViewLines.geometry.copy(geometry); 223 | 224 | geometry.dispose(); 225 | } 226 | 227 | this.camViewMesh.geometry.computeBoundingSphere(); 228 | this.camViewMesh.position.copy(position); 229 | this.camViewLines.position.copy(position); 230 | 231 | }, 1000); 232 | } 233 | 234 | initPlane(){ 235 | 236 | var camera = new PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 ); 237 | camera.position.z = 20; 238 | this.controls = new MapControls( camera, this.renderer.domElement, { 239 | target: new Plane(new Vector3(0,0,1), 0), 240 | mode: 'plane', 241 | minDistance: 2.0, 242 | maxDistance: 20 243 | }); 244 | 245 | var offset = 3; 246 | 247 | for(var x = 0; x < this.dims; x++){ 248 | for(var y = 0; y < this.dims; y++){ 249 | var geometry = new CubeGeometry(1, 1, 1); 250 | var material = new MeshNormalMaterial(); 251 | 252 | var mesh = new Mesh( geometry, material ); 253 | mesh.position.x += ((-0.5 * this.dims * offset) + (x * offset)); 254 | mesh.position.y += ((-0.5 * this.dims * offset) + (y * offset)); 255 | mesh.userData.selectable = true; 256 | this.meshes.push( mesh ); 257 | this.scene.add( mesh ); 258 | 259 | mesh.geometry.computeBoundingSphere(); 260 | } 261 | } 262 | } 263 | 264 | init () { 265 | this.renderer = new WebGLRenderer(); 266 | this.renderer.setPixelRatio( window.devicePixelRatio ); 267 | this.renderer.setSize( window.innerWidth, window.innerHeight ); 268 | 269 | this.container.appendChild( this.renderer.domElement ); 270 | 271 | window.addEventListener( 'resize', ()=>{ 272 | this.onWindowResize(); 273 | }, false ); 274 | 275 | this.renderer.domElement.addEventListener( 'mousedown', (_e) => {this.pick(_e)} ); 276 | this.renderer.domElement.addEventListener( 'dblclick', (_e) => {this.zoomTo(_e)} ); 277 | 278 | const cb = document.getElementById('toggleCamDebug'); 279 | cb.addEventListener('click', this.toggleDebugCamView.bind(this)); 280 | } 281 | 282 | zoomTo(){ 283 | if(!this.selectedObject) 284 | return; 285 | 286 | switch(this.mode){ 287 | case 'sphere': 288 | this.controls.zoomToFit( 289 | this.selectedObject, 290 | this.selectedObject.userData.zoom.center, 291 | this.selectedObject.userData.zoom.dims 292 | ); 293 | break; 294 | case 'plane': 295 | this.controls.zoomToFit( this.selectedObject ); 296 | break; 297 | } 298 | } 299 | 300 | deselect() { 301 | if(this.selectedObject){ 302 | switch (this.mode) { 303 | case 'sphere': 304 | this.selectedObject.parent.remove(this.selectedObject); 305 | this.selectedObject.geometry.dispose(); 306 | this.selectedObject.material.dispose(); 307 | break; 308 | case 'plane': 309 | this.selectedObject.material.dispose(); 310 | this.selectedObject.material = new MeshNormalMaterial(); 311 | break; 312 | } 313 | } 314 | 315 | this.selectedObject = null; 316 | } 317 | 318 | pick(event){ 319 | this.deselect(); 320 | 321 | const mouse = new Vector2(); 322 | 323 | mouse.x = ( event.clientX / this.renderer.domElement.clientWidth ) * 2 - 1; 324 | mouse.y = - ( event.clientY / this.renderer.domElement.clientHeight ) * 2 + 1; 325 | 326 | const raycaster = new Raycaster(); 327 | 328 | raycaster.setFromCamera(mouse, this.controls.camera); 329 | 330 | // calculate objects intersecting the picking ray 331 | const intersect = (raycaster.intersectObjects( this.scene.children, true )).filter(_int => { 332 | return _int.object.userData.selectable; 333 | })[0]; 334 | 335 | if(intersect !== undefined && intersect.face){ 336 | switch (this.mode) { 337 | case 'sphere': 338 | //create a new selectedObject from the triangle that was raycasted. 339 | const geo = new BufferGeometry(); 340 | const orig_geo = intersect.object.geometry; 341 | const orig_vtx_ar = orig_geo.getAttribute('position').array; 342 | const f = intersect.face; 343 | const new_vtx_ar = [f.a, f.b, f.c].map(_idx => { 344 | const vtx_ar = []; 345 | for(let i = 0; i < 3; i++){ 346 | vtx_ar.push(orig_vtx_ar[(_idx * 3) + i]); 347 | } 348 | return vtx_ar; 349 | }).flat(); 350 | 351 | geo.setIndex([0,1,2]); 352 | geo.addAttribute('position', new Float32BufferAttribute(Float32Array.from(new_vtx_ar), 3)); 353 | 354 | geo.computeBoundingSphere(); 355 | geo.computeVertexNormals(); 356 | geo.computeBoundingBox(); 357 | 358 | this.selectedObject = new Mesh(geo, new MeshBasicMaterial({ 359 | color: new Color(1,0,0), 360 | transparent: true, 361 | opacity: 0.5 362 | })); 363 | 364 | const projected = this.projectTriangleToPlane(new_vtx_ar); 365 | 366 | Object.assign(this.selectedObject.userData, { 367 | zoom: { 368 | center: projected.center, 369 | dims: projected.projection_size 370 | } 371 | }); 372 | 373 | this.scene.add(this.selectedObject); 374 | break; 375 | case 'plane': 376 | this.selectedObject = intersect.object; 377 | this.selectedObject.material.dispose(); 378 | this.selectedObject.material = new MeshBasicMaterial({ 379 | color: new Color(1,0,0), 380 | wireframe: true 381 | }); 382 | break; 383 | } 384 | 385 | }else{ 386 | this.deselect(); 387 | } 388 | 389 | } 390 | 391 | projectTriangleToPlane(verts_ar){ 392 | const centroid = this.findTriangleCentroid(verts_ar); 393 | 394 | const vecs = [0,1,2].map(_t => { 395 | return new Vector3().fromArray([0,1,2].map(_v => { 396 | return verts_ar[(_t*3)+_v]; 397 | })).sub(centroid); 398 | }); 399 | 400 | const norm = new Vector3().crossVectors(vecs[1], vecs[0]).normalize(); 401 | const right = new Vector3().crossVectors(new Vector3(0,1,0), norm).normalize(); 402 | const up = new Vector3().crossVectors(norm, right).normalize(); 403 | const prj_vecs = vecs.map(_v => { 404 | return new Vector2( 405 | _v.dot(right), 406 | _v.dot(up) 407 | ); 408 | }); 409 | 410 | const prj_bbox = new Box2().setFromPoints(prj_vecs); 411 | const prj_dims = new Vector2(); 412 | prj_bbox.getSize(prj_dims); 413 | 414 | const prj_center = new Vector2( 415 | prj_bbox.min.x + (prj_dims.x / 2), 416 | prj_bbox.min.y + (prj_dims.y / 2) 417 | ); 418 | 419 | const prj_delta = new Vector2().subVectors(prj_center, prj_vecs[0]); 420 | 421 | //translate the 3d centroid to the position of the 2d projected center via prj_delta, up, and right. 422 | const center = vecs[0].clone().add(centroid); 423 | center.add(up.clone().multiplyScalar(prj_delta.y)); 424 | center.add(right.clone().multiplyScalar(prj_delta.x)); 425 | 426 | return { 427 | center: center, 428 | projection: prj_vecs, 429 | projection_bbox: prj_bbox, 430 | projection_size: prj_dims 431 | }; 432 | 433 | } 434 | 435 | findTriangleCentroid (verts_ar) { 436 | let center = [0,0,0]; 437 | 438 | [0,1,2].forEach(_v => { 439 | [0,1,2].forEach(_d => { 440 | center[_d] += verts_ar[(_v*3) + _d]; 441 | }); 442 | }); 443 | 444 | center = center.map(_d => {return _d / 3;}); 445 | 446 | return (new Vector3()).fromArray(center); 447 | } 448 | 449 | onWindowResize(){ 450 | this.renderer.setSize( window.innerWidth, window.innerHeight ); 451 | this.controls.camera.aspect = this.renderer.domElement.clientWidth / this.renderer.domElement.clientHeight; 452 | this.controls.camera.updateProjectionMatrix(); 453 | this.renderer.setSize( this.renderer.domElement.clientWidth, this.renderer.domElement.clientHeight ); 454 | } 455 | 456 | animate(){ 457 | requestAnimationFrame( () => { 458 | this.animate(); 459 | } ); 460 | 461 | if(this.selectedObject){ 462 | const currentColor = this.selectedObject.material.color; 463 | this.selectedObjectTween += 0.025; 464 | currentColor.g = Math.abs(1 - (this.selectedObjectTween % 2)); 465 | } 466 | 467 | if(this.mode == 'plane'){ 468 | this.meshes.forEach(( mesh ) => { 469 | mesh.rotation.x += 0.005; 470 | mesh.rotation.y += 0.01; 471 | }); 472 | } 473 | 474 | this.controls.update(); 475 | this.renderer.render( this.scene, this.controls.camera ); 476 | } 477 | }; 478 | 479 | window.addEventListener('load', () => { 480 | window.demo = new MapControlsDemo('sphere'); 481 | }); -------------------------------------------------------------------------------- /dist/demo.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function s(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,s),o.l=!0,o.exports}s.m=e,s.c=t,s.d=function(e,t,i){s.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:i})},s.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,"a",t),t},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},s.p="/",s(s.s=4)}([function(e,t,s){e.exports=s(3)(0)},function(e,t,s){"use strict";s.r(t);var i=s(0);if("undefined"==typeof window){s(2)}t.default=class extends i.EventDispatcher{constructor(e,t,s){super(),this.camera=e,this.domElement=void 0!==t?t:window.document.body,this.enabled=!0,this.target,this.minDistance=1,this.maxDistance=100,this.enableZoom=!0,this.zoomSpeed=6,this.zoomDampingAlpha=.1,this.initialZoom=0,this.enablePan=!0,this.keyPanSpeed=50,this.keyZoomSpeed=this.zoomSpeed,this.panDampingAlpha=.1,this.enableKeys=!0,this.keys={PAN_LEFT:"ArrowLeft",PAN_UP:"ArrowUp",PAN_RIGHT:"ArrowRight",PAN_BOTTOM:"ArrowDown",ZOOM_IN:"]",ZOOM_OUT:"["},this.mouseButtons={ZOOM:i.MOUSE.MIDDLE,PAN:i.MOUSE.LEFT},Object.assign(this,s);let o=!1;if(void 0===this.mode)throw new Error("'mode' option must be set to either 'plane' or 'sphere'");switch(this.mode){case"plane":o=void 0!==this.target.normal&&void 0!==this.target.constant;break;case"sphere":o=void 0!==this.target.center&&void 0!==this.target.radius}if(!o)throw new Error("'target' option must be an instance of type THREE.Plane or THREE.Sphere");this._eventListeners={contextmenu:this._onContextMenu.bind(this),mousedown:this._onMouseDown.bind(this),mousewheel:this._onMouseWheel.bind(this),MozMousePixelScroll:this._onMouseWheel.bind(this),touchstart:this._onTouchStart.bind(this),touchend:this._onTouchEnd.bind(this),touchmove:this._onTouchMove.bind(this),keydown:this._onKeyDown.bind(this),mouseover:this._onMouseOver.bind(this),mouseout:this._onMouseOut.bind(this),mousemove:this._onMouseMove.bind(this),mouseup:this._onMouseUp.bind(this)},this._init()}_init(){if(this.target0=this.target.clone(),this.position0=this.camera.position.clone(),this.zoom0=this.camera.zoom,this._changeEvent={type:"change"},this._startEvent={type:"start"},this._endEvent={type:"end"},this._STATES={NONE:-1,DOLLY:1,PAN:2,TOUCH_DOLLY:4,TOUCH_PAN:5},0==this.target.distanceToPoint(this.camera.position))throw new Error("ORIENTATION_UNKNOWABLE: initial Camera position cannot intersect target plane.");this._state=this._STATES.NONE,this._mouse=new i.Vector2,this._finalTargetDistance=0,this._currentTargetDistance=0,this._panTarget=new i.Vector3(0,0,0),this._panCurrent=new i.Vector3(0,0,0),this._minZoomPosition=new i.Vector3,this._maxZoomPosition=new i.Vector3,this._panStart=new i.Vector2,this._panEnd=new i.Vector2,this._panDelta=new i.Vector2,this._dollyStart=new i.Vector2,this._dollyEnd=new i.Vector2,this._dollyDelta=new i.Vector2,this._camOrientation=new i.Vector2,this._zoomAlpha,this._screenWorldXform=Math.tan(this.camera.fov/2*Math.PI/180),this._straightDollyTrack(),this.camera.position.lerpVectors(this._minZoomPosition,this._maxZoomPosition,this.initialZoom),this._finalTargetDistance=this._currentTargetDistance=Math.abs(this.target.distanceToPoint(this.camera.position));const e=this._intersectCameraTarget();this.camera.lookAt(e.intersection),this._camOrientation=e.ray.direction.clone().normalize(),this._updateZoomAlpha(),["contextmenu","mousedown","mousewheel","MozMousePixelScroll","touchstart","touchend","touchmove","mouseover","mouseout","keydown"].forEach(e=>{this.domElement.addEventListener(e,this._eventListeners[e],!1)}),"CANVAS"!=this.domElement.tagName||this.domElement.getAttribute("tabindex")||this.domElement.setAttribute("tabindex","1"),this.update()}_intersectCameraTarget(){let e,t=new i.Vector3;switch(this.mode){case"plane":const s=new i.Vector3;this.target.projectPoint(this.camera.position,s),(e=new i.Ray(this.camera.position,(new i.Vector3).subVectors(s,this.camera.position).normalize())).intersectPlane(this.target,t);break;case"sphere":(e=new i.Ray(this.camera.position,(new i.Vector3).subVectors(this.target.center,this.camera.position).normalize())).intersectSphere(this.target,t)}return{intersection:t,ray:e}}_straightDollyTrack(){this._updateDollyTrack(this._intersectCameraTarget().ray)}getZoomAlpha(){return this._zoomAlpha}reset(){this.target.copy(this.target0),this.camera.position.copy(this.position0),this.camera.zoom=this.zoom0,this.camera.updateProjectionMatrix(),this._init(),this.dispatchEvent(this._changeEvent),this.update(),this._state=this._STATES.NONE}update(){const e=new i.Vector3,t=new i.Vector3,s=this.camera.position;switch(t.copy(this._panCurrent),this._panCurrent.lerp(this._panTarget,this.panDampingAlpha),e.subVectors(this._panCurrent,t),this.mode){case"plane":this._maxZoomPosition.add(e),this._minZoomPosition.add(e);break;case"sphere":const s=new i.Vector3,o=new i.Quaternion;o.setFromAxisAngle(s.setFromMatrixColumn(this.camera.matrix,1),e.x),this._maxZoomPosition.applyQuaternion(o),this._minZoomPosition.applyQuaternion(o),o.setFromAxisAngle(s.setFromMatrixColumn(this.camera.matrix,0),e.y),this._maxZoomPosition.applyQuaternion(o),this._minZoomPosition.applyQuaternion(o),o.setFromAxisAngle(new i.Vector3(0,1,0),e.z),this._maxZoomPosition.applyQuaternion(o),this._minZoomPosition.applyQuaternion(o)}s.lerpVectors(this._minZoomPosition,this._maxZoomPosition,this._updateZoomAlpha()),"sphere"==this.mode&&this.camera.lookAt(this.target.center)}dispose(){Object.keys(this._eventListeners).forEach(e=>{this.domElement.removeEventListener(e,this._eventListeners[e],!1)})}zoomToFit(e,t,s){if(void 0===t&&(t=e.geometry.boundingSphere.center.clone()),t=e.localToWorld(t.clone()),void 0===s){const t=2*e.geometry.boundingSphere.radius;s=new i.Vector2(t,t)}switch(this.mode){case"plane":this._panTarget.copy(t),this._panCurrent.copy(this._intersectCameraTarget().intersection);break;case"sphere":const e=this._sphericalCoordinatesFrom(t),s=this._sphericalCoordinatesFrom(this.camera.position),o=(new i.Vector2).subVectors(e,s);Math.abs(o.x)>Math.PI&&(o.x=-Math.abs(o.x)/o.x*(2*Math.PI-Math.abs(o.x))),this._panTarget.add(new i.Vector3(0,-o.y,o.x))}this._straightDollyTrack();const o=this.camera.fov*(Math.PI/180),n=2*Math.atan(Math.tan(o/2)*this.camera.aspect),a=s.x/s.y;this._finalTargetDistance=(a>this.camera.aspect?s.x:s.y)/2/Math.tan((a>this.camera.aspect?n:o)/2)}targetAreaVisible(){let e,t,s,o;switch(this.mode){case"plane":var n=new i.Ray(this.camera.position,this._camOrientation).distanceToPlane(this.target);o=this.camera.position.clone(),s=(t=this._screenWorldXform*n)*this.camera.aspect,e=new i.Box2(new i.Vector2(o.x-s,o.y-t),new i.Vector2(o.x+s,o.y+t));break;case"sphere":const a=(new i.Vector3).subVectors(this.target.center,this.camera.position);o=this._sphericalCoordinatesFrom(this.camera.position);const r=Math.PI/2,h=a.length();t=this._screenWorldXform*(h/this.target.radius-1),t=Math.min(t,r);const c=this.target.radius*Math.cos(o.y-r);s=t*this.camera.aspect*(this.target.radius/c),s=Math.min(s,r),e=new i.Box2(new i.Vector2(o.x-s-r,o.y-t-r),new i.Vector2(o.x+s-r,o.y+t-r)),["min","max"].forEach(t=>{e[t].x=e[t].x>Math.PI?-2*Math.PI+e[t].x:e[t].x})}return e}targetAreaVisibleDeg(){let e=this.targetAreaVisible();return"sphere"==this.mode&&(e.min.x=e.min.x*(180/Math.PI),e.min.y=e.min.y*(180/Math.PI),e.max.x=e.max.x*(180/Math.PI),e.max.y=e.max.y*(180/Math.PI)),e}_sphericalCoordinatesFrom(e){const t=(new i.Vector3).subVectors(this.target.center,e),s=new i.Vector3(t.x,0,t.z),o=(new i.Vector3,new i.Vector2(s.angleTo(new i.Vector3(1,0,0)),t.angleTo(new i.Vector3(0,1,0))));return o.x=t.z>0?2*Math.PI-o.x:o.x,o}_updateZoomAlpha(){this._finalTargetDistance=Math.max(this.minDistance,Math.min(this.maxDistance,this._finalTargetDistance));var e=this._currentTargetDistance-this._finalTargetDistance,t=this.zoomDampingAlpha;return this._currentTargetDistance-=e*t,this._zoomAlpha=Math.abs(Math.round(1e5*(1-(this._currentTargetDistance-this.minDistance)/(this.maxDistance-this.minDistance)))/1e5),this._zoomAlpha}_updateDollyTrack(e){let t=new i.Vector3;switch(this.mode){case"plane":e.intersectPlane(this.target,t);break;case"sphere":e.intersectSphere(this.target,t)}t&&(this._maxZoomPosition.addVectors(t,(new i.Vector3).subVectors(this.camera.position,t).normalize().multiplyScalar(this.minDistance)),this._minZoomPosition.copy(this._calculateMinZoom(this.camera.position,t)),this._finalTargetDistance=this._currentTargetDistance=t.clone().sub(this.camera.position).length())}_getZoomScale(e){return e=e||this.zoomSpeed,Math.pow(.95,e)}_panLeft(e,t){var s=new i.Vector3;switch(this.mode){case"sphere":s.set(-e,0,0);break;case"plane":s.setFromMatrixColumn(t,0),s.multiplyScalar(-e)}this._panTarget.add(s)}_panUp(e,t){var s=new i.Vector3;switch(this.mode){case"sphere":s.set(0,-e,0);break;case"plane":s.setFromMatrixColumn(t,1),s.multiplyScalar(e)}this._panTarget.add(s)}_pan(e,t){var s,o=this.domElement,n=new i.Ray(this.camera.position,this._camOrientation);switch(this.mode){case"plane":s=this._screenWorldXform*n.distanceToPlane(this.target);break;case"sphere":const e=(new i.Vector3).subVectors(this.camera.position,this.target.center);s=this._screenWorldXform*(e.length()/this.target.radius-1)}this._panLeft(2*e*s/o.clientHeight,this.camera.matrix),this._panUp(2*t*s/o.clientHeight,this.camera.matrix)}_dollyIn(e){this._cameraOfKnownType()?this._finalTargetDistance/=e:(console.warn("WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_dollyOut(e){this._cameraOfKnownType()?this._finalTargetDistance*=e:(console.warn("WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_cameraOfKnownType(){return"PerspectiveCamera"===this.camera.type}_handleUpdateDollyTrackMouse(e){var t=this._mouse.clone();if(this._mouse.set(e.offsetX/this.domElement.clientWidth*2-1,-e.offsetY/this.domElement.clientHeight*2+1),!t.equals(this._mouse)){var s=new i.Raycaster;s.setFromCamera(this._mouse,this.camera),this._updateDollyTrack(s.ray)}}_handleMouseDownDolly(e){this._handleUpdateDollyTrackMouse(e),this._dollyStart.set(e.offsetX,e.offsetY)}_handleMouseDownPan(e){this._panStart.set(e.offsetX,e.offsetY)}_handleMouseMoveDolly(e){this._handleUpdateDollyTrackMouse(e),this._dollyEnd.set(e.offsetX,e.offsetY),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyIn(this._getZoomScale()):this._dollyDelta.y<0&&this._dollyOut(this._getZoomScale()),this._dollyStart.copy(this._dollyEnd),this.update()}_handleMouseMovePan(e){this._panEnd.set(e.offsetX,e.offsetY),this._panDelta.subVectors(this._panEnd,this._panStart),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleMouseUp(e){}_calculateMinZoom(e,t){return t.clone().add(e.clone().sub(t).normalize().multiplyScalar(this.maxDistance))}_handleMouseWheel(e){this._handleUpdateDollyTrackMouse(e);var t=0;void 0!==e.wheelDelta?t=e.wheelDelta:void 0!==e.detail&&(t=-e.detail),t>0?this._dollyOut(this._getZoomScale()):t<0&&this._dollyIn(this._getZoomScale()),this.update()}_handleKeyDown(e){switch(e.key){case this.keys.PAN_UP:this._pan(0,this.keyPanSpeed),this.update();break;case this.keys.PAN_BOTTOM:this._pan(0,-this.keyPanSpeed),this.update();break;case this.keys.PAN_LEFT:this._pan(this.keyPanSpeed,0),this.update();break;case this.keys.PAN_RIGHT:this._pan(-this.keyPanSpeed,0),this.update();break;case this.keys.ZOOM_IN:this._dollyIn(this._getZoomScale(this.keyZoomSpeed)),this.update();break;case this.keys.ZOOM_OUT:this._dollyOut(this._getZoomScale(this.keyZoomSpeed)),this.update()}}_handleUpdateDollyTrackTouch(e){var t=new i.Vector2,s=e.touches[0].pageX-e.touches[1].pageX,o=e.touches[0].pageY-e.touches[1].pageY;t.x=e.touches[0].pageX+s/2,t.y=e.touches[0].pageY+o/2;var n=new i.Vector2;n.x=t.x/domElement.clientWidth*2-1,n.y=-t.y/domElement.clientHeight*2+1,this._updateDollyTrack(n)}_handleTouchStartDolly(e){this._handleUpdateDollyTrackTouch(e);var t=e.touches[0].pageX-e.touches[1].pageX,s=e.touches[0].pageY-e.touches[1].pageY,i=Math.sqrt(t*t+s*s);this._dollyStart.set(0,i)}_handleTouchStartPan(e){this._panStart.set(e.touches[0].pageX,e.touches[0].pageY)}_handleTouchMoveDolly(e){this._handleUpdateDollyTrackTouch(e);var t=e.touches[0].pageX-e.touches[1].pageX,s=e.touches[0].pageY-e.touches[1].pageY,i=Math.sqrt(t*t+s*s);this._dollyEnd.set(0,i),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyOut(this._getZoomScale()):this._dollyDelta.y<0&&this._dollyIn(this._getZoomScale()),this._dollyStart.copy(this._dollyEnd),this.update()}_handleTouchMovePan(e){this._panEnd.set(e.touches[0].pageX,e.touches[0].pageY),this._panDelta.subVectors(this._panEnd,this._panStart),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleTouchEnd(e){}_onMouseDown(e){if(!1!==this.enabled){if(e.preventDefault(),e.button===this.mouseButtons.ZOOM){if(!1===this.enableZoom)return;this._handleMouseDownDolly(e),this._state=this._STATES.DOLLY}else if(e.button===this.mouseButtons.PAN){if(!1===this.enablePan)return;this._handleMouseDownPan(e),this._state=this._STATES.PAN}this._state!==this._STATES.NONE&&(this.domElement.addEventListener("mousemove",this._eventListeners.mousemove,!1),this.domElement.addEventListener("mouseup",this._eventListeners.mouseup,!1),this.dispatchEvent(this._startEvent))}}_onMouseMove(e){if(!1!==this.enabled)if(e.preventDefault(),this._state===this._STATES.DOLLY){if(!1===this.enableZoom)return;this._handleMouseMoveDolly(e)}else if(this._state===this._STATES.PAN){if(!1===this.enablePan)return;this._handleMouseMovePan(e)}}_onMouseUp(e){!1!==this.enabled&&(this._handleMouseUp(e),this.domElement.removeEventListener("mousemove",this._eventListeners.mousemove,!1),this.domElement.removeEventListener("mouseup",this._eventListeners.mouseup,!1),this.dispatchEvent(this._endEvent),this._state=this._STATES.NONE)}_onMouseWheel(e){!1!==this.enabled&&!1!==this.enableZoom&&this._state===this._STATES.NONE&&(e.preventDefault(),e.stopPropagation(),this._handleMouseWheel(e),this.dispatchEvent(this._startEvent),this.dispatchEvent(this._endEvent))}_onKeyDown(e){!1!==this.enabled&&!1!==this.enableKeys&&!1!==this.enablePan&&this._handleKeyDown(e)}_onTouchStart(e){if(!1!==this.enabled){switch(e.touches.length){case 1:if(!1===this.enablePan)return;this._handleTouchStartPan(e),this._state=this._STATES.TOUCH_PAN;break;case 2:if(!1===this.enableZoom)return;this._handleTouchStartDolly(e),this._state=this._STATES.TOUCH_DOLLY;break;default:this._state=this._STATES.NONE}this._state!==this._STATES.NONE&&this.dispatchEvent(this._startEvent)}}_onTouchMove(e){if(!1!==this.enabled)switch(e.preventDefault(),e.stopPropagation(),e.touches.length){case 1:if(!1===this.enablePan)return;if(this._state!==this._STATES.TOUCH_PAN)return;this._handleTouchMovePan(e);break;case 2:if(!1===this.enableZoom)return;if(this._state!==this._STATES.TOUCH_DOLLY)return;this._handleTouchMoveDolly(e);break;default:this._state=this._STATES.NONE}}_onTouchEnd(e){!1!==this.enabled&&(this._handleTouchEnd(e),this.dispatchEvent(this._endEvent),this._state=this._STATES.NONE)}_onContextMenu(e){e.preventDefault()}_onMouseOver(e){return this.domElement.focus(),!1}_onMouseOut(e){return this.domElement.blur(),!1}}},function(e,t){e.exports={console:{log:function(){}},document:{body:{clientWidth:1920,clientHeight:1080,addEventListener:function(){},removeEventListener:function(){}}}}},function(e,t){e.exports=vendor},function(e,t,s){"use strict";s.r(t);var i=s(0),o=s(1);const n=10;window.addEventListener("load",()=>{window.demo=new class{constructor(e){this.container=document.body,this.scene=new i.Scene,this.renderer=null,this.meshes=[],this.dims=10,this.selectedObjectTween=0,this.selectedObject=null,this.controls,this.mode,this.debugCamViewInterval,this.camViewMesh,this.camViewLines,this.init(),this.setMode(e),this.animate()}setMode(e){this.deselect(),this.mode=e;const t={sphere:document.getElementById("sphere-link"),plane:document.getElementById("plane-link")};switch(t[this.mode].style.display="none",t["plane"==this.mode?"sphere":"plane"].style.display="inline-block",this.meshes.concat([this.camViewLines,this.camViewMesh]).forEach(e=>{void 0!==e&&(this.scene.remove(e),e.geometry.dispose())}),this.camViewLines=this.camViewMesh=void 0,this.mode){case"sphere":this.initSphere();break;case"plane":this.initPlane()}}initSphere(){var e=new i.PerspectiveCamera(45,window.innerWidth/window.innerHeight,1,1e3);e.position.z=40,this.controls=new o.default(e,this.renderer.domElement,{target:new i.Sphere(new i.Vector3(0,0,0),n),mode:"sphere",minDistance:1,maxDistance:e.position.z});const t=[],s=new i.SphereBufferGeometry(n,this.dims,this.dims);s.computeBoundingSphere();const a=s.getAttribute("position").array;for(var r=0;r{const e=this.controls.targetAreaVisible();let t,s;switch(console.log(`${e.min.x}, ${e.min.y}, ${e.max.x}, ${e.max.y}`),s=new i.Vector3(0,0,0),this.mode){case"sphere":let o=Math.abs(e.max.x-e.min.x);o>Math.PI&&(o=Math.abs(e.max.x+2*Math.PI-e.min.x)),t=new i.SphereBufferGeometry(n,this.dims,this.dims,e.min.x+Math.PI/2,o,-e.max.y+Math.PI/2,Math.abs(e.max.y-e.min.y));break;case"plane":t=new i.PlaneBufferGeometry(e.max.x-e.min.x,e.max.y-e.min.y,this.dims,this.dims),s.copy(this.controls.camera.position),s.z=0}void 0==this.camViewMesh?(this.camViewMesh=new i.Mesh(t,new i.MeshBasicMaterial({color:new i.Color(1,0,0),side:i.DoubleSide,transparent:!0,opacity:.5})),this.camViewLines=new i.Mesh(t,new i.MeshBasicMaterial({color:new i.Color(1,0,0),wireframe:!0})),this.scene.add(this.camViewMesh),this.scene.add(this.camViewLines)):(this.camViewMesh.geometry.copy(t),this.camViewLines.geometry.copy(t),t.dispose()),this.camViewMesh.geometry.computeBoundingSphere(),this.camViewMesh.position.copy(s),this.camViewLines.position.copy(s)},1e3)}initPlane(){var e=new i.PerspectiveCamera(70,window.innerWidth/window.innerHeight,1,1e3);e.position.z=20,this.controls=new o.default(e,this.renderer.domElement,{target:new i.Plane(new i.Vector3(0,0,1),0),mode:"plane",minDistance:2,maxDistance:20});for(var t=0;t{this.onWindowResize()},!1),this.renderer.domElement.addEventListener("mousedown",e=>{this.pick(e)}),this.renderer.domElement.addEventListener("dblclick",e=>{this.zoomTo(e)}),document.getElementById("toggleCamDebug").addEventListener("click",this.toggleDebugCamView.bind(this))}zoomTo(){if(this.selectedObject)switch(this.mode){case"sphere":this.controls.zoomToFit(this.selectedObject,this.selectedObject.userData.zoom.center,this.selectedObject.userData.zoom.dims);break;case"plane":this.controls.zoomToFit(this.selectedObject)}}deselect(){if(this.selectedObject)switch(this.mode){case"sphere":this.selectedObject.parent.remove(this.selectedObject),this.selectedObject.geometry.dispose(),this.selectedObject.material.dispose();break;case"plane":this.selectedObject.material.dispose(),this.selectedObject.material=new i.MeshNormalMaterial}this.selectedObject=null}pick(e){this.deselect();const t=new i.Vector2;t.x=e.clientX/this.renderer.domElement.clientWidth*2-1,t.y=-e.clientY/this.renderer.domElement.clientHeight*2+1;const s=new i.Raycaster;s.setFromCamera(t,this.controls.camera);const o=s.intersectObjects(this.scene.children,!0).filter(e=>e.object.userData.selectable)[0];if(void 0!==o&&o.face)switch(this.mode){case"sphere":const e=new i.BufferGeometry,t=o.object.geometry.getAttribute("position").array,s=o.face,n=[s.a,s.b,s.c].map(e=>{const s=[];for(let i=0;i<3;i++)s.push(t[3*e+i]);return s}).flat();e.setIndex([0,1,2]),e.addAttribute("position",new i.Float32BufferAttribute(Float32Array.from(n),3)),e.computeBoundingSphere(),e.computeVertexNormals(),e.computeBoundingBox(),this.selectedObject=new i.Mesh(e,new i.MeshBasicMaterial({color:new i.Color(1,0,0),transparent:!0,opacity:.5}));const a=this.projectTriangleToPlane(n);Object.assign(this.selectedObject.userData,{zoom:{center:a.center,dims:a.projection_size}}),this.scene.add(this.selectedObject);break;case"plane":this.selectedObject=o.object,this.selectedObject.material.dispose(),this.selectedObject.material=new i.MeshBasicMaterial({color:new i.Color(1,0,0),wireframe:!0})}else this.deselect()}projectTriangleToPlane(e){const t=this.findTriangleCentroid(e),s=[0,1,2].map(s=>(new i.Vector3).fromArray([0,1,2].map(t=>e[3*s+t])).sub(t)),o=(new i.Vector3).crossVectors(s[1],s[0]).normalize(),n=(new i.Vector3).crossVectors(new i.Vector3(0,1,0),o).normalize(),a=(new i.Vector3).crossVectors(o,n).normalize(),r=s.map(e=>new i.Vector2(e.dot(n),e.dot(a))),h=(new i.Box2).setFromPoints(r),c=new i.Vector2;h.getSize(c);const l=new i.Vector2(h.min.x+c.x/2,h.min.y+c.y/2),d=(new i.Vector2).subVectors(l,r[0]),m=s[0].clone().add(t);return m.add(a.clone().multiplyScalar(d.y)),m.add(n.clone().multiplyScalar(d.x)),{center:m,projection:r,projection_bbox:h,projection_size:c}}findTriangleCentroid(e){let t=[0,0,0];return[0,1,2].forEach(s=>{[0,1,2].forEach(i=>{t[i]+=e[3*s+i]})}),t=t.map(e=>e/3),(new i.Vector3).fromArray(t)}onWindowResize(){this.renderer.setSize(window.innerWidth,window.innerHeight),this.controls.camera.aspect=this.renderer.domElement.clientWidth/this.renderer.domElement.clientHeight,this.controls.camera.updateProjectionMatrix(),this.renderer.setSize(this.renderer.domElement.clientWidth,this.renderer.domElement.clientHeight)}animate(){if(requestAnimationFrame(()=>{this.animate()}),this.selectedObject){const e=this.selectedObject.material.color;this.selectedObjectTween+=.025,e.g=Math.abs(1-this.selectedObjectTween%2)}"plane"==this.mode&&this.meshes.forEach(e=>{e.rotation.x+=.005,e.rotation.y+=.01}),this.controls.update(),this.renderer.render(this.scene,this.controls.camera)}}("sphere")})}]); 2 | //# sourceMappingURL=demo.js.map -------------------------------------------------------------------------------- /src/three-map-controls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //Alex Pilafian 2016-2019 - sikanrong@gmail.com 4 | 5 | import { 6 | Box2, 7 | Quaternion, 8 | EventDispatcher, 9 | Vector2, 10 | Vector3, 11 | Raycaster, 12 | Ray, 13 | MOUSE 14 | } from 'three' 15 | 16 | //test stubs 17 | if(typeof window == 'undefined'){ 18 | let window = require('../test/stub_dom'); 19 | } 20 | 21 | class MapControls extends EventDispatcher{ 22 | 23 | constructor(camera, domElement, options){ 24 | super(); 25 | 26 | this.camera = camera; 27 | 28 | //Object to use for listening for keyboard/mouse events 29 | this.domElement = ( domElement !== undefined ) ? domElement : window.document.body; 30 | 31 | // Set to false to disable this control (Disables all input events) 32 | this.enabled = true; 33 | 34 | // Must be set to instance of Plane or Sphere 35 | this.target; 36 | 37 | // How far you can dolly in and out 38 | this.minDistance = 1; //probably should never be 0 39 | this.maxDistance = 100; 40 | 41 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 42 | // Set to false to disable zooming 43 | this.enableZoom = true; 44 | this.zoomSpeed = 6.0; 45 | this.zoomDampingAlpha = 0.1; 46 | this.initialZoom = 0; //start zoomed all the way out unless set in options. 47 | 48 | // Set to false to disable panning 49 | this.enablePan = true; 50 | this.keyPanSpeed = 50.0; // pixels moved per arrow key push 51 | this.keyZoomSpeed = this.zoomSpeed; // keyboard zoom speed, defaults to mouse-wheel zoom speed 52 | this.panDampingAlpha = 0.1; 53 | 54 | // Set to false to disable use of the keys 55 | this.enableKeys = true; 56 | 57 | // The four arrow keys, and two zoom keys 58 | this.keys = { 59 | PAN_LEFT: "ArrowLeft", 60 | PAN_UP: "ArrowUp", 61 | PAN_RIGHT: "ArrowRight", 62 | PAN_BOTTOM: "ArrowDown", 63 | ZOOM_IN: "]", 64 | ZOOM_OUT: "[" 65 | }; 66 | 67 | // Mouse buttons 68 | this.mouseButtons = { ZOOM: MOUSE.MIDDLE, PAN: MOUSE.LEFT }; 69 | 70 | //Copy options from parameters 71 | Object.assign(this, options); 72 | let isTargetValid = false; 73 | 74 | if(this.mode === undefined){ 75 | throw new Error('\'mode\' option must be set to either \'plane\' or \'sphere\''); 76 | } 77 | 78 | switch(this.mode){ 79 | case 'plane': 80 | isTargetValid = (this.target.normal !== undefined && this.target.constant !== undefined); 81 | break; 82 | case 'sphere': 83 | isTargetValid = (this.target.center !== undefined && this.target.radius !== undefined); 84 | break; 85 | } 86 | 87 | if(!isTargetValid){ 88 | throw new Error('\'target\' option must be an instance of type THREE.Plane or THREE.Sphere'); 89 | } 90 | 91 | this._eventListeners = { 92 | 'contextmenu': this._onContextMenu.bind(this), 93 | 'mousedown': this._onMouseDown.bind(this), 94 | 'mousewheel': this._onMouseWheel.bind(this), 95 | 'MozMousePixelScroll': this._onMouseWheel.bind(this), 96 | 'touchstart': this._onTouchStart.bind(this), 97 | 'touchend': this._onTouchEnd.bind(this), 98 | 'touchmove': this._onTouchMove.bind(this), 99 | 'keydown': this._onKeyDown.bind(this), 100 | 'mouseover': this._onMouseOver.bind(this), 101 | 'mouseout': this._onMouseOut.bind(this), 102 | 'mousemove': this._onMouseMove.bind(this), 103 | 'mouseup': this._onMouseUp.bind(this) 104 | }; 105 | 106 | this._init(); 107 | } 108 | 109 | _init (){ 110 | 111 | this.target0 = this.target.clone(); 112 | this.position0 = this.camera.position.clone(); 113 | this.zoom0 = this.camera.zoom; 114 | this._changeEvent = { type: 'change' }; 115 | this._startEvent = { type: 'start' }; 116 | this._endEvent = { type: 'end' }; 117 | 118 | this._STATES = { NONE : - 1, DOLLY : 1, PAN : 2, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; 119 | 120 | if(this.target.distanceToPoint(this.camera.position) == 0){ 121 | throw new Error("ORIENTATION_UNKNOWABLE: initial Camera position cannot intersect target plane."); 122 | } 123 | 124 | this._state = this._STATES.NONE; 125 | 126 | this._mouse = new Vector2(); 127 | 128 | this._finalTargetDistance = 0; 129 | this._currentTargetDistance = 0; 130 | 131 | this._panTarget = new Vector3(0,0,0); 132 | this._panCurrent = new Vector3(0,0,0); 133 | 134 | this._minZoomPosition = new Vector3(); 135 | this._maxZoomPosition = new Vector3(); 136 | 137 | this._panStart = new Vector2(); 138 | this._panEnd = new Vector2(); 139 | this._panDelta = new Vector2(); 140 | 141 | this._dollyStart = new Vector2(); 142 | this._dollyEnd = new Vector2(); 143 | this._dollyDelta = new Vector2(); 144 | 145 | this._camOrientation = new Vector2(); 146 | 147 | this._zoomAlpha; 148 | 149 | this._screenWorldXform = Math.tan( ( this.camera.fov / 2 ) * Math.PI / 180.0 ); 150 | 151 | //establish initial camera orientation based on position w.r.t. _this.target plane 152 | this._straightDollyTrack(); 153 | 154 | this.camera.position.lerpVectors(this._minZoomPosition, this._maxZoomPosition, this.initialZoom); 155 | this._finalTargetDistance = this._currentTargetDistance = Math.abs(this.target.distanceToPoint(this.camera.position)); 156 | 157 | const res = this._intersectCameraTarget(); 158 | this.camera.lookAt(res.intersection); //set the orientation of the camera towards the map. 159 | this._camOrientation = res.ray.direction.clone().normalize(); 160 | 161 | this._updateZoomAlpha(); 162 | 163 | //Assign event listeners 164 | 165 | [ 166 | 'contextmenu', 167 | 'mousedown', 168 | 'mousewheel', 169 | 'MozMousePixelScroll', 170 | 'touchstart', 171 | 'touchend', 172 | 'touchmove', 173 | 'mouseover', 174 | 'mouseout', 175 | 'keydown' 176 | ].forEach(_e => { 177 | this.domElement.addEventListener(_e, this._eventListeners[_e], false); 178 | }); 179 | 180 | if(this.domElement.tagName == 'CANVAS' && 181 | !this.domElement.getAttribute('tabindex')){ 182 | //if we're dealing with a canvas element which has no tabindex, 183 | //give it one so that it may recieve keyboard focus 184 | this.domElement.setAttribute('tabindex', '1'); 185 | } 186 | 187 | this.update(); 188 | } 189 | 190 | _intersectCameraTarget(){ 191 | let intersection = new Vector3(); 192 | let ray; 193 | 194 | switch(this.mode){ 195 | case 'plane': 196 | const coplanar = new Vector3(); 197 | this.target.projectPoint(this.camera.position, coplanar); 198 | ray = new Ray(this.camera.position, new Vector3().subVectors(coplanar, this.camera.position).normalize()); 199 | ray.intersectPlane(this.target, intersection); 200 | break; 201 | case 'sphere': 202 | ray = new Ray(this.camera.position, (new Vector3()).subVectors(this.target.center, this.camera.position).normalize()); 203 | ray.intersectSphere(this.target, intersection); 204 | break; 205 | } 206 | 207 | return { 208 | intersection: intersection, 209 | ray: ray 210 | } 211 | } 212 | 213 | _straightDollyTrack(){ 214 | this._updateDollyTrack(this._intersectCameraTarget().ray); 215 | } 216 | 217 | getZoomAlpha () { 218 | return this._zoomAlpha; 219 | } 220 | 221 | reset () { 222 | 223 | this.target.copy( this.target0 ); 224 | this.camera.position.copy( this.position0 ); 225 | this.camera.zoom = this.zoom0; 226 | 227 | this.camera.updateProjectionMatrix(); 228 | 229 | this._init(); //reinit 230 | 231 | this.dispatchEvent( this._changeEvent ); 232 | 233 | this.update(); 234 | 235 | this._state = this._STATES.NONE; 236 | 237 | }; 238 | 239 | // this method is exposed, but perhaps it would be better if we can make it private... 240 | update () { 241 | const panDelta = new Vector3(); 242 | const oldPanCurrent = new Vector3(); 243 | const position = this.camera.position; 244 | 245 | // move target to panned location 246 | oldPanCurrent.copy(this._panCurrent); 247 | this._panCurrent.lerp( this._panTarget, this.panDampingAlpha ); 248 | panDelta.subVectors(this._panCurrent, oldPanCurrent); 249 | 250 | switch (this.mode) { 251 | case 'plane': 252 | this._maxZoomPosition.add(panDelta); 253 | this._minZoomPosition.add(panDelta); 254 | break; 255 | case 'sphere': 256 | const v = new Vector3(); 257 | const quat = new Quaternion(); 258 | 259 | quat.setFromAxisAngle(v.setFromMatrixColumn( this.camera.matrix, 1 ), panDelta.x); 260 | 261 | this._maxZoomPosition.applyQuaternion(quat); 262 | this._minZoomPosition.applyQuaternion(quat); 263 | 264 | quat.setFromAxisAngle(v.setFromMatrixColumn( this.camera.matrix, 0 ), panDelta.y); 265 | 266 | this._maxZoomPosition.applyQuaternion(quat); 267 | this._minZoomPosition.applyQuaternion(quat); 268 | 269 | //panDelta.z is only used for zoomToFit 270 | //all pan operations rotate around the camera's MatrixColumn axes, while zoomToFit needs to 271 | //rotate about the world Y-axis 272 | quat.setFromAxisAngle(new Vector3(0,1,0), panDelta.z); 273 | this._maxZoomPosition.applyQuaternion(quat); 274 | this._minZoomPosition.applyQuaternion(quat); 275 | 276 | break; 277 | } 278 | 279 | position.lerpVectors(this._minZoomPosition, this._maxZoomPosition, this._updateZoomAlpha()); 280 | 281 | if(this.mode == 'sphere'){ 282 | this.camera.lookAt(this.target.center); 283 | } 284 | } 285 | 286 | dispose () { 287 | Object.keys(this._eventListeners).forEach(_e =>{ 288 | this.domElement.removeEventListener(_e, this._eventListeners[_e], false); 289 | }); 290 | }; 291 | 292 | zoomToFit (mesh, center, dims){ 293 | 294 | if(center === undefined){ 295 | center = mesh.geometry.boundingSphere.center.clone(); 296 | } 297 | 298 | center = mesh.localToWorld(center.clone()); 299 | 300 | if(dims === undefined){ 301 | const diameter = (mesh.geometry.boundingSphere.radius * 2); 302 | dims = new Vector2( 303 | diameter, 304 | diameter 305 | ); 306 | } 307 | 308 | switch(this.mode){ 309 | case 'plane': 310 | this._panTarget.copy(center); 311 | this._panCurrent.copy(this._intersectCameraTarget().intersection); 312 | break; 313 | case 'sphere': 314 | const targetCoord = this._sphericalCoordinatesFrom(center); 315 | const camCoord = this._sphericalCoordinatesFrom(this.camera.position); 316 | const delta = new Vector2().subVectors(targetCoord, camCoord); 317 | 318 | //Handle wrapping around the antimeridian; the line of 2π (or 0) radians 319 | if(Math.abs(delta.x) > Math.PI){ 320 | delta.x = (-Math.abs(delta.x) / delta.x) * ((Math.PI * 2) - Math.abs(delta.x)); 321 | } 322 | 323 | this._panTarget.add(new Vector3(0, -delta.y, delta.x)); 324 | break; 325 | } 326 | 327 | this._straightDollyTrack(); 328 | 329 | const vFOV = this.camera.fov * (Math.PI / 180); 330 | const hFOV = 2 * Math.atan( Math.tan( vFOV / 2 ) * this.camera.aspect ); 331 | const obj_aspect = dims.x / dims.y; 332 | 333 | this._finalTargetDistance = ((((obj_aspect > this.camera.aspect)? dims.x : dims.y) / 2) / Math.tan(((obj_aspect > this.camera.aspect)? hFOV : vFOV) / 2)); 334 | 335 | 336 | }; 337 | 338 | //returns a bounding box denoting the visible target area 339 | targetAreaVisible(){ 340 | 341 | let bbox, vOffset, hOffset, center; 342 | 343 | switch(this.mode){ 344 | case 'plane': 345 | var ray = new Ray(this.camera.position, this._camOrientation); 346 | var depth = ray.distanceToPlane(this.target); 347 | 348 | center = this.camera.position.clone(); 349 | 350 | vOffset = this._screenWorldXform * depth; 351 | hOffset = vOffset * this.camera.aspect; 352 | 353 | bbox = new Box2( 354 | new Vector2(center.x - hOffset, center.y - vOffset), 355 | new Vector2(center.x + hOffset, center.y + vOffset) 356 | ); 357 | 358 | break; 359 | case 'sphere': 360 | const cam_pos = (new Vector3()).subVectors(this.target.center, this.camera.position); 361 | center = this._sphericalCoordinatesFrom(this.camera.position); 362 | 363 | const halfPi = Math.PI / 2; 364 | 365 | const d = cam_pos.length(); 366 | 367 | //Derived from solving the Haversine formula for Phi_2 when all other variables 368 | //(d, r, Theta_1, Theta_2, Phi_1) are given 369 | vOffset = this._screenWorldXform * ((d / this.target.radius) - 1); 370 | vOffset = Math.min(vOffset, halfPi); 371 | 372 | //Account for the aspect ratio of the screen, and the deformation of the sphere 373 | const r = this.target.radius * Math.cos(center.y - halfPi); 374 | hOffset = vOffset * this.camera.aspect * ( this.target.radius / r); 375 | hOffset = Math.min(hOffset, halfPi); 376 | 377 | bbox = new Box2( 378 | new Vector2(center.x - hOffset - halfPi, center.y - vOffset - halfPi), 379 | new Vector2(center.x + hOffset - halfPi, center.y + vOffset - halfPi) 380 | ); 381 | 382 | ['min', 'max'].forEach(_mm => { 383 | bbox[_mm].x = (bbox[_mm].x > Math.PI)? (-2*Math.PI + bbox[_mm].x): bbox[_mm].x; 384 | }); 385 | 386 | break; 387 | }; 388 | 389 | return bbox; 390 | } 391 | 392 | targetAreaVisibleDeg() { 393 | let bbox = this.targetAreaVisible(); 394 | if(this.mode == 'sphere'){ 395 | bbox['min'].x = bbox['min'].x * (180/Math.PI); 396 | bbox['min'].y = bbox['min'].y * (180/Math.PI); 397 | bbox['max'].x = bbox['max'].x * (180/Math.PI); 398 | bbox['max'].y = bbox['max'].y * (180/Math.PI); 399 | } 400 | return bbox; 401 | } 402 | 403 | _sphericalCoordinatesFrom (cartesian_vec) { 404 | const rel_pos = ((new Vector3()).subVectors(this.target.center, cartesian_vec)); 405 | const rel_xzcomponent = new Vector3(rel_pos.x, 0, rel_pos.z); 406 | 407 | const v = new Vector3(); 408 | const sphCoord = new Vector2( 409 | rel_xzcomponent.angleTo(new Vector3(1,0,0)), 410 | rel_pos.angleTo(new Vector3(0,1,0)) 411 | ); 412 | sphCoord.x = (rel_pos.z > 0)? (2*Math.PI - sphCoord.x) : sphCoord.x; 413 | return sphCoord; 414 | } 415 | 416 | _updateZoomAlpha(){ 417 | this._finalTargetDistance = Math.max( this.minDistance, Math.min( this.maxDistance, this._finalTargetDistance ) ); 418 | var diff = this._currentTargetDistance - this._finalTargetDistance; 419 | var damping_alpha = this.zoomDampingAlpha; 420 | this._currentTargetDistance -= diff * damping_alpha; 421 | var rounding_places = 100000; 422 | this._zoomAlpha = Math.abs(Math.round((1 - ((this._currentTargetDistance - this.minDistance) / (this.maxDistance - this.minDistance))) * rounding_places ) / rounding_places); 423 | 424 | return this._zoomAlpha; 425 | } 426 | 427 | _updateDollyTrack(ray){ 428 | let intersect = new Vector3(); 429 | 430 | switch(this.mode){ 431 | case 'plane': 432 | ray.intersectPlane(this.target, intersect); 433 | break; 434 | case 'sphere': 435 | ray.intersectSphere(this.target, intersect); 436 | break; 437 | } 438 | 439 | if(intersect){ 440 | this._maxZoomPosition.addVectors(intersect, new Vector3().subVectors(this.camera.position, intersect).normalize().multiplyScalar(this.minDistance)); 441 | this._minZoomPosition.copy(this._calculateMinZoom(this.camera.position, intersect)); 442 | 443 | this._finalTargetDistance = this._currentTargetDistance = intersect.clone().sub(this.camera.position).length(); 444 | } 445 | } 446 | 447 | _getZoomScale(speed) { 448 | speed = speed || this.zoomSpeed; 449 | return Math.pow( 0.95, speed ); 450 | } 451 | 452 | _panLeft( distance, cameraMatrix ) { 453 | var v = new Vector3(); 454 | 455 | switch(this.mode){ 456 | case 'sphere': 457 | v.set(- distance, 0, 0); 458 | break; 459 | case 'plane': 460 | v.setFromMatrixColumn( cameraMatrix, 0 ); // get Y column of cameraMatrix 461 | v.multiplyScalar( - distance ); 462 | break; 463 | } 464 | 465 | this._panTarget.add( v ); 466 | } 467 | 468 | _panUp ( distance, cameraMatrix ) { 469 | var v = new Vector3(); 470 | 471 | switch(this.mode){ 472 | case 'sphere': 473 | v.set(0, - distance, 0); 474 | break; 475 | case 'plane': 476 | v.setFromMatrixColumn( cameraMatrix, 1 ); // get Y column of cameraMatrix 477 | v.multiplyScalar( distance ); 478 | break; 479 | } 480 | 481 | this._panTarget.add( v ); 482 | } 483 | 484 | // deltaX and deltaY are in pixels; right and down are positive 485 | _pan (deltaX, deltaY) { 486 | var element = this.domElement; 487 | 488 | var r = new Ray(this.camera.position, this._camOrientation); 489 | var targetDistance; 490 | 491 | switch(this.mode){ 492 | case 'plane': 493 | targetDistance = this._screenWorldXform * r.distanceToPlane(this.target); 494 | break; 495 | case 'sphere': 496 | //in spherical mode the pan coords are saved as radians and used as rotation angles 497 | const camToTarget = (new Vector3()).subVectors(this.camera.position, this.target.center); 498 | targetDistance = this._screenWorldXform * ((camToTarget.length() / this.target.radius) - 1); 499 | break; 500 | } 501 | 502 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 503 | this._panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.camera.matrix ); 504 | this._panUp( 2 * deltaY * targetDistance / element.clientHeight, this.camera.matrix ); 505 | 506 | } 507 | 508 | _dollyIn( dollyScale ) { 509 | if ( this._cameraOfKnownType() ) { 510 | this._finalTargetDistance /= dollyScale; 511 | } else { 512 | console.warn( 'WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 513 | this.enableZoom = false; 514 | } 515 | } 516 | 517 | _dollyOut( dollyScale ) { 518 | if ( this._cameraOfKnownType() ) { 519 | this._finalTargetDistance *= dollyScale; 520 | } else { 521 | console.warn( 'WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 522 | this.enableZoom = false; 523 | } 524 | } 525 | 526 | _cameraOfKnownType() { 527 | return this.camera.type === 'PerspectiveCamera' 528 | } 529 | 530 | _handleUpdateDollyTrackMouse(event){ 531 | var prevMouse = this._mouse.clone(); 532 | this._mouse.set(( event.offsetX / this.domElement.clientWidth ) * 2 - 1, - ( event.offsetY / this.domElement.clientHeight ) * 2 + 1); 533 | 534 | if(!prevMouse.equals(this._mouse)){ 535 | var rc = new Raycaster(); 536 | rc.setFromCamera(this._mouse, this.camera); 537 | this._updateDollyTrack(rc.ray); 538 | } 539 | } 540 | 541 | _handleMouseDownDolly( event ) { 542 | this._handleUpdateDollyTrackMouse(event); 543 | this._dollyStart.set( event.offsetX, event.offsetY ); 544 | } 545 | 546 | _handleMouseDownPan( event ) { 547 | 548 | this._panStart.set( event.offsetX, event.offsetY ); 549 | 550 | } 551 | 552 | _handleMouseMoveDolly( event ) { 553 | 554 | this._handleUpdateDollyTrackMouse(event); 555 | 556 | //console.log( 'handleMouseMoveDolly' ); 557 | 558 | this._dollyEnd.set( event.offsetX, event.offsetY ); 559 | 560 | this._dollyDelta.subVectors(this._dollyEnd, this._dollyStart ); 561 | 562 | if ( this._dollyDelta.y > 0 ) { 563 | 564 | this._dollyIn( this._getZoomScale() ); 565 | 566 | } else if ( this._dollyDelta.y < 0 ) { 567 | 568 | this._dollyOut( this._getZoomScale() ); 569 | 570 | } 571 | 572 | this._dollyStart.copy( this._dollyEnd ); 573 | 574 | this.update(); 575 | 576 | } 577 | 578 | _handleMouseMovePan( event ) { 579 | 580 | //console.log( 'handleMouseMovePan' ); 581 | 582 | this._panEnd.set( event.offsetX, event.offsetY ); 583 | 584 | this._panDelta.subVectors( this._panEnd, this._panStart ); 585 | 586 | this._pan( this._panDelta.x, this._panDelta.y ); 587 | 588 | this._panStart.copy( this._panEnd ); 589 | 590 | this.update(); 591 | 592 | } 593 | 594 | _handleMouseUp( event ) { 595 | 596 | //console.log( 'handleMouseUp' ); 597 | 598 | } 599 | 600 | _calculateMinZoom(cam_pos, map_intersect){ 601 | return map_intersect.clone().add( 602 | cam_pos.clone() 603 | .sub(map_intersect) 604 | .normalize() 605 | .multiplyScalar(this.maxDistance) 606 | ); 607 | } 608 | 609 | 610 | _handleMouseWheel( event ) { 611 | this._handleUpdateDollyTrackMouse(event); 612 | 613 | var delta = 0; 614 | 615 | if ( event.wheelDelta !== undefined ) { 616 | 617 | // WebKit / Opera / Explorer 9 618 | 619 | delta = event.wheelDelta; 620 | 621 | } else if ( event.detail !== undefined ) { 622 | 623 | // Firefox 624 | 625 | delta = - event.detail; 626 | 627 | } 628 | 629 | if ( delta > 0 ) { 630 | this._dollyOut( this._getZoomScale() ); 631 | } else if ( delta < 0 ) { 632 | this._dollyIn( this._getZoomScale() ); 633 | } 634 | 635 | this.update(); 636 | } 637 | 638 | _handleKeyDown( event ) { 639 | 640 | //console.log( 'handleKeyDown' ); 641 | 642 | switch ( event.key ) { 643 | 644 | case this.keys.PAN_UP: 645 | this._pan( 0, this.keyPanSpeed ); 646 | this.update(); 647 | break; 648 | 649 | case this.keys.PAN_BOTTOM: 650 | this._pan( 0, - this.keyPanSpeed ); 651 | this.update(); 652 | break; 653 | 654 | case this.keys.PAN_LEFT: 655 | this._pan( this.keyPanSpeed, 0 ); 656 | this.update(); 657 | break; 658 | 659 | case this.keys.PAN_RIGHT: 660 | this._pan( - this.keyPanSpeed, 0 ); 661 | this.update(); 662 | break; 663 | 664 | case this.keys.ZOOM_IN: 665 | this._dollyIn( this._getZoomScale(this.keyZoomSpeed) ) 666 | this.update(); 667 | break; 668 | 669 | case this.keys.ZOOM_OUT: 670 | this._dollyOut( this._getZoomScale(this.keyZoomSpeed) ) 671 | this.update(); 672 | break; 673 | 674 | } 675 | } 676 | 677 | _handleUpdateDollyTrackTouch( event ){ 678 | var centerpoint = new Vector2(); 679 | 680 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 681 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 682 | 683 | centerpoint.x = event.touches[ 0 ].pageX + (dx / 2); 684 | centerpoint.y = event.touches[ 0 ].pageY + (dy / 2); 685 | 686 | var mouse = new Vector2(); 687 | mouse.x = ( centerpoint.x / domElement.clientWidth ) * 2 - 1; 688 | mouse.y = - ( centerpoint.y / domElement.clientHeight ) * 2 + 1; 689 | 690 | this._updateDollyTrack(mouse); 691 | } 692 | 693 | _handleTouchStartDolly( event ) { 694 | this._handleUpdateDollyTrackTouch(event); 695 | 696 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 697 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 698 | 699 | var distance = Math.sqrt( dx * dx + dy * dy ); 700 | 701 | this._dollyStart.set( 0, distance ); 702 | 703 | } 704 | 705 | _handleTouchStartPan( event ) { 706 | 707 | //console.log( 'handleTouchStartPan' ); 708 | 709 | this._panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 710 | 711 | } 712 | 713 | 714 | _handleTouchMoveDolly( event ) { 715 | this._handleUpdateDollyTrackTouch(event); 716 | 717 | //console.log( 'handleTouchMoveDolly' ); 718 | 719 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 720 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 721 | 722 | var distance = Math.sqrt( dx * dx + dy * dy ); 723 | 724 | this._dollyEnd.set( 0, distance ); 725 | 726 | this._dollyDelta.subVectors( this._dollyEnd, this._dollyStart ); 727 | 728 | if ( this._dollyDelta.y > 0 ) { 729 | 730 | this._dollyOut( this._getZoomScale() ); 731 | 732 | } else if ( this._dollyDelta.y < 0 ) { 733 | 734 | this._dollyIn( this._getZoomScale() ); 735 | 736 | } 737 | 738 | this._dollyStart.copy( this._dollyEnd ); 739 | 740 | this.update(); 741 | 742 | } 743 | 744 | _handleTouchMovePan( event ) { 745 | 746 | this._panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 747 | 748 | this._panDelta.subVectors( this._panEnd, this._panStart ); 749 | 750 | this._pan( this._panDelta.x, this._panDelta.y ); 751 | 752 | this._panStart.copy( this._panEnd ); 753 | 754 | this.update(); 755 | 756 | } 757 | 758 | _handleTouchEnd( event ) { 759 | //console.log( 'handleTouchEnd' ); 760 | } 761 | 762 | // 763 | // event handlers - FSM: listen for events and reset state 764 | // 765 | 766 | _onMouseDown( event ) { 767 | 768 | if ( this.enabled === false ) return; 769 | 770 | event.preventDefault(); 771 | 772 | if ( event.button === this.mouseButtons.ZOOM ) { 773 | 774 | if ( this.enableZoom === false ) return; 775 | 776 | this._handleMouseDownDolly( event ); 777 | 778 | this._state = this._STATES.DOLLY; 779 | 780 | } else if ( event.button === this.mouseButtons.PAN ) { 781 | 782 | if ( this.enablePan === false ) return; 783 | 784 | this._handleMouseDownPan( event ); 785 | 786 | this._state = this._STATES.PAN; 787 | 788 | } 789 | 790 | if ( this._state !== this._STATES.NONE ) { 791 | 792 | this.domElement.addEventListener( 'mousemove', this._eventListeners.mousemove, false ); 793 | this.domElement.addEventListener( 'mouseup', this._eventListeners.mouseup, false ); 794 | 795 | this.dispatchEvent( this._startEvent ); 796 | 797 | } 798 | 799 | } 800 | 801 | _onMouseMove( event ) { 802 | 803 | if ( this.enabled === false ) return; 804 | 805 | event.preventDefault(); 806 | 807 | if ( this._state === this._STATES.DOLLY ) { 808 | 809 | if ( this.enableZoom === false ) return; 810 | 811 | this._handleMouseMoveDolly( event ); 812 | 813 | } else if ( this._state === this._STATES.PAN ) { 814 | 815 | if ( this.enablePan === false ) return; 816 | 817 | this._handleMouseMovePan( event ); 818 | } 819 | } 820 | 821 | _onMouseUp( event ) { 822 | 823 | if ( this.enabled === false ) return; 824 | 825 | this._handleMouseUp( event ); 826 | 827 | this.domElement.removeEventListener( 'mousemove', this._eventListeners.mousemove, false ); 828 | this.domElement.removeEventListener( 'mouseup', this._eventListeners.mouseup, false ); 829 | 830 | this.dispatchEvent( this._endEvent ); 831 | 832 | this._state = this._STATES.NONE; 833 | 834 | } 835 | 836 | _onMouseWheel( event ) { 837 | if ( this.enabled === false || this.enableZoom === false || ( this._state !== this._STATES.NONE ) ) return; 838 | 839 | event.preventDefault(); 840 | event.stopPropagation(); 841 | 842 | this._handleMouseWheel( event ); 843 | 844 | this.dispatchEvent( this._startEvent ); // not sure why these are here... 845 | this.dispatchEvent( this._endEvent ); 846 | 847 | } 848 | 849 | _onKeyDown( event ) { 850 | if ( this.enabled === false || this.enableKeys === false || this.enablePan === false ) return; 851 | 852 | this._handleKeyDown( event ); 853 | } 854 | 855 | _onTouchStart( event ) { 856 | 857 | if ( this.enabled === false ) return; 858 | 859 | switch ( event.touches.length ) { 860 | case 1: // three-fingered touch: pan 861 | 862 | if ( this.enablePan === false ) return; 863 | 864 | this._handleTouchStartPan( event ); 865 | 866 | this._state = this._STATES.TOUCH_PAN; 867 | 868 | break; 869 | 870 | case 2: // two-fingered touch: dolly 871 | 872 | if ( this.enableZoom === false ) return; 873 | 874 | this._handleTouchStartDolly( event ); 875 | 876 | this._state = this._STATES.TOUCH_DOLLY; 877 | 878 | break; 879 | 880 | default: 881 | 882 | this._state = this._STATES.NONE; 883 | 884 | } 885 | 886 | if ( this._state !== this._STATES.NONE ) { 887 | 888 | this.dispatchEvent( this._startEvent ); 889 | 890 | } 891 | 892 | } 893 | 894 | _onTouchMove( event ) { 895 | 896 | if ( this.enabled === false ) return; 897 | 898 | event.preventDefault(); 899 | event.stopPropagation(); 900 | 901 | switch ( event.touches.length ) { 902 | 903 | case 1: // one-fingered touch: pan 904 | if ( this.enablePan === false ) return; 905 | if ( this._state !== this._STATES.TOUCH_PAN ) return; // is this needed?... 906 | 907 | this._handleTouchMovePan( event ); 908 | 909 | break; 910 | 911 | case 2: // two-fingered touch: dolly 912 | 913 | if ( this.enableZoom === false ) return; 914 | if ( this._state !== this._STATES.TOUCH_DOLLY ) return; // is this needed?... 915 | 916 | this._handleTouchMoveDolly( event ); 917 | 918 | break; 919 | 920 | default: 921 | 922 | this._state = this._STATES.NONE; 923 | 924 | } 925 | 926 | } 927 | 928 | _onTouchEnd( event ) { 929 | 930 | if ( this.enabled === false ) return; 931 | 932 | this._handleTouchEnd( event ); 933 | 934 | this.dispatchEvent( this._endEvent ); 935 | 936 | this._state = this._STATES.NONE; 937 | 938 | } 939 | 940 | _onContextMenu( event ) { 941 | event.preventDefault(); 942 | } 943 | 944 | _onMouseOver ( event ) { 945 | this.domElement.focus(); 946 | return false; 947 | } 948 | 949 | _onMouseOut ( event ) { 950 | this.domElement.blur(); 951 | return false; 952 | } 953 | 954 | }; 955 | 956 | export default MapControls; 957 | -------------------------------------------------------------------------------- /dist/three-map-controls.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///delegated ./node_modules/three/build/three.module.js from dll-reference vendor","webpack:///./src/three-map-controls.js","webpack:///./test/stub_dom.js","webpack:///external \"vendor\""],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","configurable","enumerable","get","r","value","n","__esModule","object","property","prototype","hasOwnProperty","p","s","__webpack_exports__","three__WEBPACK_IMPORTED_MODULE_0__","window","require","MapControls","EventDispatcher","constructor","camera","domElement","options","super","this","undefined","document","body","enabled","target","minDistance","maxDistance","enableZoom","zoomSpeed","zoomDampingAlpha","initialZoom","enablePan","keyPanSpeed","keyZoomSpeed","panDampingAlpha","enableKeys","keys","PAN_LEFT","PAN_UP","PAN_RIGHT","PAN_BOTTOM","ZOOM_IN","ZOOM_OUT","mouseButtons","ZOOM","MOUSE","MIDDLE","PAN","LEFT","assign","isTargetValid","mode","Error","normal","constant","center","radius","_eventListeners","contextmenu","_onContextMenu","bind","mousedown","_onMouseDown","mousewheel","_onMouseWheel","MozMousePixelScroll","touchstart","_onTouchStart","touchend","_onTouchEnd","touchmove","_onTouchMove","keydown","_onKeyDown","mouseover","_onMouseOver","mouseout","_onMouseOut","mousemove","_onMouseMove","mouseup","_onMouseUp","_init","target0","clone","position0","position","zoom0","zoom","_changeEvent","type","_startEvent","_endEvent","_STATES","NONE","DOLLY","TOUCH_DOLLY","TOUCH_PAN","distanceToPoint","_state","_mouse","Vector2","_finalTargetDistance","_currentTargetDistance","_panTarget","Vector3","_panCurrent","_minZoomPosition","_maxZoomPosition","_panStart","_panEnd","_panDelta","_dollyStart","_dollyEnd","_dollyDelta","_camOrientation","_zoomAlpha","_screenWorldXform","Math","tan","fov","PI","_straightDollyTrack","lerpVectors","abs","res","_intersectCameraTarget","lookAt","intersection","ray","direction","normalize","_updateZoomAlpha","forEach","_e","addEventListener","tagName","getAttribute","setAttribute","update","coplanar","projectPoint","Ray","subVectors","intersectPlane","intersectSphere","_updateDollyTrack","getZoomAlpha","reset","copy","updateProjectionMatrix","dispatchEvent","panDelta","oldPanCurrent","lerp","add","v","quat","Quaternion","setFromAxisAngle","setFromMatrixColumn","matrix","x","applyQuaternion","y","z","dispose","removeEventListener","zoomToFit","mesh","dims","geometry","boundingSphere","localToWorld","diameter","targetCoord","_sphericalCoordinatesFrom","camCoord","delta","vFOV","hFOV","atan","aspect","obj_aspect","targetAreaVisible","bbox","vOffset","hOffset","depth","distanceToPlane","Box2","cam_pos","halfPi","length","min","cos","_mm","targetAreaVisibleDeg","cartesian_vec","rel_pos","rel_xzcomponent","sphCoord","angleTo","max","diff","damping_alpha","round","intersect","addVectors","multiplyScalar","_calculateMinZoom","sub","_getZoomScale","speed","pow","_panLeft","distance","cameraMatrix","set","_panUp","_pan","deltaX","deltaY","targetDistance","element","camToTarget","clientHeight","_dollyIn","dollyScale","_cameraOfKnownType","console","warn","_dollyOut","_handleUpdateDollyTrackMouse","event","prevMouse","offsetX","clientWidth","offsetY","equals","rc","Raycaster","setFromCamera","_handleMouseDownDolly","_handleMouseDownPan","_handleMouseMoveDolly","_handleMouseMovePan","_handleMouseUp","map_intersect","_handleMouseWheel","wheelDelta","detail","_handleKeyDown","key","_handleUpdateDollyTrackTouch","centerpoint","dx","touches","pageX","dy","pageY","mouse","_handleTouchStartDolly","sqrt","_handleTouchStartPan","_handleTouchMoveDolly","_handleTouchMovePan","_handleTouchEnd","preventDefault","button","stopPropagation","focus","blur","log","vendor"],"mappings":"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","file":"three-map-controls.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 1);\n","module.exports = (__webpack_require__(3))(0);","'use strict';\n\n//Alex Pilafian 2016-2019 - sikanrong@gmail.com\n\nimport {\n Box2,\n Quaternion,\n EventDispatcher,\n Vector2,\n Vector3,\n Raycaster,\n Ray,\n MOUSE\n} from 'three'\n\n//test stubs\nif(typeof window == 'undefined'){\n let window = require('../test/stub_dom');\n}\n\nclass MapControls extends EventDispatcher{\n\n constructor(camera, domElement, options){\n super();\n\n this.camera = camera;\n\n //Object to use for listening for keyboard/mouse events\n this.domElement = ( domElement !== undefined ) ? domElement : window.document.body;\n\n // Set to false to disable this control (Disables all input events)\n this.enabled = true;\n\n // Must be set to instance of Plane or Sphere\n this.target;\n\n // How far you can dolly in and out\n this.minDistance = 1; //probably should never be 0\n this.maxDistance = 100;\n\n // This option actually enables dollying in and out; left as \"zoom\" for backwards compatibility.\n // Set to false to disable zooming\n this.enableZoom = true;\n this.zoomSpeed = 6.0;\n this.zoomDampingAlpha = 0.1;\n this.initialZoom = 0; //start zoomed all the way out unless set in options.\n\n // Set to false to disable panning\n this.enablePan = true;\n this.keyPanSpeed = 50.0;\t// pixels moved per arrow key push\n this.keyZoomSpeed = this.zoomSpeed;\t// keyboard zoom speed, defaults to mouse-wheel zoom speed\n this.panDampingAlpha = 0.1;\n\n // Set to false to disable use of the keys\n this.enableKeys = true;\n\n // The four arrow keys, and two zoom keys\n this.keys = {\n PAN_LEFT: \"ArrowLeft\",\n PAN_UP: \"ArrowUp\",\n PAN_RIGHT: \"ArrowRight\",\n PAN_BOTTOM: \"ArrowDown\",\n ZOOM_IN: \"]\",\n ZOOM_OUT: \"[\"\n };\n\n // Mouse buttons\n this.mouseButtons = { ZOOM: MOUSE.MIDDLE, PAN: MOUSE.LEFT };\n \n //Copy options from parameters\n Object.assign(this, options);\n let isTargetValid = false;\n\n if(this.mode === undefined){\n throw new Error('\\'mode\\' option must be set to either \\'plane\\' or \\'sphere\\'');\n }\n\n switch(this.mode){\n case 'plane':\n isTargetValid = (this.target.normal !== undefined && this.target.constant !== undefined);\n break;\n case 'sphere':\n isTargetValid = (this.target.center !== undefined && this.target.radius !== undefined);\n break;\n }\n\n if(!isTargetValid){\n throw new Error('\\'target\\' option must be an instance of type THREE.Plane or THREE.Sphere');\n }\n\n this._eventListeners = {\n 'contextmenu': this._onContextMenu.bind(this),\n 'mousedown': this._onMouseDown.bind(this),\n 'mousewheel': this._onMouseWheel.bind(this),\n 'MozMousePixelScroll': this._onMouseWheel.bind(this),\n 'touchstart': this._onTouchStart.bind(this),\n 'touchend': this._onTouchEnd.bind(this),\n 'touchmove': this._onTouchMove.bind(this),\n 'keydown': this._onKeyDown.bind(this),\n 'mouseover': this._onMouseOver.bind(this),\n 'mouseout': this._onMouseOut.bind(this),\n 'mousemove': this._onMouseMove.bind(this),\n 'mouseup': this._onMouseUp.bind(this)\n };\n\n this._init();\n }\n\n _init (){\n\n this.target0 = this.target.clone();\n this.position0 = this.camera.position.clone();\n this.zoom0 = this.camera.zoom;\n this._changeEvent = { type: 'change' };\n this._startEvent = { type: 'start' };\n this._endEvent = { type: 'end' };\n\n this._STATES = { NONE : - 1, DOLLY : 1, PAN : 2, TOUCH_DOLLY : 4, TOUCH_PAN : 5 };\n\n if(this.target.distanceToPoint(this.camera.position) == 0){\n throw new Error(\"ORIENTATION_UNKNOWABLE: initial Camera position cannot intersect target plane.\");\n }\n\n this._state = this._STATES.NONE;\n\n this._mouse = new Vector2();\n\n this._finalTargetDistance = 0;\n this._currentTargetDistance = 0;\n\n this._panTarget = new Vector3(0,0,0);\n this._panCurrent = new Vector3(0,0,0);\n\n this._minZoomPosition = new Vector3();\n this._maxZoomPosition = new Vector3();\n\n this._panStart = new Vector2();\n this._panEnd = new Vector2();\n this._panDelta = new Vector2();\n\n this._dollyStart = new Vector2();\n this._dollyEnd = new Vector2();\n this._dollyDelta = new Vector2();\n\n this._camOrientation = new Vector2();\n\n this._zoomAlpha;\n\n this._screenWorldXform = Math.tan( ( this.camera.fov / 2 ) * Math.PI / 180.0 );\n\n //establish initial camera orientation based on position w.r.t. _this.target plane\n this._straightDollyTrack();\n\n this.camera.position.lerpVectors(this._minZoomPosition, this._maxZoomPosition, this.initialZoom);\n this._finalTargetDistance = this._currentTargetDistance = Math.abs(this.target.distanceToPoint(this.camera.position));\n\n const res = this._intersectCameraTarget();\n this.camera.lookAt(res.intersection); //set the orientation of the camera towards the map.\n this._camOrientation = res.ray.direction.clone().normalize();\n\n this._updateZoomAlpha();\n\n //Assign event listeners\n\n [\n 'contextmenu',\n 'mousedown',\n 'mousewheel',\n 'MozMousePixelScroll',\n 'touchstart',\n 'touchend',\n 'touchmove',\n 'mouseover',\n 'mouseout',\n 'keydown'\n ].forEach(_e => {\n this.domElement.addEventListener(_e, this._eventListeners[_e], false);\n });\n\n if(this.domElement.tagName == 'CANVAS' &&\n !this.domElement.getAttribute('tabindex')){\n //if we're dealing with a canvas element which has no tabindex,\n //give it one so that it may recieve keyboard focus\n this.domElement.setAttribute('tabindex', '1');\n }\n\n this.update();\n }\n\n _intersectCameraTarget(){\n let intersection = new Vector3();\n let ray;\n\n switch(this.mode){\n case 'plane':\n const coplanar = new Vector3();\n this.target.projectPoint(this.camera.position, coplanar);\n ray = new Ray(this.camera.position, new Vector3().subVectors(coplanar, this.camera.position).normalize());\n ray.intersectPlane(this.target, intersection);\n break;\n case 'sphere':\n ray = new Ray(this.camera.position, (new Vector3()).subVectors(this.target.center, this.camera.position).normalize());\n ray.intersectSphere(this.target, intersection);\n break;\n }\n\n return {\n intersection: intersection,\n ray: ray\n }\n }\n\n _straightDollyTrack(){\n this._updateDollyTrack(this._intersectCameraTarget().ray);\n }\n\n getZoomAlpha () {\n return this._zoomAlpha;\n }\n\n reset () {\n\n this.target.copy( this.target0 );\n this.camera.position.copy( this.position0 );\n this.camera.zoom = this.zoom0;\n\n this.camera.updateProjectionMatrix();\n\n this._init(); //reinit\n\n this.dispatchEvent( this._changeEvent );\n\n this.update();\n\n this._state = this._STATES.NONE;\n\n };\n\n // this method is exposed, but perhaps it would be better if we can make it private...\n update () {\n const panDelta = new Vector3();\n const oldPanCurrent = new Vector3();\n const position = this.camera.position;\n\n // move target to panned location\n oldPanCurrent.copy(this._panCurrent);\n this._panCurrent.lerp( this._panTarget, this.panDampingAlpha );\n panDelta.subVectors(this._panCurrent, oldPanCurrent);\n\n switch (this.mode) {\n case 'plane':\n this._maxZoomPosition.add(panDelta);\n this._minZoomPosition.add(panDelta);\n break;\n case 'sphere':\n const v = new Vector3();\n const quat = new Quaternion();\n\n quat.setFromAxisAngle(v.setFromMatrixColumn( this.camera.matrix, 1 ), panDelta.x);\n\n this._maxZoomPosition.applyQuaternion(quat);\n this._minZoomPosition.applyQuaternion(quat);\n\n quat.setFromAxisAngle(v.setFromMatrixColumn( this.camera.matrix, 0 ), panDelta.y);\n\n this._maxZoomPosition.applyQuaternion(quat);\n this._minZoomPosition.applyQuaternion(quat);\n\n //panDelta.z is only used for zoomToFit\n //all pan operations rotate around the camera's MatrixColumn axes, while zoomToFit needs to\n //rotate about the world Y-axis\n quat.setFromAxisAngle(new Vector3(0,1,0), panDelta.z);\n this._maxZoomPosition.applyQuaternion(quat);\n this._minZoomPosition.applyQuaternion(quat);\n\n break;\n }\n\n position.lerpVectors(this._minZoomPosition, this._maxZoomPosition, this._updateZoomAlpha());\n\n if(this.mode == 'sphere'){\n this.camera.lookAt(this.target.center);\n }\n }\n\n dispose () {\n Object.keys(this._eventListeners).forEach(_e =>{\n this.domElement.removeEventListener(_e, this._eventListeners[_e], false);\n });\n };\n\n zoomToFit (mesh, center, dims){\n\n if(center === undefined){\n center = mesh.geometry.boundingSphere.center.clone();\n }\n\n center = mesh.localToWorld(center.clone());\n\n if(dims === undefined){\n const diameter = (mesh.geometry.boundingSphere.radius * 2);\n dims = new Vector2(\n diameter,\n diameter\n );\n }\n\n switch(this.mode){\n case 'plane':\n this._panTarget.copy(center);\n this._panCurrent.copy(this._intersectCameraTarget().intersection);\n break;\n case 'sphere':\n const targetCoord = this._sphericalCoordinatesFrom(center);\n const camCoord = this._sphericalCoordinatesFrom(this.camera.position);\n const delta = new Vector2().subVectors(targetCoord, camCoord);\n\n //Handle wrapping around the antimeridian; the line of 2π (or 0) radians\n if(Math.abs(delta.x) > Math.PI){\n delta.x = (-Math.abs(delta.x) / delta.x) * ((Math.PI * 2) - Math.abs(delta.x));\n }\n\n this._panTarget.add(new Vector3(0, -delta.y, delta.x));\n break;\n }\n\n this._straightDollyTrack();\n\n const vFOV = this.camera.fov * (Math.PI / 180);\n const hFOV = 2 * Math.atan( Math.tan( vFOV / 2 ) * this.camera.aspect );\n const obj_aspect = dims.x / dims.y;\n\n this._finalTargetDistance = ((((obj_aspect > this.camera.aspect)? dims.x : dims.y) / 2) / Math.tan(((obj_aspect > this.camera.aspect)? hFOV : vFOV) / 2));\n\n\n };\n\n //returns a bounding box denoting the visible target area\n targetAreaVisible(){\n\n let bbox, vOffset, hOffset, center;\n\n switch(this.mode){\n case 'plane':\n var ray = new Ray(this.camera.position, this._camOrientation);\n var depth = ray.distanceToPlane(this.target);\n\n center = this.camera.position.clone();\n\n vOffset = this._screenWorldXform * depth;\n hOffset = vOffset * this.camera.aspect;\n\n bbox = new Box2(\n new Vector2(center.x - hOffset, center.y - vOffset),\n new Vector2(center.x + hOffset, center.y + vOffset)\n );\n\n break;\n case 'sphere':\n const cam_pos = (new Vector3()).subVectors(this.target.center, this.camera.position);\n center = this._sphericalCoordinatesFrom(this.camera.position);\n\n const halfPi = Math.PI / 2;\n\n const d = cam_pos.length();\n\n //Derived from solving the Haversine formula for Phi_2 when all other variables\n //(d, r, Theta_1, Theta_2, Phi_1) are given\n vOffset = this._screenWorldXform * ((d / this.target.radius) - 1);\n vOffset = Math.min(vOffset, halfPi);\n\n //Account for the aspect ratio of the screen, and the deformation of the sphere\n const r = this.target.radius * Math.cos(center.y - halfPi);\n hOffset = vOffset * this.camera.aspect * ( this.target.radius / r);\n hOffset = Math.min(hOffset, halfPi);\n\n bbox = new Box2(\n new Vector2(center.x - hOffset - halfPi, center.y - vOffset - halfPi),\n new Vector2(center.x + hOffset - halfPi, center.y + vOffset - halfPi)\n );\n\n ['min', 'max'].forEach(_mm => {\n bbox[_mm].x = (bbox[_mm].x > Math.PI)? (-2*Math.PI + bbox[_mm].x): bbox[_mm].x;\n });\n\n break;\n };\n\n return bbox;\n }\n \n targetAreaVisibleDeg() {\n let bbox = this.targetAreaVisible();\n if(this.mode == 'sphere'){\n bbox['min'].x = bbox['min'].x * (180/Math.PI);\n bbox['min'].y = bbox['min'].y * (180/Math.PI);\n bbox['max'].x = bbox['max'].x * (180/Math.PI);\n bbox['max'].y = bbox['max'].y * (180/Math.PI);\n }\n return bbox;\n }\n\n _sphericalCoordinatesFrom (cartesian_vec) {\n const rel_pos = ((new Vector3()).subVectors(this.target.center, cartesian_vec));\n const rel_xzcomponent = new Vector3(rel_pos.x, 0, rel_pos.z);\n\n const v = new Vector3();\n const sphCoord = new Vector2(\n rel_xzcomponent.angleTo(new Vector3(1,0,0)),\n rel_pos.angleTo(new Vector3(0,1,0))\n );\n sphCoord.x = (rel_pos.z > 0)? (2*Math.PI - sphCoord.x) : sphCoord.x;\n return sphCoord;\n }\n\n _updateZoomAlpha(){\n this._finalTargetDistance = Math.max( this.minDistance, Math.min( this.maxDistance, this._finalTargetDistance ) );\n var diff = this._currentTargetDistance - this._finalTargetDistance;\n var damping_alpha = this.zoomDampingAlpha;\n this._currentTargetDistance -= diff * damping_alpha;\n var rounding_places = 100000;\n this._zoomAlpha = Math.abs(Math.round((1 - ((this._currentTargetDistance - this.minDistance) / (this.maxDistance - this.minDistance))) * rounding_places ) / rounding_places);\n\n return this._zoomAlpha;\n }\n\n _updateDollyTrack(ray){\n let intersect = new Vector3();\n\n switch(this.mode){\n case 'plane':\n ray.intersectPlane(this.target, intersect);\n break;\n case 'sphere':\n ray.intersectSphere(this.target, intersect);\n break;\n }\n\n if(intersect){\n this._maxZoomPosition.addVectors(intersect, new Vector3().subVectors(this.camera.position, intersect).normalize().multiplyScalar(this.minDistance));\n this._minZoomPosition.copy(this._calculateMinZoom(this.camera.position, intersect));\n\n this._finalTargetDistance = this._currentTargetDistance = intersect.clone().sub(this.camera.position).length();\n }\n }\n\n _getZoomScale(speed) {\n speed = speed || this.zoomSpeed;\n return Math.pow( 0.95, speed );\n }\n\n _panLeft( distance, cameraMatrix ) {\n var v = new Vector3();\n\n switch(this.mode){\n case 'sphere':\n v.set(- distance, 0, 0);\n break;\n case 'plane':\n v.setFromMatrixColumn( cameraMatrix, 0 ); // get Y column of cameraMatrix\n v.multiplyScalar( - distance );\n break;\n }\n\n this._panTarget.add( v );\n }\n\n _panUp ( distance, cameraMatrix ) {\n var v = new Vector3();\n\n switch(this.mode){\n case 'sphere':\n v.set(0, - distance, 0);\n break;\n case 'plane':\n v.setFromMatrixColumn( cameraMatrix, 1 ); // get Y column of cameraMatrix\n v.multiplyScalar( distance );\n break;\n }\n\n this._panTarget.add( v );\n }\n\n // deltaX and deltaY are in pixels; right and down are positive\n _pan (deltaX, deltaY) {\n var element = this.domElement;\n\n var r = new Ray(this.camera.position, this._camOrientation);\n var targetDistance;\n\n switch(this.mode){\n case 'plane':\n targetDistance = this._screenWorldXform * r.distanceToPlane(this.target);\n break;\n case 'sphere':\n //in spherical mode the pan coords are saved as radians and used as rotation angles\n const camToTarget = (new Vector3()).subVectors(this.camera.position, this.target.center);\n targetDistance = this._screenWorldXform * ((camToTarget.length() / this.target.radius) - 1);\n break;\n }\n\n // we actually don't use screenWidth, since perspective camera is fixed to screen height\n this._panLeft( 2 * deltaX * targetDistance / element.clientHeight, this.camera.matrix );\n this._panUp( 2 * deltaY * targetDistance / element.clientHeight, this.camera.matrix );\n\n }\n\n _dollyIn( dollyScale ) {\n if ( this._cameraOfKnownType() ) {\n this._finalTargetDistance /= dollyScale;\n } else {\n console.warn( 'WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled.' );\n this.enableZoom = false;\n }\n }\n\n _dollyOut( dollyScale ) {\n if ( this._cameraOfKnownType() ) {\n this._finalTargetDistance *= dollyScale;\n } else {\n console.warn( 'WARNING: MapControls.js encountered an unknown camera type - dolly/zoom disabled.' );\n this.enableZoom = false;\n }\n }\n\n _cameraOfKnownType() {\n return this.camera.type === 'PerspectiveCamera'\n }\n\n _handleUpdateDollyTrackMouse(event){\n var prevMouse = this._mouse.clone();\n this._mouse.set(( event.offsetX / this.domElement.clientWidth ) * 2 - 1, - ( event.offsetY / this.domElement.clientHeight ) * 2 + 1);\n\n if(!prevMouse.equals(this._mouse)){\n var rc = new Raycaster();\n rc.setFromCamera(this._mouse, this.camera);\n this._updateDollyTrack(rc.ray);\n }\n }\n\n _handleMouseDownDolly( event ) {\n this._handleUpdateDollyTrackMouse(event);\n this._dollyStart.set( event.offsetX, event.offsetY );\n }\n\n _handleMouseDownPan( event ) {\n\n this._panStart.set( event.offsetX, event.offsetY );\n\n }\n\n _handleMouseMoveDolly( event ) {\n\n this._handleUpdateDollyTrackMouse(event);\n\n //console.log( 'handleMouseMoveDolly' );\n\n this._dollyEnd.set( event.offsetX, event.offsetY );\n\n this._dollyDelta.subVectors(this._dollyEnd, this._dollyStart );\n\n if ( this._dollyDelta.y > 0 ) {\n\n this._dollyIn( this._getZoomScale() );\n\n } else if ( this._dollyDelta.y < 0 ) {\n\n this._dollyOut( this._getZoomScale() );\n\n }\n\n this._dollyStart.copy( this._dollyEnd );\n\n this.update();\n\n }\n\n _handleMouseMovePan( event ) {\n\n //console.log( 'handleMouseMovePan' );\n\n this._panEnd.set( event.offsetX, event.offsetY );\n\n this._panDelta.subVectors( this._panEnd, this._panStart );\n\n this._pan( this._panDelta.x, this._panDelta.y );\n\n this._panStart.copy( this._panEnd );\n\n this.update();\n\n }\n\n _handleMouseUp( event ) {\n\n //console.log( 'handleMouseUp' );\n\n }\n\n _calculateMinZoom(cam_pos, map_intersect){\n return map_intersect.clone().add(\n cam_pos.clone()\n .sub(map_intersect)\n .normalize()\n .multiplyScalar(this.maxDistance)\n );\n }\n\n\n _handleMouseWheel( event ) {\n this._handleUpdateDollyTrackMouse(event);\n\n var delta = 0;\n\n if ( event.wheelDelta !== undefined ) {\n\n // WebKit / Opera / Explorer 9\n\n delta = event.wheelDelta;\n\n } else if ( event.detail !== undefined ) {\n\n // Firefox\n\n delta = - event.detail;\n\n }\n\n if ( delta > 0 ) {\n this._dollyOut( this._getZoomScale() );\n } else if ( delta < 0 ) {\n this._dollyIn( this._getZoomScale() );\n }\n\n this.update();\n }\n\n _handleKeyDown( event ) {\n\n //console.log( 'handleKeyDown' );\n\n switch ( event.key ) {\n\n case this.keys.PAN_UP:\n this._pan( 0, this.keyPanSpeed );\n this.update();\n break;\n\n case this.keys.PAN_BOTTOM:\n this._pan( 0, - this.keyPanSpeed );\n this.update();\n break;\n\n case this.keys.PAN_LEFT:\n this._pan( this.keyPanSpeed, 0 );\n this.update();\n break;\n\n case this.keys.PAN_RIGHT:\n this._pan( - this.keyPanSpeed, 0 );\n this.update();\n break;\n\n case this.keys.ZOOM_IN:\n this._dollyIn( this._getZoomScale(this.keyZoomSpeed) )\n this.update();\n break;\n\n case this.keys.ZOOM_OUT:\n this._dollyOut( this._getZoomScale(this.keyZoomSpeed) )\n this.update();\n break;\n\n }\n }\n\n _handleUpdateDollyTrackTouch( event ){\n var centerpoint = new Vector2();\n\n var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;\n var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;\n\n centerpoint.x = event.touches[ 0 ].pageX + (dx / 2);\n centerpoint.y = event.touches[ 0 ].pageY + (dy / 2);\n\n var mouse = new Vector2();\n mouse.x = ( centerpoint.x / domElement.clientWidth ) * 2 - 1;\n mouse.y = - ( centerpoint.y / domElement.clientHeight ) * 2 + 1;\n\n this._updateDollyTrack(mouse);\n }\n\n _handleTouchStartDolly( event ) {\n this._handleUpdateDollyTrackTouch(event);\n\n var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;\n var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;\n\n var distance = Math.sqrt( dx * dx + dy * dy );\n\n this._dollyStart.set( 0, distance );\n\n }\n\n _handleTouchStartPan( event ) {\n\n //console.log( 'handleTouchStartPan' );\n\n this._panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );\n\n }\n\n\n _handleTouchMoveDolly( event ) {\n this._handleUpdateDollyTrackTouch(event);\n\n //console.log( 'handleTouchMoveDolly' );\n\n var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;\n var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;\n\n var distance = Math.sqrt( dx * dx + dy * dy );\n\n this._dollyEnd.set( 0, distance );\n\n this._dollyDelta.subVectors( this._dollyEnd, this._dollyStart );\n\n if ( this._dollyDelta.y > 0 ) {\n\n this._dollyOut( this._getZoomScale() );\n\n } else if ( this._dollyDelta.y < 0 ) {\n\n this._dollyIn( this._getZoomScale() );\n\n }\n\n this._dollyStart.copy( this._dollyEnd );\n\n this.update();\n\n }\n\n _handleTouchMovePan( event ) {\n\n this._panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );\n\n this._panDelta.subVectors( this._panEnd, this._panStart );\n\n this._pan( this._panDelta.x, this._panDelta.y );\n\n this._panStart.copy( this._panEnd );\n\n this.update();\n\n }\n\n _handleTouchEnd( event ) {\n //console.log( 'handleTouchEnd' );\n }\n\n //\n // event handlers - FSM: listen for events and reset state\n //\n\n _onMouseDown( event ) {\n\n if ( this.enabled === false ) return;\n\n event.preventDefault();\n\n if ( event.button === this.mouseButtons.ZOOM ) {\n\n if ( this.enableZoom === false ) return;\n\n this._handleMouseDownDolly( event );\n\n this._state = this._STATES.DOLLY;\n\n } else if ( event.button === this.mouseButtons.PAN ) {\n\n if ( this.enablePan === false ) return;\n\n this._handleMouseDownPan( event );\n\n this._state = this._STATES.PAN;\n\n }\n\n if ( this._state !== this._STATES.NONE ) {\n\n this.domElement.addEventListener( 'mousemove', this._eventListeners.mousemove, false );\n this.domElement.addEventListener( 'mouseup', this._eventListeners.mouseup, false );\n\n this.dispatchEvent( this._startEvent );\n\n }\n\n }\n\n _onMouseMove( event ) {\n\n if ( this.enabled === false ) return;\n\n event.preventDefault();\n\n if ( this._state === this._STATES.DOLLY ) {\n\n if ( this.enableZoom === false ) return;\n\n this._handleMouseMoveDolly( event );\n\n } else if ( this._state === this._STATES.PAN ) {\n\n if ( this.enablePan === false ) return;\n\n this._handleMouseMovePan( event );\n }\n }\n\n _onMouseUp( event ) {\n\n if ( this.enabled === false ) return;\n\n this._handleMouseUp( event );\n\n this.domElement.removeEventListener( 'mousemove', this._eventListeners.mousemove, false );\n this.domElement.removeEventListener( 'mouseup', this._eventListeners.mouseup, false );\n\n this.dispatchEvent( this._endEvent );\n\n this._state = this._STATES.NONE;\n\n }\n\n _onMouseWheel( event ) {\n if ( this.enabled === false || this.enableZoom === false || ( this._state !== this._STATES.NONE ) ) return;\n\n event.preventDefault();\n event.stopPropagation();\n\n this._handleMouseWheel( event );\n\n this.dispatchEvent( this._startEvent ); // not sure why these are here...\n this.dispatchEvent( this._endEvent );\n\n }\n\n _onKeyDown( event ) {\n if ( this.enabled === false || this.enableKeys === false || this.enablePan === false ) return;\n\n this._handleKeyDown( event );\n }\n\n _onTouchStart( event ) {\n\n if ( this.enabled === false ) return;\n\n switch ( event.touches.length ) {\n case 1: // three-fingered touch: pan\n\n if ( this.enablePan === false ) return;\n\n this._handleTouchStartPan( event );\n\n this._state = this._STATES.TOUCH_PAN;\n\n break;\n\n case 2:\t// two-fingered touch: dolly\n\n if ( this.enableZoom === false ) return;\n\n this._handleTouchStartDolly( event );\n\n this._state = this._STATES.TOUCH_DOLLY;\n\n break;\n\n default:\n\n this._state = this._STATES.NONE;\n\n }\n\n if ( this._state !== this._STATES.NONE ) {\n\n this.dispatchEvent( this._startEvent );\n\n }\n\n }\n\n _onTouchMove( event ) {\n\n if ( this.enabled === false ) return;\n\n event.preventDefault();\n event.stopPropagation();\n\n switch ( event.touches.length ) {\n\n case 1: // one-fingered touch: pan\n if ( this.enablePan === false ) return;\n if ( this._state !== this._STATES.TOUCH_PAN ) return; // is this needed?...\n\n this._handleTouchMovePan( event );\n\n break;\n\n case 2: // two-fingered touch: dolly\n\n if ( this.enableZoom === false ) return;\n if ( this._state !== this._STATES.TOUCH_DOLLY ) return; // is this needed?...\n\n this._handleTouchMoveDolly( event );\n\n break;\n\n default:\n\n this._state = this._STATES.NONE;\n\n }\n\n }\n\n _onTouchEnd( event ) {\n\n if ( this.enabled === false ) return;\n\n this._handleTouchEnd( event );\n\n this.dispatchEvent( this._endEvent );\n\n this._state = this._STATES.NONE;\n\n }\n\n _onContextMenu( event ) {\n event.preventDefault();\n }\n\n _onMouseOver ( event ) {\n this.domElement.focus();\n return false;\n }\n\n _onMouseOut ( event ) {\n this.domElement.blur();\n return false;\n }\n\n};\n\nexport default MapControls;\n","module.exports = {\n console: {\n log: function () {}\n },\n document: {\n body: {\n clientWidth: 1920,\n clientHeight: 1080,\n addEventListener: function () {},\n removeEventListener: function () {}\n }\n }\n};\n","module.exports = vendor;"],"sourceRoot":""} --------------------------------------------------------------------------------