├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── dist ├── .DS_Store └── threebox.js ├── docs ├── SymbolLayer3D.md ├── Threebox.md ├── gallery.jpg └── img │ ├── features-3D-paths.png │ ├── features-3D-symbols.png │ ├── features-complex-buildings.png │ ├── features-point-clouds.png │ ├── features-shaders.png │ └── features-terrain.png ├── examples ├── Object3D.html ├── basic.html ├── config_template.js ├── line.html ├── logistics.html ├── mercator.html ├── models │ ├── Truck.mtl │ └── Truck.obj ├── old │ ├── basic.html │ ├── flocking.html │ └── orientation.html ├── raycaster.html └── tube.html ├── exports.js ├── main.js ├── package-lock.json ├── package.json ├── src ├── Animation │ └── AnimationManager.js ├── Camera │ └── CameraSync.js ├── Threebox.js ├── Utils │ ├── Utils.js │ └── ValueGenerator.js ├── objects │ ├── Object3D.js │ ├── customLines │ │ ├── Line2.js │ │ ├── LineGeometry.js │ │ ├── LineMaterial.js │ │ ├── LineSegments2.js │ │ ├── LineSegmentsGeometry.js │ │ ├── Wireframe.js │ │ └── WireframeGeometry2.js │ ├── line.js │ ├── loadObj.js │ ├── loaders │ │ ├── MTLLoader.js │ │ └── OBJLoader.js │ ├── objects.js │ ├── sphere.js │ └── tube.js ├── three.js └── utils │ ├── constants.js │ ├── material.js │ └── validate.js └── tests ├── threebox-tests-bundle.js ├── threebox-tests.html ├── threebox-tests.js └── unit ├── camera.test.js ├── material.test.js ├── object.test.js ├── utilities.test.js └── validate.test.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 2 | 3 | #### Enhancements 4 | 5 | - Add an Object class: convenience methods to produce lines, spheres, tubes, and imported OBJ/MTL meshes, as well as a method to bring in THREE.Object3D's produced elsewhere with vanilla Three.js. Most of these are moveable, and have methods to move/rotate/rescale 6 | 7 | - No need to call `tb.update()` after putting it in the custom layer's `render()` function. 8 | 9 | #### Bug fixes 10 | 11 | - Automatically adjust for viewport size (https://github.com/peterqliu/threebox/issues/43) 12 | 13 | #### Deprecated (but still functional) 14 | - `.setupDefaultLights()` has moved to an optional `defaultLights` parameter, in the third argument for Threebox(). 15 | - `tb.addAtCoordinate()` and `tb.moveToCoordinate()` have been deprecated. `tb.add(Object)` and `Object.setCoords()` replace them -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Peter Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `threebox` 2 | 3 | A three.js plugin for Mapbox GL JS, using the custom layer feature. Provides convenient methods to manage objects in lnglat coordinates, and to synchronize the map and scene cameras. 4 | 5 | threebox 6 | 7 | ### Compatibility/Dependencies 8 | 9 | - Mapbox v.0.50.0 and later (for custom layer support) 10 | - Three.r94 (already bundled into the Threebox build). If desired, other versions can be swapped in and rebuilt [here](https://github.com/peterqliu/threebox/blob/master/src/three.js), though compatibility is not guaranteed. 11 | 12 | ### Getting started 13 | 14 | Download the bundle from [`dist/threebox.js`](dist/threebox.js) and add include it in a ` 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 77 | -------------------------------------------------------------------------------- /examples/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sphere Example 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 71 | -------------------------------------------------------------------------------- /examples/config_template.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | accessToken: '' 3 | }; -------------------------------------------------------------------------------- /examples/line.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Line Example 4 | 5 | 6 | 7 | 8 | 9 | 21 | 22 | 23 |
24 | 25 | 110 | -------------------------------------------------------------------------------- /examples/logistics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Animated truck 4 | 5 | 6 | 7 | 8 | 9 | 24 | 25 | 26 |
27 |
Click on the map to drive the truck there
28 | 151 | -------------------------------------------------------------------------------- /examples/mercator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mercator projection 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 100 | -------------------------------------------------------------------------------- /examples/models/Truck.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'Truck.blend' 2 | # Material Count: 7 3 | 4 | newmtl Black 5 | Ns 96.078431 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.013347 0.013347 0.013347 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | 14 | newmtl Cream 15 | Ns 96.078431 16 | Ka 1.000000 1.000000 1.000000 17 | Kd 0.565467 0.640000 0.496259 18 | Ks 0.500000 0.500000 0.500000 19 | Ke 0.000000 0.000000 0.000000 20 | Ni 1.000000 21 | d 1.000000 22 | illum 2 23 | 24 | newmtl Silver 25 | Ns 96.078431 26 | Ka 1.000000 1.000000 1.000000 27 | Kd 0.385546 0.385546 0.385546 28 | Ks 0.500000 0.500000 0.500000 29 | Ke 0.000000 0.000000 0.000000 30 | Ni 1.000000 31 | d 1.000000 32 | illum 2 33 | 34 | newmtl Wheel_Rim 35 | Ns 96.078431 36 | Ka 1.000000 1.000000 1.000000 37 | Kd 0.640000 0.640000 0.640000 38 | Ks 0.500000 0.500000 0.500000 39 | Ke 0.000000 0.000000 0.000000 40 | Ni 1.000000 41 | d 1.000000 42 | illum 2 43 | 44 | newmtl Wheels 45 | Ns 96.078431 46 | Ka 1.000000 1.000000 1.000000 47 | Kd 0.047401 0.047401 0.047401 48 | Ks 0.500000 0.500000 0.500000 49 | Ke 0.000000 0.000000 0.000000 50 | Ni 1.000000 51 | d 1.000000 52 | illum 2 53 | 54 | newmtl White 55 | Ns 96.078431 56 | Ka 1.000000 1.000000 1.000000 57 | Kd 0.800000 0.800000 0.800000 58 | Ks 0.500000 0.500000 0.500000 59 | Ke 0.000000 0.000000 0.000000 60 | Ni 1.000000 61 | d 1.000000 62 | illum 2 63 | 64 | newmtl Window 65 | Ns 96.078431 66 | Ka 1.000000 1.000000 1.000000 67 | Kd 0.362586 0.585789 0.640000 68 | Ks 0.500000 0.500000 0.500000 69 | Ke 0.000000 0.000000 0.000000 70 | Ni 1.000000 71 | d 1.000000 72 | illum 2 73 | -------------------------------------------------------------------------------- /examples/old/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Threebox Basic Example 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 63 | -------------------------------------------------------------------------------- /examples/old/flocking.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Flocking Trucks SymbolLayer3D example 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 249 | -------------------------------------------------------------------------------- /examples/old/orientation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | SymboLayer3D example 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 117 | -------------------------------------------------------------------------------- /examples/raycaster.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Raycaster 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 112 | -------------------------------------------------------------------------------- /examples/tube.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tube Example 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 | 24 | 97 | -------------------------------------------------------------------------------- /exports.js: -------------------------------------------------------------------------------- 1 | window.Threebox = require('./src/Threebox.js'), 2 | window.THREE = require('./src/three.js') 3 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | module.exports = exports = { 2 | Threebox: require('./src/Threebox'), 3 | THREE: require('./src/three.js') 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threebox", 3 | "version": "0.2.0", 4 | "description": "A Mapbox GL JS plugin that combines the power of the Three.js 3D library with Mapbox geospatial tools.", 5 | "main": "main.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/peterqliu/threebox.git" 9 | }, 10 | "author": "@peterqliu and @kronick", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/peterqliu/threebox/issues" 14 | }, 15 | "dev-dependencies": { 16 | "tap-prettify": "0.0.2", 17 | "tape": "^4.6.3" 18 | }, 19 | "scripts": { 20 | "build": "browserify -g ./node_modules/uglifyify exports.js > dist/threebox.min.js", 21 | "dev": "watchify exports.js --verbose -o dist/threebox.js", 22 | "test": "browserify tests/threebox-tests.js > tests/threebox-tests-bundle.js; echo 'Open tests/threebox-tests.html to run tests in the browser.'" 23 | }, 24 | "dependencies": { 25 | "@turf/turf": "^5.1.6", 26 | "tape": "^4.10.1", 27 | "turf": "^3.0.14", 28 | "watchify": "^3.11.1", 29 | "uglifyify": "5.0.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Animation/AnimationManager.js: -------------------------------------------------------------------------------- 1 | var threebox = require('../Threebox.js'); 2 | var utils = require("../utils/utils.js"); 3 | var validate = require("../utils/validate.js"); 4 | 5 | function AnimationManager(map) { 6 | 7 | this.map = map 8 | this.enrolledObjects = []; 9 | this.previousFrameTime; 10 | 11 | }; 12 | 13 | AnimationManager.prototype = { 14 | 15 | enroll: function(obj) { 16 | 17 | /* Extend the provided object with animation-specific properties and track in the animation manager */ 18 | 19 | this.enrolledObjects.push(obj); 20 | 21 | // Give this object its own internal animation queue 22 | obj.animationQueue = []; 23 | 24 | obj.set = function(options) { 25 | 26 | //if duration is set, animate to the new state 27 | if (options.duration > 0 ){ 28 | 29 | var newParams = { 30 | start: Date.now(), 31 | expiration: Date.now() + options.duration, 32 | endState: {} 33 | } 34 | 35 | utils.extend(options, newParams); 36 | 37 | var translating = options.coords; 38 | var rotating = options.rotation; 39 | var scaling = options.scale || options.scaleX || options.scaleY || options.scaleZ; 40 | 41 | if (rotating) { 42 | 43 | var r = obj.rotation; 44 | options.startRotation = [r.x, r.y, r.z]; 45 | 46 | 47 | options.endState.rotation = utils.types.rotation(options.rotation, options.startRotation); 48 | options.rotationPerMs = options.endState.rotation 49 | .map(function(angle, index){ 50 | return (angle-options.startRotation[index])/options.duration; 51 | }) 52 | } 53 | 54 | if (scaling) { 55 | var s = obj.scale; 56 | options.startScale = [s.x, s.y, s.z]; 57 | options.endState.scale = utils.types.scale(options.scale, options.startScale); 58 | 59 | options.scalePerMs = options.endState.scale 60 | .map(function(scale, index){ 61 | return (scale-options.startScale[index])/options.duration; 62 | }) 63 | } 64 | 65 | if (translating) options.pathCurve = new THREE.CatmullRomCurve3(utils.lnglatsToWorld([obj.coordinates, options.coords])); 66 | 67 | var entry = { 68 | type:'set', 69 | parameters: options 70 | } 71 | 72 | this.animationQueue 73 | .push(entry); 74 | 75 | map.repaint = true; 76 | } 77 | 78 | //if no duration set, stop object's existing animations and go to that state immediately 79 | else { 80 | this.stop(); 81 | options.rotation = utils.radify(options.rotation); 82 | this._setObject(options); 83 | } 84 | 85 | return this 86 | 87 | }; 88 | 89 | obj.stop = function(){ 90 | this.animationQueue = []; 91 | return this; 92 | } 93 | 94 | obj.followPath = function (options, cb){ 95 | 96 | var entry = { 97 | type: 'followPath', 98 | parameters: utils._validate(options, defaults.followPath) 99 | }; 100 | 101 | utils.extend( 102 | entry.parameters, 103 | { 104 | pathCurve: new THREE.CatmullRomCurve3( 105 | utils.lnglatsToWorld(options.path) 106 | ), 107 | start: Date.now(), 108 | expiration: Date.now() + entry.parameters.duration, 109 | cb: cb 110 | } 111 | ); 112 | 113 | this.animationQueue 114 | .push(entry); 115 | 116 | map.repaint = true; 117 | 118 | return this; 119 | }; 120 | 121 | obj._setObject = function (options){ 122 | 123 | var p = options.position; // lnglat 124 | var r = options.rotation; // radians 125 | var s = options.scale; // 126 | var w = options.worldCoordinates; //Vector3 127 | var q = options.quaternion // [axis, angle] 128 | 129 | if (p) { 130 | this.coordinates = p; 131 | var c = utils.projectToWorld(p); 132 | this.position.copy(c) 133 | } 134 | 135 | if (r) this.rotation.set(r[0], r[1], r[2]); 136 | 137 | if (s) this.scale.set(s[0], s[1], s[2]); 138 | 139 | if (q) this.quaternion.setFromAxisAngle(q[0], q[1]); 140 | 141 | if (w) this.position.copy(w); 142 | 143 | map.repaint = true 144 | } 145 | }, 146 | 147 | update: function(now) { 148 | 149 | if (this.previousFrameTime === undefined) this.previousFrameTime = now; 150 | 151 | var dimensions = ['X','Y','Z']; 152 | 153 | //iterate through objects in queue. count in reverse so we can cull objects without frame shifting 154 | for (var a = this.enrolledObjects.length-1; a>=0; a--){ 155 | 156 | var object = this.enrolledObjects[a]; 157 | 158 | if(!object.animationQueue || object.animationQueue.length === 0) continue; 159 | 160 | //focus on first item in queue 161 | var item = object.animationQueue[0]; 162 | var options = item.parameters; 163 | 164 | // if an animation is past its expiration date, cull it 165 | if (!options.expiration) { 166 | // console.log('culled') 167 | 168 | object.animationQueue.splice(0,1); 169 | 170 | // set the start time of the next animation 171 | if (object.animationQueue[0]) object.animationQueue[0].parameters.start = now; 172 | 173 | return 174 | } 175 | 176 | //if finished, jump to end state and flag animation entry for removal next time around. Execute callback if there is one 177 | var expiring = now >= options.expiration; 178 | 179 | if (expiring) { 180 | options.expiration = false; 181 | if (options.endState) object._setObject(options.endState); 182 | options.cb(); 183 | } 184 | 185 | else { 186 | 187 | var timeProgress = (now - options.start) / options.duration; 188 | 189 | if (item.type === 'set'){ 190 | 191 | var objectState = {}; 192 | 193 | if (options.pathCurve) objectState.worldCoordinates = options.pathCurve.getPoint(timeProgress); 194 | 195 | if (options.rotationPerMs) { 196 | objectState.rotation = options.startRotation.map(function(rad, index){ 197 | return rad + options.rotationPerMs[index] * timeProgress * options.duration 198 | }) 199 | } 200 | 201 | if (options.scalePerMs) { 202 | objectState.scale = options.startScale.map(function(scale, index){ 203 | return scale + options.scalePerMs[index]*timeProgress * options.duration 204 | }) 205 | } 206 | 207 | object._setObject(objectState); 208 | } 209 | 210 | if (item.type === 'followPath'){ 211 | 212 | var position = options.pathCurve.getPointAt(timeProgress); 213 | objectState = {worldCoordinates: position}; 214 | 215 | // if we need to track heading 216 | if (options.trackHeading){ 217 | 218 | var tangent = options.pathCurve 219 | .getTangentAt(timeProgress) 220 | .normalize(); 221 | 222 | var axis = new THREE.Vector3(0,0,0); 223 | var up = new THREE.Vector3(0,1,0); 224 | 225 | axis 226 | .crossVectors(up, tangent) 227 | .normalize(); 228 | 229 | var radians = Math.acos(up.dot(tangent)); 230 | 231 | objectState.quaternion = [axis, radians]; 232 | 233 | } 234 | 235 | object._setObject(objectState); 236 | 237 | } 238 | } 239 | 240 | } 241 | 242 | this.previousFrameTime = now; 243 | } 244 | } 245 | 246 | const defaults = { 247 | followPath: { 248 | path: null, 249 | duration: 1000, 250 | trackHeading: true 251 | } 252 | } 253 | module.exports = exports = AnimationManager; -------------------------------------------------------------------------------- /src/Camera/CameraSync.js: -------------------------------------------------------------------------------- 1 | var THREE = require("../three.js"); 2 | var utils = require("../utils/utils.js"); 3 | var ThreeboxConstants = require("../utils/constants.js"); 4 | 5 | function CameraSync(map, camera, world) { 6 | 7 | this.map = map; 8 | this.camera = camera; 9 | this.active = true; 10 | 11 | this.camera.matrixAutoUpdate = false; // We're in charge of the camera now! 12 | 13 | // Postion and configure the world group so we can scale it appropriately when the camera zooms 14 | this.world = world || new THREE.Group(); 15 | this.world.position.x = this.world.position.y = ThreeboxConstants.WORLD_SIZE/2 16 | this.world.matrixAutoUpdate = false; 17 | 18 | 19 | //set up basic camera state 20 | 21 | this.state = { 22 | fov: 0.6435011087932844, 23 | translateCenter: new THREE.Matrix4, 24 | worldSizeRatio: 512/ThreeboxConstants.WORLD_SIZE 25 | }; 26 | 27 | this.state.translateCenter.makeTranslation(ThreeboxConstants.WORLD_SIZE/2, -ThreeboxConstants.WORLD_SIZE / 2, 0); 28 | 29 | // Listen for move events from the map and update the Three.js camera. Some attributes only change when viewport resizes, so update those accordingly 30 | var _this = this; 31 | 32 | this.map 33 | .on('move', function() { 34 | _this.updateCamera() 35 | }) 36 | .on('resize', function(){ 37 | _this.setupCamera(); 38 | }) 39 | 40 | 41 | this.setupCamera(); 42 | } 43 | 44 | CameraSync.prototype = { 45 | 46 | setupCamera: function() { 47 | 48 | var t = this.map.transform 49 | const halfFov = this.state.fov / 2; 50 | var cameraToCenterDistance = 0.5 / Math.tan(halfFov) * t.height; 51 | const groundAngle = Math.PI / 2 + t._pitch; 52 | 53 | this.state.cameraToCenterDistance = cameraToCenterDistance; 54 | this.state.cameraTranslateZ = new THREE.Matrix4().makeTranslation(0,0,cameraToCenterDistance); 55 | this.state.topHalfSurfaceDistance = Math.sin(halfFov) * cameraToCenterDistance / Math.sin(Math.PI - groundAngle - halfFov); 56 | 57 | this.updateCamera(); 58 | }, 59 | 60 | updateCamera: function(ev) { 61 | 62 | if(!this.camera) { 63 | console.log('nocamera') 64 | return; 65 | } 66 | 67 | var t = this.map.transform 68 | 69 | // Calculate z distance of the farthest fragment that should be rendered. 70 | const furthestDistance = Math.cos(Math.PI / 2 - t._pitch) * this.state.topHalfSurfaceDistance + this.state.cameraToCenterDistance; 71 | 72 | // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` 73 | const farZ = furthestDistance * 1.01; 74 | 75 | this.camera.projectionMatrix = utils.makePerspectiveMatrix(this.state.fov, t.width / t.height, 1, farZ); 76 | 77 | 78 | var cameraWorldMatrix = new THREE.Matrix4(); 79 | var cameraTranslateZ = new THREE.Matrix4().makeTranslation(0,0,this.state.cameraToCenterDistance); 80 | var rotatePitch = new THREE.Matrix4().makeRotationX(t._pitch); 81 | var rotateBearing = new THREE.Matrix4().makeRotationZ(t.angle); 82 | 83 | // Unlike the Mapbox GL JS camera, separate camera translation and rotation out into its world matrix 84 | // If this is applied directly to the projection matrix, it will work OK but break raycasting 85 | 86 | cameraWorldMatrix 87 | .premultiply(this.state.cameraTranslateZ) 88 | .premultiply(rotatePitch) 89 | .premultiply(rotateBearing) 90 | 91 | this.camera.matrixWorld.copy(cameraWorldMatrix); 92 | 93 | 94 | var zoomPow = t.scale * this.state.worldSizeRatio; 95 | 96 | // Handle scaling and translation of objects in the map in the world's matrix transform, not the camera 97 | var scale = new THREE.Matrix4; 98 | var translateCenter = new THREE.Matrix4; 99 | var translateMap = new THREE.Matrix4; 100 | var rotateMap = new THREE.Matrix4; 101 | 102 | scale 103 | .makeScale( zoomPow, zoomPow , zoomPow ); 104 | 105 | 106 | var x = -this.map.transform.x || -this.map.transform.point.x; 107 | var y = this.map.transform.y || this.map.transform.point.y; 108 | 109 | translateMap 110 | .makeTranslation(x, y, 0); 111 | 112 | rotateMap 113 | .makeRotationZ(Math.PI); 114 | 115 | this.world.matrix = new THREE.Matrix4; 116 | this.world.matrix 117 | .premultiply(rotateMap) 118 | .premultiply(this.state.translateCenter) 119 | .premultiply(scale) 120 | .premultiply(translateMap) 121 | 122 | 123 | // utils.prettyPrintMatrix(this.camera.projectionMatrix.elements); 124 | } 125 | } 126 | 127 | module.exports = exports = CameraSync; 128 | -------------------------------------------------------------------------------- /src/Threebox.js: -------------------------------------------------------------------------------- 1 | var THREE = require("./three.js"); 2 | var CameraSync = require("./camera/CameraSync.js"); 3 | var utils = require("./utils/utils.js"); 4 | var AnimationManager = require("./animation/AnimationManager.js"); 5 | var ThreeboxConstants = require("./utils/constants.js"); 6 | 7 | var Objects = require("./objects/objects.js"); 8 | var material = require("./utils/material.js"); 9 | var sphere = require("./objects/sphere.js"); 10 | var loadObj = require("./objects/loadObj.js"); 11 | var Object3D = require("./objects/Object3D.js"); 12 | var line = require("./objects/line.js"); 13 | var tube = require("./objects/tube.js"); 14 | 15 | function Threebox(map, glContext, options){ 16 | 17 | this.init(map, glContext, options); 18 | 19 | }; 20 | 21 | Threebox.prototype = { 22 | 23 | repaint: function(){ 24 | this.map.repaint = true; 25 | }, 26 | 27 | init: function (map, glContext, options){ 28 | 29 | this.map = map; 30 | 31 | // Set up a THREE.js scene 32 | this.renderer = new THREE.WebGLRenderer( { 33 | alpha: true, 34 | antialias: true, 35 | canvas: map.getCanvas(), 36 | context: glContext 37 | } ); 38 | 39 | this.renderer.shadowMap.enabled = true; 40 | this.renderer.autoClear = false; 41 | 42 | this.scene = new THREE.Scene(); 43 | this.camera = new THREE.PerspectiveCamera( 28, window.innerWidth / window.innerHeight, 0.000000000001, Infinity); 44 | 45 | // The CameraSync object will keep the Mapbox and THREE.js camera movements in sync. 46 | // It requires a world group to scale as we zoom in. Rotation is handled in the camera's 47 | // projection matrix itself (as is field of view and near/far clipping) 48 | // It automatically registers to listen for move events on the map so we don't need to do that here 49 | this.world = new THREE.Group(); 50 | this.scene.add(this.world); 51 | 52 | this.cameraSync = new CameraSync(this.map, this.camera, this.world); 53 | 54 | //raycaster for mouse events 55 | this.raycaster = new THREE.Raycaster(); 56 | 57 | // apply starter options 58 | 59 | this.options = utils._validate(options || {}, defaultOptions); 60 | if (this.options.defaultLights) this.defaultLights(); 61 | 62 | }, 63 | 64 | // Objects 65 | 66 | objects: new Objects(AnimationManager), 67 | 68 | sphere: sphere, 69 | 70 | line: line, 71 | 72 | tube: function(obj){ 73 | return tube(obj, this.world) 74 | }, 75 | 76 | Object3D: function(obj, o) { 77 | return Object3D(obj, o) 78 | }, 79 | 80 | loadObj: loadObj, 81 | 82 | 83 | // Material 84 | 85 | material: function(o){ 86 | return material(o) 87 | }, 88 | 89 | utils: utils, 90 | 91 | projectToWorld: function(coords) { 92 | return this.utils.projectToWorld(coords) 93 | }, 94 | 95 | unprojectFromWorld: function(v3) { 96 | return this.utils.unprojectFromWorld(v3) 97 | }, 98 | 99 | projectedUnitsPerMeter: function(lat) { 100 | return this.utils.projectedUnitsPerMeter(lat) 101 | }, 102 | 103 | queryRenderedFeatures: function(point){ 104 | 105 | var mouse = new THREE.Vector2(); 106 | 107 | // // scale mouse pixel position to a percentage of the screen's width and height 108 | mouse.x = ( point.x / this.map.transform.width ) * 2 - 1; 109 | mouse.y = 1 - ( point.y / this.map.transform.height ) * 2; 110 | 111 | this.raycaster.setFromCamera(mouse, this.camera); 112 | 113 | // calculate objects intersecting the picking ray 114 | var intersects = this.raycaster.intersectObjects(this.world.children, true); 115 | 116 | return intersects 117 | }, 118 | 119 | update: function() { 120 | 121 | if (this.map.repaint) this.map.repaint = false 122 | 123 | var timestamp = Date.now(); 124 | 125 | // Update any animations 126 | this.objects.animationManager.update(timestamp); 127 | 128 | this.renderer.state.reset(); 129 | 130 | // Render the scene and repaint the map 131 | this.renderer.render( this.scene, this.camera ); 132 | 133 | if (this.options.passiveRendering === false) this.map.triggerRepaint(); 134 | }, 135 | 136 | add: function(obj) { 137 | this.world.add(obj); 138 | }, 139 | 140 | remove: function(obj) { 141 | this.world.remove(obj); 142 | }, 143 | 144 | 145 | defaultLights: function(){ 146 | 147 | this.scene.add( new THREE.AmbientLight( 0xffffff ) ); 148 | var sunlight = new THREE.DirectionalLight(0xffffff, 0.25); 149 | sunlight.position.set(0,80000000,100000000); 150 | sunlight.matrixWorldNeedsUpdate = true; 151 | this.world.add(sunlight); 152 | 153 | }, 154 | 155 | memory: function (){ return this.renderer.info.memory}, 156 | 157 | version: '0.3.0', 158 | 159 | // DEPRECATED METHODS 160 | 161 | setupDefaultLights: function() { 162 | console.warn('.setupDefaultLights() has been moved to a "defaultLights" option inside Threebox()') 163 | this.defaultLights(); 164 | }, 165 | 166 | addAtCoordinate: function(obj, lnglat, options) { 167 | 168 | console.warn('addAtCoordinate() has been deprecated. Check out the and threebox.add() Object.setCoords() methods instead.') 169 | 170 | obj = this.Object3D({obj:obj}); 171 | 172 | obj.setCoords(lnglat) 173 | this.add(obj); 174 | 175 | return obj; 176 | }, 177 | 178 | moveToCoordinate: function(obj, lnglat, options) { 179 | console.warn('addAtCoordinate() has been deprecated. Check out the Object.setCoords() and threebox.add() methods instead.') 180 | 181 | if (!obj.setCoords) obj = this.Object3D(obj); 182 | obj.setCoords(lnglat, options); 183 | 184 | return obj; 185 | } 186 | } 187 | 188 | var defaultOptions = { 189 | defaultLights: false, 190 | passiveRendering: true 191 | } 192 | module.exports = exports = Threebox; 193 | 194 | -------------------------------------------------------------------------------- /src/Utils/Utils.js: -------------------------------------------------------------------------------- 1 | var THREE = require("../three.js"); 2 | var Constants = require("./constants.js"); 3 | var validate = require("./validate.js"); 4 | 5 | var utils = { 6 | 7 | prettyPrintMatrix: function(uglymatrix){ 8 | for (var s=0; s<4; s++){ 9 | var quartet=[uglymatrix[s], 10 | uglymatrix[s+4], 11 | uglymatrix[s+8], 12 | uglymatrix[s+12]]; 13 | console.log(quartet.map(function(num){return num.toFixed(4)})) 14 | } 15 | }, 16 | 17 | makePerspectiveMatrix: function(fovy, aspect, near, far) { 18 | 19 | var out = new THREE.Matrix4(); 20 | var f = 1.0 / Math.tan(fovy / 2), 21 | nf = 1 / (near - far); 22 | 23 | var newMatrix = [ 24 | f / aspect, 0, 0, 0, 25 | 0, f, 0, 0, 26 | 0, 0, (far + near) * nf, -1, 27 | 0, 0, (2 * far * near) * nf, 0 28 | ] 29 | 30 | out.elements = newMatrix 31 | return out; 32 | }, 33 | 34 | //gimme radians 35 | radify: function(deg){ 36 | 37 | function convert(degrees){ 38 | degrees = degrees || 0; 39 | return Math.PI*2*degrees/360 40 | } 41 | 42 | if (typeof deg === 'object'){ 43 | 44 | //if [x,y,z] array of rotations 45 | if (deg.length > 0){ 46 | return deg.map(function(degree){ 47 | return convert(degree) 48 | }) 49 | } 50 | 51 | // if {x: y: z:} rotation object 52 | else { 53 | return [convert(deg.x), convert(deg.y), convert(deg.z)] 54 | } 55 | } 56 | 57 | //if just a number 58 | else return convert(deg) 59 | }, 60 | 61 | //gimme degrees 62 | degreeify: function(rad){ 63 | function convert(radians){ 64 | radians = radians || 0; 65 | return radians * 360/(Math.PI*2) 66 | } 67 | 68 | if (typeof rad === 'object') { 69 | return [convert(rad.x), convert(rad.y), convert(rad.z)] 70 | } 71 | 72 | else return convert(rad) 73 | }, 74 | 75 | projectToWorld: function(coords){ 76 | 77 | // Spherical mercator forward projection, re-scaling to WORLD_SIZE 78 | 79 | var projected = [ 80 | -Constants.MERCATOR_A * Constants.DEG2RAD* coords[0] * Constants.PROJECTION_WORLD_SIZE, 81 | -Constants.MERCATOR_A * Math.log(Math.tan((Math.PI*0.25) + (0.5 * Constants.DEG2RAD * coords[1]) )) * Constants.PROJECTION_WORLD_SIZE 82 | ]; 83 | 84 | //z dimension, defaulting to 0 if not provided 85 | 86 | if (!coords[2]) projected.push(0) 87 | else { 88 | var pixelsPerMeter = this.projectedUnitsPerMeter(coords[1]); 89 | projected.push( coords[2] * pixelsPerMeter ); 90 | } 91 | 92 | var result = new THREE.Vector3(projected[0], projected[1], projected[2]); 93 | 94 | return result; 95 | }, 96 | 97 | projectedUnitsPerMeter: function(latitude) { 98 | return Math.abs( Constants.WORLD_SIZE / Math.cos( Constants.DEG2RAD * latitude ) / Constants.EARTH_CIRCUMFERENCE ); 99 | }, 100 | 101 | _scaleVerticesToMeters: function(centerLatLng, vertices) { 102 | var pixelsPerMeter = this.projectedUnitsPerMeter(centerLatLng[1]); 103 | var centerProjected = this.projectToWorld(centerLatLng); 104 | 105 | for (var i = 0; i < vertices.length; i++) { 106 | vertices[i].multiplyScalar(pixelsPerMeter); 107 | } 108 | 109 | return vertices; 110 | }, 111 | 112 | projectToScreen: function(coords) { 113 | console.log("WARNING: Projecting to screen coordinates is not yet implemented"); 114 | }, 115 | 116 | unprojectFromScreen: function (pixel) { 117 | console.log("WARNING: unproject is not yet implemented"); 118 | }, 119 | 120 | //world units to lnglat 121 | unprojectFromWorld: function (worldUnits) { 122 | 123 | var unprojected = [ 124 | -worldUnits.x / (Constants.MERCATOR_A * Constants.DEG2RAD * Constants.PROJECTION_WORLD_SIZE), 125 | 2*(Math.atan(Math.exp(worldUnits.y/(Constants.PROJECTION_WORLD_SIZE*(-Constants.MERCATOR_A))))-Math.PI/4)/Constants.DEG2RAD 126 | ]; 127 | 128 | var pixelsPerMeter = this.projectedUnitsPerMeter(unprojected[1]); 129 | 130 | //z dimension 131 | var height = worldUnits.z || 0; 132 | unprojected.push( height / pixelsPerMeter ); 133 | 134 | return unprojected; 135 | }, 136 | 137 | _flipMaterialSides: function(obj) { 138 | 139 | }, 140 | 141 | // to improve precision, normalize a series of vector3's to their collective center, and move the resultant mesh to that center 142 | normalizeVertices(vertices) { 143 | 144 | var geometry = new THREE.Geometry(); 145 | 146 | for (v3 of vertices) { 147 | geometry.vertices.push(v3) 148 | } 149 | 150 | geometry.computeBoundingSphere(); 151 | var center = geometry.boundingSphere.center; 152 | var radius = geometry.boundingSphere.radius; 153 | 154 | var scaled = vertices.map(function(v3){ 155 | var normalized = v3.sub(center); 156 | return normalized; 157 | }); 158 | 159 | return {vertices: scaled, position: center} 160 | }, 161 | 162 | //flatten an array of Vector3's into a shallow array of values in x-y-z order, for bufferGeometry 163 | flattenVectors(vectors) { 164 | var flattenedArray = []; 165 | for (vertex of vectors) { 166 | flattenedArray.push(vertex.x, vertex.y, vertex.z); 167 | } 168 | return flattenedArray 169 | }, 170 | 171 | //convert a line/polygon to Vector3's 172 | 173 | lnglatsToWorld: function(coords){ 174 | 175 | var vector3 = coords.map( 176 | function(pt){ 177 | var p = utils.projectToWorld(pt); 178 | var v3 = new THREE.Vector3(p.x, p.y, p.z); 179 | return v3 180 | } 181 | ); 182 | 183 | return vector3 184 | }, 185 | 186 | extend: function(original, addition) { 187 | for (key in addition) original[key] = addition[key]; 188 | }, 189 | 190 | clone: function(original) { 191 | var clone = {}; 192 | for (key in original) clone[key] = original[key]; 193 | return clone; 194 | }, 195 | 196 | // retrieve object parameters from an options object 197 | 198 | types: { 199 | 200 | rotation: function(r, currentRotation){ 201 | 202 | // if number provided, rotate only in Z by that amount 203 | if (typeof r === 'number') r = {z:r}; 204 | 205 | var degrees = this.applyDefault([r.x, r.y, r.z], currentRotation); 206 | var radians = utils.radify(degrees); 207 | return radians; 208 | 209 | }, 210 | 211 | scale: function(s, currentScale){ 212 | if (typeof s === 'number') return s = [s,s,s]; 213 | else return this.applyDefault([s.x, s.y, s.z], currentScale); 214 | }, 215 | 216 | applyDefault: function(array, current){ 217 | 218 | var output = array.map(function(item, index){ 219 | item = item || current[index]; 220 | return item 221 | }) 222 | 223 | return output 224 | } 225 | }, 226 | 227 | _validate: function(userInputs, defaults){ 228 | 229 | userInputs = userInputs || {}; 230 | var validatedOutput = {}; 231 | utils.extend(validatedOutput, userInputs); 232 | 233 | for (key of Object.keys(defaults)){ 234 | 235 | if (userInputs[key] === undefined) { 236 | //make sure required params are present 237 | if (defaults[key] === null) { 238 | console.error(key + ' is required') 239 | return; 240 | } 241 | 242 | else validatedOutput[key] = defaults[key] 243 | 244 | } 245 | 246 | else validatedOutput[key] = userInputs[key] 247 | } 248 | 249 | return validatedOutput 250 | }, 251 | Validator: new validate(), 252 | exposedMethods: ['projectToWorld', 'projectedUnitsPerMeter', 'extend', 'unprojectFromWorld'] 253 | } 254 | 255 | module.exports = exports = utils -------------------------------------------------------------------------------- /src/Utils/ValueGenerator.js: -------------------------------------------------------------------------------- 1 | const ValueGenerator = function(input) { 2 | if(typeof input === 'object' && input.property !== undefined) // Value name comes from a property in each item 3 | return (f => f.properties[input.property]); 4 | else if(typeof input === 'object' && input.generator !== undefined) // Value name generated by a function run on each item 5 | return input.generator; 6 | else return (() => input); 7 | 8 | return undefined; 9 | } 10 | 11 | module.exports = exports = ValueGenerator; -------------------------------------------------------------------------------- /src/objects/Object3D.js: -------------------------------------------------------------------------------- 1 | var Objects = require('./objects.js'); 2 | var utils = require("../utils/utils.js"); 3 | 4 | function Object3D(options) { 5 | options = utils._validate(options, Objects.prototype._defaults.Object3D); 6 | 7 | var obj = options.obj; 8 | 9 | if (options.units === 'meters') obj = Objects.prototype._makeGroup(options.obj, options); 10 | 11 | obj = Objects.prototype._addMethods(obj); 12 | return obj 13 | } 14 | 15 | 16 | module.exports = exports = Object3D; -------------------------------------------------------------------------------- /src/objects/customLines/Line2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | THREE.Line2 = function ( geometry, material ) { 7 | 8 | THREE.LineSegments2.call( this ); 9 | 10 | this.type = 'Line2'; 11 | 12 | this.geometry = geometry !== undefined ? geometry : new THREE.LineGeometry(); 13 | this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } ); 14 | 15 | }; 16 | 17 | THREE.Line2.prototype = Object.assign( Object.create( THREE.LineSegments2.prototype ), { 18 | 19 | constructor: THREE.Line2, 20 | 21 | isLine2: true, 22 | 23 | copy: function ( source ) { 24 | 25 | // todo 26 | 27 | return this; 28 | 29 | } 30 | 31 | } ); 32 | -------------------------------------------------------------------------------- /src/objects/customLines/LineGeometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | THREE.LineGeometry = function () { 7 | 8 | THREE.LineSegmentsGeometry.call( this ); 9 | 10 | this.type = 'LineGeometry'; 11 | 12 | }; 13 | 14 | THREE.LineGeometry.prototype = Object.assign( Object.create( THREE.LineSegmentsGeometry.prototype ), { 15 | 16 | constructor: THREE.LineGeometry, 17 | 18 | isLineGeometry: true, 19 | 20 | setPositions: function ( array ) { 21 | 22 | // converts [ x1, y1, z1, x2, y2, z2, ... ] to pairs format 23 | 24 | var length = array.length - 3; 25 | var points = new Float32Array( 2 * length ); 26 | 27 | for ( var i = 0; i < length; i += 3 ) { 28 | 29 | points[ 2 * i ] = array[ i ]; 30 | points[ 2 * i + 1 ] = array[ i + 1 ]; 31 | points[ 2 * i + 2 ] = array[ i + 2 ]; 32 | 33 | points[ 2 * i + 3 ] = array[ i + 3 ]; 34 | points[ 2 * i + 4 ] = array[ i + 4 ]; 35 | points[ 2 * i + 5 ] = array[ i + 5 ]; 36 | 37 | } 38 | 39 | THREE.LineSegmentsGeometry.prototype.setPositions.call( this, points ); 40 | 41 | return this; 42 | 43 | }, 44 | 45 | setColors: function ( array ) { 46 | 47 | // converts [ r1, g1, b1, r2, g2, b2, ... ] to pairs format 48 | 49 | var length = array.length - 3; 50 | var colors = new Float32Array( 2 * length ); 51 | 52 | for ( var i = 0; i < length; i += 3 ) { 53 | 54 | colors[ 2 * i ] = array[ i ]; 55 | colors[ 2 * i + 1 ] = array[ i + 1 ]; 56 | colors[ 2 * i + 2 ] = array[ i + 2 ]; 57 | 58 | colors[ 2 * i + 3 ] = array[ i + 3 ]; 59 | colors[ 2 * i + 4 ] = array[ i + 4 ]; 60 | colors[ 2 * i + 5 ] = array[ i + 5 ]; 61 | 62 | } 63 | 64 | THREE.LineSegmentsGeometry.prototype.setColors.call( this, colors ); 65 | 66 | return this; 67 | 68 | }, 69 | 70 | fromLine: function ( line ) { 71 | 72 | var geometry = line.geometry; 73 | 74 | if ( geometry.isGeometry ) { 75 | 76 | this.setPositions( geometry.vertices ); 77 | 78 | } else if ( geometry.isBufferGeometry ) { 79 | 80 | this.setPositions( geometry.position.array ); // assumes non-indexed 81 | 82 | } 83 | 84 | // set colors, maybe 85 | 86 | return this; 87 | 88 | }, 89 | 90 | copy: function ( source ) { 91 | 92 | // todo 93 | 94 | return this; 95 | 96 | } 97 | 98 | } ); 99 | -------------------------------------------------------------------------------- /src/objects/customLines/LineMaterial.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | * parameters = { 5 | * color: , 6 | * linewidth: , 7 | * dashed: , 8 | * dashScale: , 9 | * dashSize: , 10 | * gapSize: , 11 | * resolution: , // to be set by renderer 12 | * } 13 | */ 14 | 15 | THREE.UniformsLib.line = { 16 | 17 | linewidth: { value: 1 }, 18 | resolution: { value: new THREE.Vector2( 1, 1 ) }, 19 | dashScale: { value: 1 }, 20 | dashSize: { value: 1 }, 21 | gapSize: { value: 1 } // todo FIX - maybe change to totalSize 22 | 23 | }; 24 | 25 | THREE.ShaderLib[ 'line' ] = { 26 | 27 | uniforms: THREE.UniformsUtils.merge( [ 28 | THREE.UniformsLib.common, 29 | THREE.UniformsLib.fog, 30 | THREE.UniformsLib.line 31 | ] ), 32 | 33 | vertexShader: 34 | ` 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | 41 | uniform float linewidth; 42 | uniform vec2 resolution; 43 | 44 | attribute vec3 instanceStart; 45 | attribute vec3 instanceEnd; 46 | 47 | attribute vec3 instanceColorStart; 48 | attribute vec3 instanceColorEnd; 49 | 50 | varying vec2 vUv; 51 | 52 | #ifdef USE_DASH 53 | 54 | uniform float dashScale; 55 | attribute float instanceDistanceStart; 56 | attribute float instanceDistanceEnd; 57 | varying float vLineDistance; 58 | 59 | #endif 60 | 61 | void trimSegment( const in vec4 start, inout vec4 end ) { 62 | 63 | // trim end segment so it terminates between the camera plane and the near plane 64 | 65 | // conservative estimate of the near plane 66 | float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column 67 | float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column 68 | float nearEstimate = - 0.5 * b / a; 69 | 70 | float alpha = ( nearEstimate - start.z ) / ( end.z - start.z ); 71 | 72 | end.xyz = mix( start.xyz, end.xyz, alpha ); 73 | 74 | } 75 | 76 | void main() { 77 | 78 | #ifdef USE_COLOR 79 | 80 | vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd; 81 | 82 | #endif 83 | 84 | #ifdef USE_DASH 85 | 86 | vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd; 87 | 88 | #endif 89 | 90 | float aspect = resolution.x / resolution.y; 91 | 92 | vUv = uv; 93 | 94 | // camera space 95 | vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 ); 96 | vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 ); 97 | 98 | // special case for perspective projection, and segments that terminate either in, or behind, the camera plane 99 | // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space 100 | // but we need to perform ndc-space calculations in the shader, so we must address this issue directly 101 | // perhaps there is a more elegant solution -- WestLangley 102 | 103 | bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column 104 | 105 | if ( perspective ) { 106 | 107 | if ( start.z < 0.0 && end.z >= 0.0 ) { 108 | 109 | trimSegment( start, end ); 110 | 111 | } else if ( end.z < 0.0 && start.z >= 0.0 ) { 112 | 113 | trimSegment( end, start ); 114 | 115 | } 116 | 117 | } 118 | 119 | // clip space 120 | vec4 clipStart = projectionMatrix * start; 121 | vec4 clipEnd = projectionMatrix * end; 122 | 123 | // ndc space 124 | vec2 ndcStart = clipStart.xy / clipStart.w; 125 | vec2 ndcEnd = clipEnd.xy / clipEnd.w; 126 | 127 | // direction 128 | vec2 dir = ndcEnd - ndcStart; 129 | 130 | // account for clip-space aspect ratio 131 | dir.x *= aspect; 132 | dir = normalize( dir ); 133 | 134 | // perpendicular to dir 135 | vec2 offset = vec2( dir.y, - dir.x ); 136 | 137 | // undo aspect ratio adjustment 138 | dir.x /= aspect; 139 | offset.x /= aspect; 140 | 141 | // sign flip 142 | if ( position.x < 0.0 ) offset *= - 1.0; 143 | 144 | // endcaps 145 | if ( position.y < 0.0 ) { 146 | 147 | offset += - dir; 148 | 149 | } else if ( position.y > 1.0 ) { 150 | 151 | offset += dir; 152 | 153 | } 154 | 155 | // adjust for linewidth 156 | offset *= linewidth; 157 | 158 | // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ... 159 | offset /= resolution.y; 160 | 161 | // select end 162 | vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd; 163 | 164 | // back to clip space 165 | offset *= clip.w; 166 | 167 | clip.xy += offset; 168 | 169 | gl_Position = clip; 170 | 171 | #include 172 | 173 | #include 174 | #include 175 | #include 176 | 177 | } 178 | `, 179 | 180 | fragmentShader: 181 | ` 182 | uniform vec3 diffuse; 183 | uniform float opacity; 184 | 185 | #ifdef USE_DASH 186 | 187 | uniform float dashSize; 188 | uniform float gapSize; 189 | 190 | #endif 191 | 192 | varying float vLineDistance; 193 | 194 | #include 195 | #include 196 | #include 197 | #include 198 | #include 199 | 200 | varying vec2 vUv; 201 | 202 | void main() { 203 | 204 | #include 205 | 206 | #ifdef USE_DASH 207 | 208 | if ( vUv.y < 0.5 || vUv.y > 0.5 ) discard; // discard endcaps 209 | 210 | if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX 211 | 212 | #endif 213 | 214 | if ( vUv.y < 0.5 || vUv.y > 0.5 ) { 215 | 216 | float a = vUv.x - 0.5; 217 | float b = vUv.y - 0.5; 218 | float len2 = a * a + b * b; 219 | 220 | if ( len2 > 0.25 ) discard; 221 | 222 | } 223 | 224 | vec4 diffuseColor = vec4( diffuse, opacity ); 225 | 226 | #include 227 | #include 228 | 229 | gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a ); 230 | 231 | #include 232 | #include 233 | #include 234 | #include 235 | 236 | } 237 | ` 238 | }; 239 | 240 | THREE.LineMaterial = function ( parameters ) { 241 | 242 | THREE.ShaderMaterial.call( this, { 243 | 244 | type: 'LineMaterial', 245 | 246 | uniforms: THREE.UniformsUtils.clone( THREE.ShaderLib[ 'line' ].uniforms ), 247 | 248 | vertexShader: THREE.ShaderLib[ 'line' ].vertexShader, 249 | fragmentShader: THREE.ShaderLib[ 'line' ].fragmentShader 250 | 251 | } ); 252 | 253 | this.dashed = false; 254 | 255 | Object.defineProperties( this, { 256 | 257 | color: { 258 | 259 | enumerable: true, 260 | 261 | get: function () { 262 | 263 | return this.uniforms.diffuse.value; 264 | 265 | }, 266 | 267 | set: function ( value ) { 268 | 269 | this.uniforms.diffuse.value = value; 270 | 271 | } 272 | 273 | }, 274 | 275 | linewidth: { 276 | 277 | enumerable: true, 278 | 279 | get: function () { 280 | 281 | return this.uniforms.linewidth.value; 282 | 283 | }, 284 | 285 | set: function ( value ) { 286 | 287 | this.uniforms.linewidth.value = value; 288 | 289 | } 290 | 291 | }, 292 | 293 | dashScale: { 294 | 295 | enumerable: true, 296 | 297 | get: function () { 298 | 299 | return this.uniforms.dashScale.value; 300 | 301 | }, 302 | 303 | set: function ( value ) { 304 | 305 | this.uniforms.dashScale.value = value; 306 | 307 | } 308 | 309 | }, 310 | 311 | dashSize: { 312 | 313 | enumerable: true, 314 | 315 | get: function () { 316 | 317 | return this.uniforms.dashSize.value; 318 | 319 | }, 320 | 321 | set: function ( value ) { 322 | 323 | this.uniforms.dashSize.value = value; 324 | 325 | } 326 | 327 | }, 328 | 329 | gapSize: { 330 | 331 | enumerable: true, 332 | 333 | get: function () { 334 | 335 | return this.uniforms.gapSize.value; 336 | 337 | }, 338 | 339 | set: function ( value ) { 340 | 341 | this.uniforms.gapSize.value = value; 342 | 343 | } 344 | 345 | }, 346 | 347 | resolution: { 348 | 349 | enumerable: true, 350 | 351 | get: function () { 352 | 353 | return this.uniforms.resolution.value; 354 | 355 | }, 356 | 357 | set: function ( value ) { 358 | 359 | this.uniforms.resolution.value.copy( value ); 360 | 361 | } 362 | 363 | } 364 | 365 | } ); 366 | 367 | this.setValues( parameters ); 368 | 369 | }; 370 | 371 | THREE.LineMaterial.prototype = Object.create( THREE.ShaderMaterial.prototype ); 372 | THREE.LineMaterial.prototype.constructor = THREE.LineMaterial; 373 | 374 | THREE.LineMaterial.prototype.isLineMaterial = true; 375 | 376 | THREE.LineMaterial.prototype.copy = function ( source ) { 377 | 378 | THREE.ShaderMaterial.prototype.copy.call( this, source ); 379 | 380 | this.color.copy( source.color ); 381 | 382 | this.linewidth = source.linewidth; 383 | 384 | this.resolution = source.resolution; 385 | 386 | // todo 387 | 388 | return this; 389 | 390 | }; 391 | 392 | -------------------------------------------------------------------------------- /src/objects/customLines/LineSegments2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | THREE.LineSegments2 = function ( geometry, material ) { 7 | 8 | THREE.Mesh.call( this ); 9 | 10 | this.type = 'LineSegments2'; 11 | 12 | this.geometry = geometry !== undefined ? geometry : new THREE.LineSegmentsGeometry(); 13 | this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } ); 14 | 15 | }; 16 | 17 | THREE.LineSegments2.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { 18 | 19 | constructor: THREE.LineSegments2, 20 | 21 | isLineSegments2: true, 22 | 23 | computeLineDistances: ( function () { // for backwards-compatability, but could be a method of LineSegmentsGeometry... 24 | 25 | var start = new THREE.Vector3(); 26 | var end = new THREE.Vector3(); 27 | 28 | return function computeLineDistances() { 29 | 30 | var geometry = this.geometry; 31 | 32 | var instanceStart = geometry.attributes.instanceStart; 33 | var instanceEnd = geometry.attributes.instanceEnd; 34 | var lineDistances = new Float32Array( 2 * instanceStart.data.count ); 35 | 36 | for ( var i = 0, j = 0, l = instanceStart.data.count; i < l; i ++, j += 2 ) { 37 | 38 | start.fromBufferAttribute( instanceStart, i ); 39 | end.fromBufferAttribute( instanceEnd, i ); 40 | 41 | lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ]; 42 | lineDistances[ j + 1 ] = lineDistances[ j ] + start.distanceTo( end ); 43 | 44 | } 45 | 46 | var instanceDistanceBuffer = new THREE.InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1 47 | 48 | geometry.addAttribute( 'instanceDistanceStart', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0 49 | geometry.addAttribute( 'instanceDistanceEnd', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1 50 | 51 | return this; 52 | 53 | }; 54 | 55 | }() ), 56 | 57 | copy: function ( source ) { 58 | 59 | // todo 60 | 61 | return this; 62 | 63 | } 64 | 65 | } ); 66 | -------------------------------------------------------------------------------- /src/objects/customLines/LineSegmentsGeometry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | THREE.LineSegmentsGeometry = function () { 7 | 8 | THREE.InstancedBufferGeometry.call( this ); 9 | 10 | this.type = 'LineSegmentsGeometry'; 11 | 12 | var plane = new THREE.BufferGeometry(); 13 | 14 | var positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ]; 15 | var uvs = [ 0, 1, 1, 1, 0, .5, 1, .5, 0, .5, 1, .5, 0, 0, 1, 0 ]; 16 | var index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ]; 17 | 18 | this.setIndex( index ); 19 | this.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); 20 | this.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) ); 21 | 22 | }; 23 | 24 | THREE.LineSegmentsGeometry.prototype = Object.assign( Object.create( THREE.InstancedBufferGeometry.prototype ), { 25 | 26 | constructor: THREE.LineSegmentsGeometry, 27 | 28 | isLineSegmentsGeometry: true, 29 | 30 | applyMatrix: function ( matrix ) { 31 | 32 | var start = this.attributes.instanceStart; 33 | var end = this.attributes.instanceEnd; 34 | 35 | if ( start !== undefined ) { 36 | 37 | matrix.applyToBufferAttribute( start ); 38 | 39 | matrix.applyToBufferAttribute( end ); 40 | 41 | start.data.needsUpdate = true; 42 | 43 | } 44 | 45 | if ( this.boundingBox !== null ) { 46 | 47 | this.computeBoundingBox(); 48 | 49 | } 50 | 51 | if ( this.boundingSphere !== null ) { 52 | 53 | this.computeBoundingSphere(); 54 | 55 | } 56 | 57 | return this; 58 | 59 | }, 60 | 61 | setPositions: function ( array ) { 62 | 63 | var lineSegments; 64 | 65 | if ( array instanceof Float32Array ) { 66 | 67 | lineSegments = array; 68 | 69 | } else if ( Array.isArray( array ) ) { 70 | 71 | lineSegments = new Float32Array( array ); 72 | 73 | } 74 | 75 | var instanceBuffer = new THREE.InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz 76 | 77 | this.addAttribute( 'instanceStart', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz 78 | this.addAttribute( 'instanceEnd', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz 79 | 80 | // 81 | 82 | this.computeBoundingBox(); 83 | this.computeBoundingSphere(); 84 | 85 | return this; 86 | 87 | }, 88 | 89 | setColors: function ( array ) { 90 | 91 | var colors; 92 | 93 | if ( array instanceof Float32Array ) { 94 | 95 | colors = array; 96 | 97 | } else if ( Array.isArray( array ) ) { 98 | 99 | colors = new Float32Array( array ); 100 | 101 | } 102 | 103 | var instanceColorBuffer = new THREE.InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb 104 | 105 | this.addAttribute( 'instanceColorStart', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb 106 | this.addAttribute( 'instanceColorEnd', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb 107 | 108 | return this; 109 | 110 | }, 111 | 112 | fromWireframeGeometry: function ( geometry ) { 113 | 114 | this.setPositions( geometry.attributes.position.array ); 115 | 116 | return this; 117 | 118 | }, 119 | 120 | fromEdgesGeometry: function ( geometry ) { 121 | 122 | this.setPositions( geometry.attributes.position.array ); 123 | 124 | return this; 125 | 126 | }, 127 | 128 | fromMesh: function ( mesh ) { 129 | 130 | this.fromWireframeGeometry( new THREE.WireframeGeometry( mesh.geometry ) ); 131 | 132 | // set colors, maybe 133 | 134 | return this; 135 | 136 | }, 137 | 138 | fromLineSegements: function ( lineSegments ) { 139 | 140 | var geometry = lineSegments.geometry; 141 | 142 | if ( geometry.isGeometry ) { 143 | 144 | this.setPositions( geometry.vertices ); 145 | 146 | } else if ( geometry.isBufferGeometry ) { 147 | 148 | this.setPositions( geometry.position.array ); // assumes non-indexed 149 | 150 | } 151 | 152 | // set colors, maybe 153 | 154 | return this; 155 | 156 | }, 157 | 158 | computeBoundingBox: function () { 159 | 160 | var box = new THREE.Box3(); 161 | 162 | return function computeBoundingBox() { 163 | 164 | if ( this.boundingBox === null ) { 165 | 166 | this.boundingBox = new THREE.Box3(); 167 | 168 | } 169 | 170 | var start = this.attributes.instanceStart; 171 | var end = this.attributes.instanceEnd; 172 | 173 | if ( start !== undefined && end !== undefined ) { 174 | 175 | this.boundingBox.setFromBufferAttribute( start ); 176 | 177 | box.setFromBufferAttribute( end ); 178 | 179 | this.boundingBox.union( box ); 180 | 181 | } 182 | 183 | }; 184 | 185 | }(), 186 | 187 | computeBoundingSphere: function () { 188 | 189 | var vector = new THREE.Vector3(); 190 | 191 | return function computeBoundingSphere() { 192 | 193 | if ( this.boundingSphere === null ) { 194 | 195 | this.boundingSphere = new THREE.Sphere(); 196 | 197 | } 198 | 199 | if ( this.boundingBox === null ) { 200 | 201 | this.computeBoundingBox(); 202 | 203 | } 204 | 205 | var start = this.attributes.instanceStart; 206 | var end = this.attributes.instanceEnd; 207 | 208 | if ( start !== undefined && end !== undefined ) { 209 | 210 | var center = this.boundingSphere.center; 211 | 212 | this.boundingBox.getCenter( center ); 213 | 214 | var maxRadiusSq = 0; 215 | 216 | for ( var i = 0, il = start.count; i < il; i ++ ) { 217 | 218 | vector.fromBufferAttribute( start, i ); 219 | maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); 220 | 221 | vector.fromBufferAttribute( end, i ); 222 | maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); 223 | 224 | } 225 | 226 | this.boundingSphere.radius = Math.sqrt( maxRadiusSq ); 227 | 228 | if ( isNaN( this.boundingSphere.radius ) ) { 229 | 230 | console.error( 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this ); 231 | 232 | } 233 | 234 | } 235 | 236 | }; 237 | 238 | }(), 239 | 240 | toJSON: function () { 241 | 242 | // todo 243 | 244 | }, 245 | 246 | clone: function () { 247 | 248 | // todo 249 | 250 | }, 251 | 252 | copy: function ( source ) { 253 | 254 | // todo 255 | 256 | return this; 257 | 258 | } 259 | 260 | } ); 261 | -------------------------------------------------------------------------------- /src/objects/customLines/Wireframe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | THREE.Wireframe = function ( geometry, material ) { 7 | 8 | THREE.Mesh.call( this ); 9 | 10 | this.type = 'Wireframe'; 11 | 12 | this.geometry = geometry !== undefined ? geometry : new THREE.LineSegmentsGeometry(); 13 | this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } ); 14 | 15 | }; 16 | 17 | THREE.Wireframe.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { 18 | 19 | constructor: THREE.Wireframe, 20 | 21 | isWireframe: true, 22 | 23 | computeLineDistances: ( function () { // for backwards-compatability, but could be a method of LineSegmentsGeometry... 24 | 25 | var start = new THREE.Vector3(); 26 | var end = new THREE.Vector3(); 27 | 28 | return function computeLineDistances() { 29 | 30 | var geometry = this.geometry; 31 | 32 | var instanceStart = geometry.attributes.instanceStart; 33 | var instanceEnd = geometry.attributes.instanceEnd; 34 | var lineDistances = new Float32Array( 2 * instanceStart.data.count ); 35 | 36 | for ( var i = 0, j = 0, l = instanceStart.data.count; i < l; i ++, j += 2 ) { 37 | 38 | start.fromBufferAttribute( instanceStart, i ); 39 | end.fromBufferAttribute( instanceEnd, i ); 40 | 41 | lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ]; 42 | lineDistances[ j + 1 ] = lineDistances[ j ] + start.distanceTo( end ); 43 | 44 | } 45 | 46 | var instanceDistanceBuffer = new THREE.InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1 47 | 48 | geometry.addAttribute( 'instanceDistanceStart', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0 49 | geometry.addAttribute( 'instanceDistanceEnd', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1 50 | 51 | return this; 52 | 53 | }; 54 | 55 | }() ), 56 | 57 | copy: function ( source ) { 58 | 59 | // todo 60 | 61 | return this; 62 | 63 | } 64 | 65 | } ); 66 | -------------------------------------------------------------------------------- /src/objects/customLines/WireframeGeometry2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author WestLangley / http://github.com/WestLangley 3 | * 4 | */ 5 | 6 | THREE.WireframeGeometry2 = function ( geometry ) { 7 | 8 | THREE.LineSegmentsGeometry.call( this ); 9 | 10 | this.type = 'WireframeGeometry2'; 11 | 12 | this.fromWireframeGeometry( new THREE.WireframeGeometry( geometry ) ); 13 | 14 | // set colors, maybe 15 | 16 | }; 17 | 18 | THREE.WireframeGeometry2.prototype = Object.assign( Object.create( THREE.LineSegmentsGeometry.prototype ), { 19 | 20 | constructor: THREE.WireframeGeometry2, 21 | 22 | isWireframeGeometry2: true, 23 | 24 | copy: function ( source ) { 25 | 26 | // todo 27 | 28 | return this; 29 | 30 | } 31 | 32 | } ); 33 | -------------------------------------------------------------------------------- /src/objects/line.js: -------------------------------------------------------------------------------- 1 | var THREE = require("../three.js"); 2 | var utils = require("../utils/utils.js"); 3 | var Objects = require('./objects.js'); 4 | 5 | function line(obj){ 6 | 7 | obj = utils._validate(obj, Objects.prototype._defaults.line); 8 | 9 | // Geometry 10 | var straightProject = utils.lnglatsToWorld(obj.geometry); 11 | var normalized = utils.normalizeVertices(straightProject); 12 | var flattenedArray = utils.flattenVectors(normalized.vertices); 13 | console.log('line', normalized.vertices) 14 | 15 | var geometry = new THREE.LineGeometry(); 16 | geometry.setPositions( flattenedArray ); 17 | // geometry.setColors( colors ); 18 | 19 | // Material 20 | matLine = new THREE.LineMaterial( { 21 | color: obj.color, 22 | linewidth: obj.width, // in pixels 23 | dashed: false, 24 | opacity: obj.opacity 25 | } ); 26 | 27 | matLine.resolution.set( window.innerWidth, window.innerHeight ); 28 | matLine.isMaterial = true; 29 | matLine.transparent = true; 30 | matLine.depthWrite = false; 31 | 32 | // Mesh 33 | line = new THREE.Line2( geometry, matLine ); 34 | line.position.copy(normalized.position) 35 | line.computeLineDistances(); 36 | 37 | return line 38 | } 39 | 40 | module.exports = exports = line; 41 | 42 | 43 | 44 | /** 45 | * custom line shader by WestLangley, sourced from https://github.com/mrdoob/three.js/tree/master/examples/js/lines 46 | * 47 | */ 48 | 49 | THREE.LineSegmentsGeometry = function () { 50 | 51 | THREE.InstancedBufferGeometry.call( this ); 52 | 53 | this.type = 'LineSegmentsGeometry'; 54 | 55 | var plane = new THREE.BufferGeometry(); 56 | 57 | var positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ]; 58 | var uvs = [ 0, 1, 1, 1, 0, .5, 1, .5, 0, .5, 1, .5, 0, 0, 1, 0 ]; 59 | var index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ]; 60 | 61 | this.setIndex( index ); 62 | this.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); 63 | this.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) ); 64 | 65 | }; 66 | 67 | THREE.LineSegmentsGeometry.prototype = Object.assign( Object.create( THREE.InstancedBufferGeometry.prototype ), { 68 | 69 | constructor: THREE.LineSegmentsGeometry, 70 | 71 | isLineSegmentsGeometry: true, 72 | 73 | applyMatrix: function ( matrix ) { 74 | 75 | var start = this.attributes.instanceStart; 76 | var end = this.attributes.instanceEnd; 77 | 78 | if ( start !== undefined ) { 79 | 80 | matrix.applyToBufferAttribute( start ); 81 | 82 | matrix.applyToBufferAttribute( end ); 83 | 84 | start.data.needsUpdate = true; 85 | 86 | } 87 | 88 | if ( this.boundingBox !== null ) { 89 | 90 | this.computeBoundingBox(); 91 | 92 | } 93 | 94 | if ( this.boundingSphere !== null ) { 95 | 96 | this.computeBoundingSphere(); 97 | 98 | } 99 | 100 | return this; 101 | 102 | }, 103 | 104 | setPositions: function ( array ) { 105 | 106 | var lineSegments; 107 | 108 | if ( array instanceof Float32Array ) { 109 | 110 | lineSegments = array; 111 | 112 | } else if ( Array.isArray( array ) ) { 113 | 114 | lineSegments = new Float32Array( array ); 115 | 116 | } 117 | 118 | var instanceBuffer = new THREE.InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz 119 | 120 | this.addAttribute( 'instanceStart', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz 121 | this.addAttribute( 'instanceEnd', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz 122 | 123 | // 124 | 125 | this.computeBoundingBox(); 126 | this.computeBoundingSphere(); 127 | 128 | return this; 129 | 130 | }, 131 | 132 | setColors: function ( array ) { 133 | 134 | var colors; 135 | 136 | if ( array instanceof Float32Array ) { 137 | 138 | colors = array; 139 | 140 | } else if ( Array.isArray( array ) ) { 141 | 142 | colors = new Float32Array( array ); 143 | 144 | } 145 | 146 | var instanceColorBuffer = new THREE.InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb 147 | 148 | this.addAttribute( 'instanceColorStart', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb 149 | this.addAttribute( 'instanceColorEnd', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb 150 | 151 | return this; 152 | 153 | }, 154 | 155 | fromWireframeGeometry: function ( geometry ) { 156 | 157 | this.setPositions( geometry.attributes.position.array ); 158 | 159 | return this; 160 | 161 | }, 162 | 163 | fromEdgesGeometry: function ( geometry ) { 164 | 165 | this.setPositions( geometry.attributes.position.array ); 166 | 167 | return this; 168 | 169 | }, 170 | 171 | fromMesh: function ( mesh ) { 172 | 173 | this.fromWireframeGeometry( new THREE.WireframeGeometry( mesh.geometry ) ); 174 | 175 | // set colors, maybe 176 | 177 | return this; 178 | 179 | }, 180 | 181 | fromLineSegements: function ( lineSegments ) { 182 | 183 | var geometry = lineSegments.geometry; 184 | 185 | if ( geometry.isGeometry ) { 186 | 187 | this.setPositions( geometry.vertices ); 188 | 189 | } else if ( geometry.isBufferGeometry ) { 190 | 191 | this.setPositions( geometry.position.array ); // assumes non-indexed 192 | 193 | } 194 | 195 | // set colors, maybe 196 | 197 | return this; 198 | 199 | }, 200 | 201 | computeBoundingBox: function () { 202 | 203 | var box = new THREE.Box3(); 204 | 205 | return function computeBoundingBox() { 206 | 207 | if ( this.boundingBox === null ) { 208 | 209 | this.boundingBox = new THREE.Box3(); 210 | 211 | } 212 | 213 | var start = this.attributes.instanceStart; 214 | var end = this.attributes.instanceEnd; 215 | 216 | if ( start !== undefined && end !== undefined ) { 217 | 218 | this.boundingBox.setFromBufferAttribute( start ); 219 | 220 | box.setFromBufferAttribute( end ); 221 | 222 | this.boundingBox.union( box ); 223 | 224 | } 225 | 226 | }; 227 | 228 | }(), 229 | 230 | computeBoundingSphere: function () { 231 | 232 | var vector = new THREE.Vector3(); 233 | 234 | return function computeBoundingSphere() { 235 | 236 | if ( this.boundingSphere === null ) { 237 | 238 | this.boundingSphere = new THREE.Sphere(); 239 | 240 | } 241 | 242 | if ( this.boundingBox === null ) { 243 | 244 | this.computeBoundingBox(); 245 | 246 | } 247 | 248 | var start = this.attributes.instanceStart; 249 | var end = this.attributes.instanceEnd; 250 | 251 | if ( start !== undefined && end !== undefined ) { 252 | 253 | var center = this.boundingSphere.center; 254 | 255 | this.boundingBox.getCenter( center ); 256 | 257 | var maxRadiusSq = 0; 258 | 259 | for ( var i = 0, il = start.count; i < il; i ++ ) { 260 | 261 | vector.fromBufferAttribute( start, i ); 262 | maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); 263 | 264 | vector.fromBufferAttribute( end, i ); 265 | maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); 266 | 267 | } 268 | 269 | this.boundingSphere.radius = Math.sqrt( maxRadiusSq ); 270 | 271 | if ( isNaN( this.boundingSphere.radius ) ) { 272 | 273 | console.error( 'THREE.LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this ); 274 | 275 | } 276 | 277 | } 278 | 279 | }; 280 | 281 | }(), 282 | 283 | toJSON: function () { 284 | 285 | // todo 286 | 287 | }, 288 | 289 | clone: function () { 290 | 291 | // todo 292 | 293 | }, 294 | 295 | copy: function ( source ) { 296 | 297 | // todo 298 | 299 | return this; 300 | 301 | } 302 | 303 | } ); 304 | 305 | /** 306 | * @author WestLangley / http://github.com/WestLangley 307 | * 308 | */ 309 | 310 | THREE.LineGeometry = function () { 311 | 312 | THREE.LineSegmentsGeometry.call( this ); 313 | 314 | this.type = 'LineGeometry'; 315 | 316 | }; 317 | 318 | THREE.LineGeometry.prototype = Object.assign( Object.create( THREE.LineSegmentsGeometry.prototype ), { 319 | 320 | constructor: THREE.LineGeometry, 321 | 322 | isLineGeometry: true, 323 | 324 | setPositions: function ( array ) { 325 | 326 | // converts [ x1, y1, z1, x2, y2, z2, ... ] to pairs format 327 | 328 | var length = array.length - 3; 329 | var points = new Float32Array( 2 * length ); 330 | 331 | for ( var i = 0; i < length; i += 3 ) { 332 | 333 | points[ 2 * i ] = array[ i ]; 334 | points[ 2 * i + 1 ] = array[ i + 1 ]; 335 | points[ 2 * i + 2 ] = array[ i + 2 ]; 336 | 337 | points[ 2 * i + 3 ] = array[ i + 3 ]; 338 | points[ 2 * i + 4 ] = array[ i + 4 ]; 339 | points[ 2 * i + 5 ] = array[ i + 5 ]; 340 | 341 | } 342 | 343 | THREE.LineSegmentsGeometry.prototype.setPositions.call( this, points ); 344 | 345 | return this; 346 | 347 | }, 348 | 349 | setColors: function ( array ) { 350 | 351 | // converts [ r1, g1, b1, r2, g2, b2, ... ] to pairs format 352 | 353 | var length = array.length - 3; 354 | var colors = new Float32Array( 2 * length ); 355 | 356 | for ( var i = 0; i < length; i += 3 ) { 357 | 358 | colors[ 2 * i ] = array[ i ]; 359 | colors[ 2 * i + 1 ] = array[ i + 1 ]; 360 | colors[ 2 * i + 2 ] = array[ i + 2 ]; 361 | 362 | colors[ 2 * i + 3 ] = array[ i + 3 ]; 363 | colors[ 2 * i + 4 ] = array[ i + 4 ]; 364 | colors[ 2 * i + 5 ] = array[ i + 5 ]; 365 | 366 | } 367 | 368 | THREE.LineSegmentsGeometry.prototype.setColors.call( this, colors ); 369 | 370 | return this; 371 | 372 | }, 373 | 374 | fromLine: function ( line ) { 375 | 376 | var geometry = line.geometry; 377 | 378 | if ( geometry.isGeometry ) { 379 | 380 | this.setPositions( geometry.vertices ); 381 | 382 | } else if ( geometry.isBufferGeometry ) { 383 | 384 | this.setPositions( geometry.position.array ); // assumes non-indexed 385 | 386 | } 387 | 388 | // set colors, maybe 389 | 390 | return this; 391 | 392 | }, 393 | 394 | copy: function ( source ) { 395 | 396 | // todo 397 | 398 | return this; 399 | 400 | } 401 | 402 | } ); 403 | 404 | /** 405 | * @author WestLangley / http://github.com/WestLangley 406 | * 407 | */ 408 | 409 | THREE.WireframeGeometry2 = function ( geometry ) { 410 | 411 | THREE.LineSegmentsGeometry.call( this ); 412 | 413 | this.type = 'WireframeGeometry2'; 414 | 415 | this.fromWireframeGeometry( new THREE.WireframeGeometry( geometry ) ); 416 | 417 | // set colors, maybe 418 | 419 | }; 420 | 421 | THREE.WireframeGeometry2.prototype = Object.assign( Object.create( THREE.LineSegmentsGeometry.prototype ), { 422 | 423 | constructor: THREE.WireframeGeometry2, 424 | 425 | isWireframeGeometry2: true, 426 | 427 | copy: function ( source ) { 428 | 429 | // todo 430 | 431 | return this; 432 | 433 | } 434 | 435 | } ); 436 | 437 | /** 438 | * @author WestLangley / http://github.com/WestLangley 439 | * 440 | * parameters = { 441 | * color: , 442 | * linewidth: , 443 | * dashed: , 444 | * dashScale: , 445 | * dashSize: , 446 | * gapSize: , 447 | * resolution: , // to be set by renderer 448 | * } 449 | */ 450 | 451 | THREE.UniformsLib.line = { 452 | 453 | linewidth: { value: 1 }, 454 | resolution: { value: new THREE.Vector2( 1, 1 ) }, 455 | dashScale: { value: 1 }, 456 | dashSize: { value: 1 }, 457 | gapSize: { value: 1 } // todo FIX - maybe change to totalSize 458 | 459 | }; 460 | 461 | THREE.ShaderLib[ 'line' ] = { 462 | 463 | uniforms: THREE.UniformsUtils.merge( [ 464 | THREE.UniformsLib.common, 465 | THREE.UniformsLib.fog, 466 | THREE.UniformsLib.line 467 | ] ), 468 | 469 | vertexShader: 470 | ` 471 | #include 472 | #include 473 | #include 474 | #include 475 | #include 476 | 477 | uniform float linewidth; 478 | uniform vec2 resolution; 479 | 480 | attribute vec3 instanceStart; 481 | attribute vec3 instanceEnd; 482 | 483 | attribute vec3 instanceColorStart; 484 | attribute vec3 instanceColorEnd; 485 | 486 | varying vec2 vUv; 487 | 488 | #ifdef USE_DASH 489 | 490 | uniform float dashScale; 491 | attribute float instanceDistanceStart; 492 | attribute float instanceDistanceEnd; 493 | varying float vLineDistance; 494 | 495 | #endif 496 | 497 | void trimSegment( const in vec4 start, inout vec4 end ) { 498 | 499 | // trim end segment so it terminates between the camera plane and the near plane 500 | 501 | // conservative estimate of the near plane 502 | float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column 503 | float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column 504 | float nearEstimate = - 0.5 * b / a; 505 | 506 | float alpha = ( nearEstimate - start.z ) / ( end.z - start.z ); 507 | 508 | end.xyz = mix( start.xyz, end.xyz, alpha ); 509 | 510 | } 511 | 512 | void main() { 513 | 514 | #ifdef USE_COLOR 515 | 516 | vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd; 517 | 518 | #endif 519 | 520 | #ifdef USE_DASH 521 | 522 | vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd; 523 | 524 | #endif 525 | 526 | float aspect = resolution.x / resolution.y; 527 | 528 | vUv = uv; 529 | 530 | // camera space 531 | vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 ); 532 | vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 ); 533 | 534 | // special case for perspective projection, and segments that terminate either in, or behind, the camera plane 535 | // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space 536 | // but we need to perform ndc-space calculations in the shader, so we must address this issue directly 537 | // perhaps there is a more elegant solution -- WestLangley 538 | 539 | bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column 540 | 541 | if ( perspective ) { 542 | 543 | if ( start.z < 0.0 && end.z >= 0.0 ) { 544 | 545 | trimSegment( start, end ); 546 | 547 | } else if ( end.z < 0.0 && start.z >= 0.0 ) { 548 | 549 | trimSegment( end, start ); 550 | 551 | } 552 | 553 | } 554 | 555 | // clip space 556 | vec4 clipStart = projectionMatrix * start; 557 | vec4 clipEnd = projectionMatrix * end; 558 | 559 | // ndc space 560 | vec2 ndcStart = clipStart.xy / clipStart.w; 561 | vec2 ndcEnd = clipEnd.xy / clipEnd.w; 562 | 563 | // direction 564 | vec2 dir = ndcEnd - ndcStart; 565 | 566 | // account for clip-space aspect ratio 567 | dir.x *= aspect; 568 | dir = normalize( dir ); 569 | 570 | // perpendicular to dir 571 | vec2 offset = vec2( dir.y, - dir.x ); 572 | 573 | // undo aspect ratio adjustment 574 | dir.x /= aspect; 575 | offset.x /= aspect; 576 | 577 | // sign flip 578 | if ( position.x < 0.0 ) offset *= - 1.0; 579 | 580 | // endcaps 581 | if ( position.y < 0.0 ) { 582 | 583 | offset += - dir; 584 | 585 | } else if ( position.y > 1.0 ) { 586 | 587 | offset += dir; 588 | 589 | } 590 | 591 | // adjust for linewidth 592 | offset *= linewidth; 593 | 594 | // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ... 595 | offset /= resolution.y; 596 | 597 | // select end 598 | vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd; 599 | 600 | // back to clip space 601 | offset *= clip.w; 602 | 603 | clip.xy += offset; 604 | 605 | gl_Position = clip; 606 | 607 | #include 608 | 609 | #include 610 | #include 611 | #include 612 | 613 | } 614 | `, 615 | 616 | fragmentShader: 617 | ` 618 | uniform vec3 diffuse; 619 | uniform float opacity; 620 | 621 | #ifdef USE_DASH 622 | 623 | uniform float dashSize; 624 | uniform float gapSize; 625 | 626 | #endif 627 | 628 | varying float vLineDistance; 629 | 630 | #include 631 | #include 632 | #include 633 | #include 634 | #include 635 | 636 | varying vec2 vUv; 637 | 638 | void main() { 639 | 640 | #include 641 | 642 | #ifdef USE_DASH 643 | 644 | if ( vUv.y < 0.5 || vUv.y > 0.5 ) discard; // discard endcaps 645 | 646 | if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX 647 | 648 | #endif 649 | 650 | if ( vUv.y < 0.5 || vUv.y > 0.5 ) { 651 | 652 | float a = vUv.x - 0.5; 653 | float b = vUv.y - 0.5; 654 | float len2 = a * a + b * b; 655 | 656 | if ( len2 > 0.25 ) discard; 657 | 658 | } 659 | 660 | vec4 diffuseColor = vec4( diffuse, opacity ); 661 | 662 | #include 663 | #include 664 | 665 | gl_FragColor = vec4( diffuseColor.rgb, opacity ); 666 | 667 | #include 668 | #include 669 | #include 670 | #include 671 | 672 | } 673 | ` 674 | }; 675 | 676 | THREE.LineMaterial = function ( parameters ) { 677 | 678 | var lineUniforms = THREE.UniformsUtils.clone( THREE.ShaderLib[ 'line' ].uniforms ); 679 | lineUniforms.opacity.value = parameters.opacity; 680 | 681 | THREE.ShaderMaterial.call( this, { 682 | 683 | type: 'LineMaterial', 684 | 685 | uniforms: lineUniforms, 686 | 687 | vertexShader: THREE.ShaderLib[ 'line' ].vertexShader, 688 | fragmentShader: THREE.ShaderLib[ 'line' ].fragmentShader 689 | 690 | } ); 691 | 692 | this.dashed = false; 693 | 694 | Object.defineProperties( this, { 695 | 696 | color: { 697 | 698 | enumerable: true, 699 | 700 | get: function () { 701 | 702 | return this.uniforms.diffuse.value; 703 | 704 | }, 705 | 706 | set: function ( value ) { 707 | 708 | this.uniforms.diffuse.value = value; 709 | 710 | } 711 | 712 | }, 713 | 714 | linewidth: { 715 | 716 | enumerable: true, 717 | 718 | get: function () { 719 | 720 | return this.uniforms.linewidth.value; 721 | 722 | }, 723 | 724 | set: function ( value ) { 725 | 726 | this.uniforms.linewidth.value = value; 727 | 728 | } 729 | 730 | }, 731 | 732 | dashScale: { 733 | 734 | enumerable: true, 735 | 736 | get: function () { 737 | 738 | return this.uniforms.dashScale.value; 739 | 740 | }, 741 | 742 | set: function ( value ) { 743 | 744 | this.uniforms.dashScale.value = value; 745 | 746 | } 747 | 748 | }, 749 | 750 | dashSize: { 751 | 752 | enumerable: true, 753 | 754 | get: function () { 755 | 756 | return this.uniforms.dashSize.value; 757 | 758 | }, 759 | 760 | set: function ( value ) { 761 | 762 | this.uniforms.dashSize.value = value; 763 | 764 | } 765 | 766 | }, 767 | 768 | gapSize: { 769 | 770 | enumerable: true, 771 | 772 | get: function () { 773 | 774 | return this.uniforms.gapSize.value; 775 | 776 | }, 777 | 778 | set: function ( value ) { 779 | 780 | this.uniforms.gapSize.value = value; 781 | 782 | } 783 | 784 | }, 785 | 786 | resolution: { 787 | 788 | enumerable: true, 789 | 790 | get: function () { 791 | 792 | return this.uniforms.resolution.value; 793 | 794 | }, 795 | 796 | set: function ( value ) { 797 | 798 | this.uniforms.resolution.value.copy( value ); 799 | 800 | } 801 | 802 | } 803 | 804 | } ); 805 | 806 | this.setValues( parameters ); 807 | 808 | }; 809 | 810 | THREE.LineMaterial.prototype = Object.create( THREE.ShaderMaterial.prototype ); 811 | THREE.LineMaterial.prototype.constructor = THREE.LineMaterial; 812 | 813 | THREE.LineMaterial.prototype.isLineMaterial = true; 814 | 815 | THREE.LineMaterial.prototype.copy = function ( source ) { 816 | 817 | THREE.ShaderMaterial.prototype.copy.call( this, source ); 818 | 819 | this.color.copy( source.color ); 820 | 821 | this.linewidth = source.linewidth; 822 | 823 | this.resolution = source.resolution; 824 | 825 | // todo 826 | 827 | return this; 828 | 829 | }; 830 | 831 | /** 832 | * @author WestLangley / http://github.com/WestLangley 833 | * 834 | */ 835 | 836 | THREE.LineSegments2 = function ( geometry, material ) { 837 | 838 | THREE.Mesh.call( this ); 839 | 840 | this.type = 'LineSegments2'; 841 | 842 | this.geometry = geometry !== undefined ? geometry : new THREE.LineSegmentsGeometry(); 843 | this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } ); 844 | 845 | }; 846 | 847 | THREE.LineSegments2.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { 848 | 849 | constructor: THREE.LineSegments2, 850 | 851 | isLineSegments2: true, 852 | 853 | computeLineDistances: ( function () { // for backwards-compatability, but could be a method of LineSegmentsGeometry... 854 | 855 | var start = new THREE.Vector3(); 856 | var end = new THREE.Vector3(); 857 | 858 | return function computeLineDistances() { 859 | 860 | var geometry = this.geometry; 861 | 862 | var instanceStart = geometry.attributes.instanceStart; 863 | var instanceEnd = geometry.attributes.instanceEnd; 864 | var lineDistances = new Float32Array( 2 * instanceStart.data.count ); 865 | 866 | for ( var i = 0, j = 0, l = instanceStart.data.count; i < l; i ++, j += 2 ) { 867 | 868 | start.fromBufferAttribute( instanceStart, i ); 869 | end.fromBufferAttribute( instanceEnd, i ); 870 | 871 | lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ]; 872 | lineDistances[ j + 1 ] = lineDistances[ j ] + start.distanceTo( end ); 873 | 874 | } 875 | 876 | var instanceDistanceBuffer = new THREE.InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1 877 | 878 | geometry.addAttribute( 'instanceDistanceStart', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0 879 | geometry.addAttribute( 'instanceDistanceEnd', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1 880 | 881 | return this; 882 | 883 | }; 884 | 885 | }() ), 886 | 887 | copy: function ( source ) { 888 | 889 | // todo 890 | 891 | return this; 892 | 893 | } 894 | 895 | } ); 896 | 897 | /** 898 | * @author WestLangley / http://github.com/WestLangley 899 | * 900 | */ 901 | 902 | THREE.Line2 = function ( geometry, material ) { 903 | 904 | THREE.LineSegments2.call( this ); 905 | 906 | this.type = 'Line2'; 907 | 908 | this.geometry = geometry !== undefined ? geometry : new THREE.LineGeometry(); 909 | this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } ); 910 | 911 | }; 912 | 913 | THREE.Line2.prototype = Object.assign( Object.create( THREE.LineSegments2.prototype ), { 914 | 915 | constructor: THREE.Line2, 916 | 917 | isLine2: true, 918 | 919 | copy: function ( source ) { 920 | 921 | // todo 922 | 923 | return this; 924 | 925 | } 926 | 927 | } ); 928 | 929 | /** 930 | * @author WestLangley / http://github.com/WestLangley 931 | * 932 | */ 933 | 934 | THREE.Wireframe = function ( geometry, material ) { 935 | 936 | THREE.Mesh.call( this ); 937 | 938 | this.type = 'Wireframe'; 939 | 940 | this.geometry = geometry !== undefined ? geometry : new THREE.LineSegmentsGeometry(); 941 | this.material = material !== undefined ? material : new THREE.LineMaterial( { color: Math.random() * 0xffffff } ); 942 | 943 | }; 944 | 945 | THREE.Wireframe.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { 946 | 947 | constructor: THREE.Wireframe, 948 | 949 | isWireframe: true, 950 | 951 | computeLineDistances: ( function () { // for backwards-compatability, but could be a method of LineSegmentsGeometry... 952 | 953 | var start = new THREE.Vector3(); 954 | var end = new THREE.Vector3(); 955 | 956 | return function computeLineDistances() { 957 | 958 | var geometry = this.geometry; 959 | 960 | var instanceStart = geometry.attributes.instanceStart; 961 | var instanceEnd = geometry.attributes.instanceEnd; 962 | var lineDistances = new Float32Array( 2 * instanceStart.data.count ); 963 | 964 | for ( var i = 0, j = 0, l = instanceStart.data.count; i < l; i ++, j += 2 ) { 965 | 966 | start.fromBufferAttribute( instanceStart, i ); 967 | end.fromBufferAttribute( instanceEnd, i ); 968 | 969 | lineDistances[ j ] = ( j === 0 ) ? 0 : lineDistances[ j - 1 ]; 970 | lineDistances[ j + 1 ] = lineDistances[ j ] + start.distanceTo( end ); 971 | 972 | } 973 | 974 | var instanceDistanceBuffer = new THREE.InstancedInterleavedBuffer( lineDistances, 2, 1 ); // d0, d1 975 | 976 | geometry.addAttribute( 'instanceDistanceStart', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 0 ) ); // d0 977 | geometry.addAttribute( 'instanceDistanceEnd', new THREE.InterleavedBufferAttribute( instanceDistanceBuffer, 1, 1 ) ); // d1 978 | 979 | return this; 980 | 981 | }; 982 | 983 | }() ), 984 | 985 | copy: function ( source ) { 986 | 987 | // todo 988 | 989 | return this; 990 | 991 | } 992 | 993 | } ); 994 | -------------------------------------------------------------------------------- /src/objects/loadObj.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils/utils.js"); 2 | var Objects = require('./objects.js'); 3 | const OBJLoader = require("./loaders/OBJLoader.js"); 4 | const MTLLoader = require("./loaders/MTLLoader.js"); 5 | 6 | function loadObj(options, cb){ 7 | 8 | if (options === undefined) return console.error("Invalid options provided to loadObj()"); 9 | 10 | this.loaded = false; 11 | 12 | const modelComplete = (m) => { 13 | console.log("Model complete!", m); 14 | 15 | if(--remaining === 0) this.loaded = true; 16 | } 17 | 18 | 19 | 20 | // TODO: Support formats other than OBJ/MTL 21 | const objLoader = new OBJLoader(); 22 | const materialLoader = new MTLLoader(); 23 | materialLoader.load(options.mtl, loadObject, () => (null), error => { 24 | console.warn("No material file found for SymbolLayer3D model " + m); 25 | }); 26 | 27 | function loadObject(materials) { 28 | 29 | if (materials) { 30 | materials.preload(); 31 | objLoader.setMaterials( materials ); 32 | } 33 | 34 | objLoader.load(options.obj, obj => { 35 | 36 | var r = utils.types.rotation(options, [0, 0, 0]); 37 | var s = utils.types.scale(options, [1, 1, 1]); 38 | 39 | obj = obj.children[0]; 40 | obj.rotation.set(r[0] + Math.PI/2, r[1] + Math.PI, r[2]); 41 | obj.scale.set(s[0], s[1], s[2]); 42 | 43 | var projScaleGroup = new THREE.Group(); 44 | projScaleGroup.add(obj) 45 | var userScaleGroup = Objects.prototype._makeGroup(projScaleGroup, options); 46 | Objects.prototype._addMethods(userScaleGroup); 47 | 48 | cb(userScaleGroup); 49 | 50 | }, () => (null), error => { 51 | console.error("Could not load model file."); 52 | } ); 53 | 54 | }; 55 | } 56 | 57 | 58 | module.exports = exports = loadObj; -------------------------------------------------------------------------------- /src/objects/loaders/MTLLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads a Wavefront .mtl file specifying materials 3 | * 4 | * @author angelxuanchang 5 | */ 6 | 7 | const THREE = require('../../three.js'); 8 | 9 | const MTLLoader = function ( manager ) { 10 | 11 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 12 | 13 | }; 14 | 15 | MTLLoader.prototype = { 16 | 17 | constructor: MTLLoader, 18 | 19 | /** 20 | * Loads and parses a MTL asset from a URL. 21 | * 22 | * @param {String} url - URL to the MTL file. 23 | * @param {Function} [onLoad] - Callback invoked with the loaded object. 24 | * @param {Function} [onProgress] - Callback for download progress. 25 | * @param {Function} [onError] - Callback for download errors. 26 | * 27 | * @see setPath setTexturePath 28 | * 29 | * @note In order for relative texture references to resolve correctly 30 | * you must call setPath and/or setTexturePath explicitly prior to load. 31 | */ 32 | load: function ( url, onLoad, onProgress, onError ) { 33 | 34 | var scope = this; 35 | 36 | var loader = new THREE.FileLoader( this.manager ); 37 | loader.setPath( this.path ); 38 | loader.load( url, function ( text ) { 39 | 40 | onLoad( scope.parse( text ) ); 41 | 42 | }, onProgress, onError ); 43 | 44 | }, 45 | 46 | /** 47 | * Set base path for resolving references. 48 | * If set this path will be prepended to each loaded and found reference. 49 | * 50 | * @see setTexturePath 51 | * @param {String} path 52 | * 53 | * @example 54 | * mtlLoader.setPath( 'assets/obj/' ); 55 | * mtlLoader.load( 'my.mtl', ... ); 56 | */ 57 | setPath: function ( path ) { 58 | 59 | this.path = path; 60 | 61 | }, 62 | 63 | /** 64 | * Set base path for resolving texture references. 65 | * If set this path will be prepended found texture reference. 66 | * If not set and setPath is, it will be used as texture base path. 67 | * 68 | * @see setPath 69 | * @param {String} path 70 | * 71 | * @example 72 | * mtlLoader.setPath( 'assets/obj/' ); 73 | * mtlLoader.setTexturePath( 'assets/textures/' ); 74 | * mtlLoader.load( 'my.mtl', ... ); 75 | */ 76 | setTexturePath: function ( path ) { 77 | 78 | this.texturePath = path; 79 | 80 | }, 81 | 82 | setBaseUrl: function ( path ) { 83 | 84 | console.warn( 'THREE.MTLLoader: .setBaseUrl() is deprecated. Use .setTexturePath( path ) for texture path or .setPath( path ) for general base path instead.' ); 85 | 86 | this.setTexturePath( path ); 87 | 88 | }, 89 | 90 | setCrossOrigin: function ( value ) { 91 | 92 | this.crossOrigin = value; 93 | 94 | }, 95 | 96 | setMaterialOptions: function ( value ) { 97 | 98 | this.materialOptions = value; 99 | 100 | }, 101 | 102 | /** 103 | * Parses a MTL file. 104 | * 105 | * @param {String} text - Content of MTL file 106 | * @return {THREE.MTLLoader.MaterialCreator} 107 | * 108 | * @see setPath setTexturePath 109 | * 110 | * @note In order for relative texture references to resolve correctly 111 | * you must call setPath and/or setTexturePath explicitly prior to parse. 112 | */ 113 | parse: function ( text ) { 114 | 115 | var lines = text.split( '\n' ); 116 | var info = {}; 117 | var delimiter_pattern = /\s+/; 118 | var materialsInfo = {}; 119 | 120 | for ( var i = 0; i < lines.length; i ++ ) { 121 | 122 | var line = lines[ i ]; 123 | line = line.trim(); 124 | 125 | if ( line.length === 0 || line.charAt( 0 ) === '#' ) { 126 | 127 | // Blank line or comment ignore 128 | continue; 129 | 130 | } 131 | 132 | var pos = line.indexOf( ' ' ); 133 | 134 | var key = ( pos >= 0 ) ? line.substring( 0, pos ) : line; 135 | key = key.toLowerCase(); 136 | 137 | var value = ( pos >= 0 ) ? line.substring( pos + 1 ) : ''; 138 | value = value.trim(); 139 | 140 | if ( key === 'newmtl' ) { 141 | 142 | // New material 143 | 144 | info = { name: value }; 145 | materialsInfo[ value ] = info; 146 | 147 | } else if ( info ) { 148 | 149 | if ( key === 'ka' || key === 'kd' || key === 'ks' ) { 150 | 151 | var ss = value.split( delimiter_pattern, 3 ); 152 | info[ key ] = [ parseFloat( ss[ 0 ] ), parseFloat( ss[ 1 ] ), parseFloat( ss[ 2 ] ) ]; 153 | 154 | } else { 155 | 156 | info[ key ] = value; 157 | 158 | } 159 | 160 | } 161 | 162 | } 163 | 164 | var materialCreator = new MTLLoader.MaterialCreator( this.texturePath || this.path, this.materialOptions ); 165 | materialCreator.setCrossOrigin( this.crossOrigin ); 166 | materialCreator.setManager( this.manager ); 167 | materialCreator.setMaterials( materialsInfo ); 168 | return materialCreator; 169 | 170 | } 171 | 172 | }; 173 | 174 | /** 175 | * Create a new THREE-MTLLoader.MaterialCreator 176 | * @param baseUrl - Url relative to which textures are loaded 177 | * @param options - Set of options on how to construct the materials 178 | * side: Which side to apply the material 179 | * THREE.FrontSide (default), THREE.BackSide, THREE.DoubleSide 180 | * wrap: What type of wrapping to apply for textures 181 | * THREE.RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping 182 | * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 183 | * Default: false, assumed to be already normalized 184 | * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's 185 | * Default: false 186 | * @constructor 187 | */ 188 | 189 | MTLLoader.MaterialCreator = function ( baseUrl, options ) { 190 | 191 | this.baseUrl = baseUrl || ''; 192 | this.options = options; 193 | this.materialsInfo = {}; 194 | this.materials = {}; 195 | this.materialsArray = []; 196 | this.nameLookup = {}; 197 | 198 | this.side = ( this.options && this.options.side ) ? this.options.side : THREE.FrontSide; 199 | this.wrap = ( this.options && this.options.wrap ) ? this.options.wrap : THREE.RepeatWrapping; 200 | 201 | }; 202 | 203 | MTLLoader.MaterialCreator.prototype = { 204 | 205 | constructor: MTLLoader.MaterialCreator, 206 | 207 | setCrossOrigin: function ( value ) { 208 | 209 | this.crossOrigin = value; 210 | 211 | }, 212 | 213 | setManager: function ( value ) { 214 | 215 | this.manager = value; 216 | 217 | }, 218 | 219 | setMaterials: function ( materialsInfo ) { 220 | 221 | this.materialsInfo = this.convert( materialsInfo ); 222 | this.materials = {}; 223 | this.materialsArray = []; 224 | this.nameLookup = {}; 225 | 226 | }, 227 | 228 | convert: function ( materialsInfo ) { 229 | 230 | if ( ! this.options ) return materialsInfo; 231 | 232 | var converted = {}; 233 | 234 | for ( var mn in materialsInfo ) { 235 | 236 | // Convert materials info into normalized form based on options 237 | 238 | var mat = materialsInfo[ mn ]; 239 | 240 | var covmat = {}; 241 | 242 | converted[ mn ] = covmat; 243 | 244 | for ( var prop in mat ) { 245 | 246 | var save = true; 247 | var value = mat[ prop ]; 248 | var lprop = prop.toLowerCase(); 249 | 250 | switch ( lprop ) { 251 | 252 | case 'kd': 253 | case 'ka': 254 | case 'ks': 255 | 256 | // Diffuse color (color under white light) using RGB values 257 | 258 | if ( this.options && this.options.normalizeRGB ) { 259 | 260 | value = [ value[ 0 ] / 255, value[ 1 ] / 255, value[ 2 ] / 255 ]; 261 | 262 | } 263 | 264 | if ( this.options && this.options.ignoreZeroRGBs ) { 265 | 266 | if ( value[ 0 ] === 0 && value[ 1 ] === 0 && value[ 2 ] === 0 ) { 267 | 268 | // ignore 269 | 270 | save = false; 271 | 272 | } 273 | 274 | } 275 | 276 | break; 277 | 278 | default: 279 | 280 | break; 281 | 282 | } 283 | 284 | if ( save ) { 285 | 286 | covmat[ lprop ] = value; 287 | 288 | } 289 | 290 | } 291 | 292 | } 293 | 294 | return converted; 295 | 296 | }, 297 | 298 | preload: function () { 299 | 300 | for ( var mn in this.materialsInfo ) { 301 | 302 | this.create( mn ); 303 | 304 | } 305 | 306 | }, 307 | 308 | getIndex: function ( materialName ) { 309 | 310 | return this.nameLookup[ materialName ]; 311 | 312 | }, 313 | 314 | getAsArray: function () { 315 | 316 | var index = 0; 317 | 318 | for ( var mn in this.materialsInfo ) { 319 | 320 | this.materialsArray[ index ] = this.create( mn ); 321 | this.nameLookup[ mn ] = index; 322 | index ++; 323 | 324 | } 325 | 326 | return this.materialsArray; 327 | 328 | }, 329 | 330 | create: function ( materialName ) { 331 | 332 | if ( this.materials[ materialName ] === undefined ) { 333 | 334 | this.createMaterial_( materialName ); 335 | 336 | } 337 | 338 | return this.materials[ materialName ]; 339 | 340 | }, 341 | 342 | createMaterial_: function ( materialName ) { 343 | 344 | // Create material 345 | 346 | var scope = this; 347 | var mat = this.materialsInfo[ materialName ]; 348 | var params = { 349 | 350 | name: materialName, 351 | side: this.side 352 | 353 | }; 354 | 355 | function resolveURL( baseUrl, url ) { 356 | 357 | if ( typeof url !== 'string' || url === '' ) 358 | return ''; 359 | 360 | // Absolute URL 361 | if ( /^https?:\/\//i.test( url ) ) return url; 362 | 363 | return baseUrl + url; 364 | 365 | } 366 | 367 | function setMapForType( mapType, value ) { 368 | 369 | if ( params[ mapType ] ) return; // Keep the first encountered texture 370 | 371 | var texParams = scope.getTextureParams( value, params ); 372 | var map = scope.loadTexture( resolveURL( scope.baseUrl, texParams.url ) ); 373 | 374 | map.repeat.copy( texParams.scale ); 375 | map.offset.copy( texParams.offset ); 376 | 377 | map.wrapS = scope.wrap; 378 | map.wrapT = scope.wrap; 379 | 380 | params[ mapType ] = map; 381 | 382 | } 383 | 384 | for ( var prop in mat ) { 385 | 386 | var value = mat[ prop ]; 387 | 388 | if ( value === '' ) continue; 389 | 390 | switch ( prop.toLowerCase() ) { 391 | 392 | // Ns is material specular exponent 393 | 394 | case 'kd': 395 | 396 | // Diffuse color (color under white light) using RGB values 397 | 398 | params.color = new THREE.Color().fromArray( value ); 399 | 400 | break; 401 | 402 | case 'ks': 403 | 404 | // Specular color (color when light is reflected from shiny surface) using RGB values 405 | params.specular = new THREE.Color().fromArray( value ); 406 | 407 | break; 408 | 409 | case 'map_kd': 410 | 411 | // Diffuse texture map 412 | 413 | setMapForType( "map", value ); 414 | 415 | break; 416 | 417 | case 'map_ks': 418 | 419 | // Specular map 420 | 421 | setMapForType( "specularMap", value ); 422 | 423 | break; 424 | 425 | case 'map_bump': 426 | case 'bump': 427 | 428 | // Bump texture map 429 | 430 | setMapForType( "bumpMap", value ); 431 | 432 | break; 433 | 434 | case 'ns': 435 | 436 | // The specular exponent (defines the focus of the specular highlight) 437 | // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. 438 | params.shininess = parseFloat( value ); 439 | 440 | break; 441 | 442 | case 'd': 443 | 444 | if ( value < 1 ) { 445 | 446 | params.opacity = value; 447 | params.transparent = true; 448 | 449 | } 450 | 451 | break; 452 | 453 | case 'Tr': 454 | 455 | if ( value > 0 ) { 456 | 457 | params.opacity = 1 - value; 458 | params.transparent = true; 459 | 460 | } 461 | 462 | break; 463 | 464 | default: 465 | break; 466 | 467 | } 468 | 469 | } 470 | 471 | this.materials[ materialName ] = new THREE.MeshPhongMaterial( params ); 472 | return this.materials[ materialName ]; 473 | 474 | }, 475 | 476 | getTextureParams: function ( value, matParams ) { 477 | 478 | var texParams = { 479 | 480 | scale: new THREE.Vector2( 1, 1 ), 481 | offset: new THREE.Vector2( 0, 0 ) 482 | 483 | }; 484 | 485 | var items = value.split( /\s+/ ); 486 | var pos; 487 | 488 | pos = items.indexOf( '-bm' ); 489 | 490 | if ( pos >= 0 ) { 491 | 492 | matParams.bumpScale = parseFloat( items[ pos + 1 ] ); 493 | items.splice( pos, 2 ); 494 | 495 | } 496 | 497 | pos = items.indexOf( '-s' ); 498 | 499 | if ( pos >= 0 ) { 500 | 501 | texParams.scale.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) ); 502 | items.splice( pos, 4 ); // we expect 3 parameters here! 503 | 504 | } 505 | 506 | pos = items.indexOf( '-o' ); 507 | 508 | if ( pos >= 0 ) { 509 | 510 | texParams.offset.set( parseFloat( items[ pos + 1 ] ), parseFloat( items[ pos + 2 ] ) ); 511 | items.splice( pos, 4 ); // we expect 3 parameters here! 512 | 513 | } 514 | 515 | texParams.url = items.join( ' ' ).trim(); 516 | return texParams; 517 | 518 | }, 519 | 520 | loadTexture: function ( url, mapping, onLoad, onProgress, onError ) { 521 | 522 | var texture; 523 | var loader = THREE.Loader.Handlers.get( url ); 524 | var manager = ( this.manager !== undefined ) ? this.manager : THREE.DefaultLoadingManager; 525 | 526 | if ( loader === null ) { 527 | 528 | loader = new THREE.TextureLoader( manager ); 529 | 530 | } 531 | 532 | if ( loader.setCrossOrigin ) loader.setCrossOrigin( this.crossOrigin ); 533 | texture = loader.load( url, onLoad, onProgress, onError ); 534 | 535 | if ( mapping !== undefined ) texture.mapping = mapping; 536 | 537 | return texture; 538 | 539 | } 540 | 541 | }; 542 | 543 | module.exports = exports = MTLLoader; -------------------------------------------------------------------------------- /src/objects/loaders/OBJLoader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | const THREE = require('../../three.js'); 5 | 6 | const OBJLoader = function ( manager ) { 7 | 8 | this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager; 9 | 10 | this.materials = null; 11 | 12 | this.regexp = { 13 | // v float float float 14 | vertex_pattern : /^v\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 15 | // vn float float float 16 | normal_pattern : /^vn\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 17 | // vt float float 18 | uv_pattern : /^vt\s+([\d|\.|\+|\-|e|E]+)\s+([\d|\.|\+|\-|e|E]+)/, 19 | // f vertex vertex vertex 20 | face_vertex : /^f\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)(?:\s+(-?\d+))?/, 21 | // f vertex/uv vertex/uv vertex/uv 22 | face_vertex_uv : /^f\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+))?/, 23 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 24 | face_vertex_uv_normal : /^f\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)\s+(-?\d+)\/(-?\d+)\/(-?\d+)(?:\s+(-?\d+)\/(-?\d+)\/(-?\d+))?/, 25 | // f vertex//normal vertex//normal vertex//normal 26 | face_vertex_normal : /^f\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)\s+(-?\d+)\/\/(-?\d+)(?:\s+(-?\d+)\/\/(-?\d+))?/, 27 | // o object_name | g group_name 28 | object_pattern : /^[og]\s*(.+)?/, 29 | // s boolean 30 | smoothing_pattern : /^s\s+(\d+|on|off)/, 31 | // mtllib file_reference 32 | material_library_pattern : /^mtllib /, 33 | // usemtl material_name 34 | material_use_pattern : /^usemtl / 35 | }; 36 | 37 | }; 38 | 39 | OBJLoader.prototype = { 40 | 41 | constructor: OBJLoader, 42 | 43 | load: function ( url, onLoad, onProgress, onError ) { 44 | 45 | var scope = this; 46 | 47 | var loader = new THREE.FileLoader( scope.manager ); 48 | loader.setPath( this.path ); 49 | loader.load( url, function ( text ) { 50 | 51 | onLoad( scope.parse( text ) ); 52 | 53 | }, onProgress, onError ); 54 | 55 | }, 56 | 57 | setPath: function ( value ) { 58 | 59 | this.path = value; 60 | 61 | }, 62 | 63 | setMaterials: function ( materials ) { 64 | 65 | this.materials = materials; 66 | 67 | }, 68 | 69 | _createParserState : function () { 70 | 71 | var state = { 72 | objects : [], 73 | object : {}, 74 | 75 | vertices : [], 76 | normals : [], 77 | uvs : [], 78 | 79 | materialLibraries : [], 80 | 81 | startObject: function ( name, fromDeclaration ) { 82 | 83 | // If the current object (initial from reset) is not from a g/o declaration in the parsed 84 | // file. We need to use it for the first parsed g/o to keep things in sync. 85 | if ( this.object && this.object.fromDeclaration === false ) { 86 | 87 | this.object.name = name; 88 | this.object.fromDeclaration = ( fromDeclaration !== false ); 89 | return; 90 | 91 | } 92 | 93 | var previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined ); 94 | 95 | if ( this.object && typeof this.object._finalize === 'function' ) { 96 | 97 | this.object._finalize( true ); 98 | 99 | } 100 | 101 | this.object = { 102 | name : name || '', 103 | fromDeclaration : ( fromDeclaration !== false ), 104 | 105 | geometry : { 106 | vertices : [], 107 | normals : [], 108 | uvs : [] 109 | }, 110 | materials : [], 111 | smooth : true, 112 | 113 | startMaterial : function( name, libraries ) { 114 | 115 | var previous = this._finalize( false ); 116 | 117 | // New usemtl declaration overwrites an inherited material, except if faces were declared 118 | // after the material, then it must be preserved for proper MultiMaterial continuation. 119 | if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { 120 | 121 | this.materials.splice( previous.index, 1 ); 122 | 123 | } 124 | 125 | var material = { 126 | index : this.materials.length, 127 | name : name || '', 128 | mtllib : ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ), 129 | smooth : ( previous !== undefined ? previous.smooth : this.smooth ), 130 | groupStart : ( previous !== undefined ? previous.groupEnd : 0 ), 131 | groupEnd : -1, 132 | groupCount : -1, 133 | inherited : false, 134 | 135 | clone : function( index ) { 136 | var cloned = { 137 | index : ( typeof index === 'number' ? index : this.index ), 138 | name : this.name, 139 | mtllib : this.mtllib, 140 | smooth : this.smooth, 141 | groupStart : 0, 142 | groupEnd : -1, 143 | groupCount : -1, 144 | inherited : false 145 | }; 146 | cloned.clone = this.clone.bind(cloned); 147 | return cloned; 148 | } 149 | }; 150 | 151 | this.materials.push( material ); 152 | 153 | return material; 154 | 155 | }, 156 | 157 | currentMaterial : function() { 158 | 159 | if ( this.materials.length > 0 ) { 160 | return this.materials[ this.materials.length - 1 ]; 161 | } 162 | 163 | return undefined; 164 | 165 | }, 166 | 167 | _finalize : function( end ) { 168 | 169 | var lastMultiMaterial = this.currentMaterial(); 170 | if ( lastMultiMaterial && lastMultiMaterial.groupEnd === -1 ) { 171 | 172 | lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; 173 | lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; 174 | lastMultiMaterial.inherited = false; 175 | 176 | } 177 | 178 | // Ignore objects tail materials if no face declarations followed them before a new o/g started. 179 | if ( end && this.materials.length > 1 ) { 180 | 181 | for ( var mi = this.materials.length - 1; mi >= 0; mi-- ) { 182 | if ( this.materials[mi].groupCount <= 0 ) { 183 | this.materials.splice( mi, 1 ); 184 | } 185 | } 186 | 187 | } 188 | 189 | // Guarantee at least one empty material, this makes the creation later more straight forward. 190 | if ( end && this.materials.length === 0 ) { 191 | 192 | this.materials.push({ 193 | name : '', 194 | smooth : this.smooth 195 | }); 196 | 197 | } 198 | 199 | return lastMultiMaterial; 200 | 201 | } 202 | }; 203 | 204 | // Inherit previous objects material. 205 | // Spec tells us that a declared material must be set to all objects until a new material is declared. 206 | // If a usemtl declaration is encountered while this new object is being parsed, it will 207 | // overwrite the inherited material. Exception being that there was already face declarations 208 | // to the inherited material, then it will be preserved for proper MultiMaterial continuation. 209 | 210 | if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === "function" ) { 211 | 212 | var declared = previousMaterial.clone( 0 ); 213 | declared.inherited = true; 214 | this.object.materials.push( declared ); 215 | 216 | } 217 | 218 | this.objects.push( this.object ); 219 | 220 | }, 221 | 222 | finalize : function() { 223 | 224 | if ( this.object && typeof this.object._finalize === 'function' ) { 225 | 226 | this.object._finalize( true ); 227 | 228 | } 229 | 230 | }, 231 | 232 | parseVertexIndex: function ( value, len ) { 233 | 234 | var index = parseInt( value, 10 ); 235 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 236 | 237 | }, 238 | 239 | parseNormalIndex: function ( value, len ) { 240 | 241 | var index = parseInt( value, 10 ); 242 | return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; 243 | 244 | }, 245 | 246 | parseUVIndex: function ( value, len ) { 247 | 248 | var index = parseInt( value, 10 ); 249 | return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; 250 | 251 | }, 252 | 253 | addVertex: function ( a, b, c ) { 254 | 255 | var src = this.vertices; 256 | var dst = this.object.geometry.vertices; 257 | 258 | dst.push( src[ a + 0 ] ); 259 | dst.push( src[ a + 1 ] ); 260 | dst.push( src[ a + 2 ] ); 261 | dst.push( src[ b + 0 ] ); 262 | dst.push( src[ b + 1 ] ); 263 | dst.push( src[ b + 2 ] ); 264 | dst.push( src[ c + 0 ] ); 265 | dst.push( src[ c + 1 ] ); 266 | dst.push( src[ c + 2 ] ); 267 | 268 | }, 269 | 270 | addVertexLine: function ( a ) { 271 | 272 | var src = this.vertices; 273 | var dst = this.object.geometry.vertices; 274 | 275 | dst.push( src[ a + 0 ] ); 276 | dst.push( src[ a + 1 ] ); 277 | dst.push( src[ a + 2 ] ); 278 | 279 | }, 280 | 281 | addNormal : function ( a, b, c ) { 282 | 283 | var src = this.normals; 284 | var dst = this.object.geometry.normals; 285 | 286 | dst.push( src[ a + 0 ] ); 287 | dst.push( src[ a + 1 ] ); 288 | dst.push( src[ a + 2 ] ); 289 | dst.push( src[ b + 0 ] ); 290 | dst.push( src[ b + 1 ] ); 291 | dst.push( src[ b + 2 ] ); 292 | dst.push( src[ c + 0 ] ); 293 | dst.push( src[ c + 1 ] ); 294 | dst.push( src[ c + 2 ] ); 295 | 296 | }, 297 | 298 | addUV: function ( a, b, c ) { 299 | 300 | var src = this.uvs; 301 | var dst = this.object.geometry.uvs; 302 | 303 | dst.push( src[ a + 0 ] ); 304 | dst.push( src[ a + 1 ] ); 305 | dst.push( src[ b + 0 ] ); 306 | dst.push( src[ b + 1 ] ); 307 | dst.push( src[ c + 0 ] ); 308 | dst.push( src[ c + 1 ] ); 309 | 310 | }, 311 | 312 | addUVLine: function ( a ) { 313 | 314 | var src = this.uvs; 315 | var dst = this.object.geometry.uvs; 316 | 317 | dst.push( src[ a + 0 ] ); 318 | dst.push( src[ a + 1 ] ); 319 | 320 | }, 321 | 322 | addFace: function ( a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd ) { 323 | 324 | var vLen = this.vertices.length; 325 | 326 | var ia = this.parseVertexIndex( a, vLen ); 327 | var ib = this.parseVertexIndex( b, vLen ); 328 | var ic = this.parseVertexIndex( c, vLen ); 329 | var id; 330 | 331 | if ( d === undefined ) { 332 | 333 | this.addVertex( ia, ib, ic ); 334 | 335 | } else { 336 | 337 | id = this.parseVertexIndex( d, vLen ); 338 | 339 | this.addVertex( ia, ib, id ); 340 | this.addVertex( ib, ic, id ); 341 | 342 | } 343 | 344 | if ( ua !== undefined ) { 345 | 346 | var uvLen = this.uvs.length; 347 | 348 | ia = this.parseUVIndex( ua, uvLen ); 349 | ib = this.parseUVIndex( ub, uvLen ); 350 | ic = this.parseUVIndex( uc, uvLen ); 351 | 352 | if ( d === undefined ) { 353 | 354 | this.addUV( ia, ib, ic ); 355 | 356 | } else { 357 | 358 | id = this.parseUVIndex( ud, uvLen ); 359 | 360 | this.addUV( ia, ib, id ); 361 | this.addUV( ib, ic, id ); 362 | 363 | } 364 | 365 | } 366 | 367 | if ( na !== undefined ) { 368 | 369 | // Normals are many times the same. If so, skip function call and parseInt. 370 | var nLen = this.normals.length; 371 | ia = this.parseNormalIndex( na, nLen ); 372 | 373 | ib = na === nb ? ia : this.parseNormalIndex( nb, nLen ); 374 | ic = na === nc ? ia : this.parseNormalIndex( nc, nLen ); 375 | 376 | if ( d === undefined ) { 377 | 378 | this.addNormal( ia, ib, ic ); 379 | 380 | } else { 381 | 382 | id = this.parseNormalIndex( nd, nLen ); 383 | 384 | this.addNormal( ia, ib, id ); 385 | this.addNormal( ib, ic, id ); 386 | 387 | } 388 | 389 | } 390 | 391 | }, 392 | 393 | addLineGeometry: function ( vertices, uvs ) { 394 | 395 | this.object.geometry.type = 'Line'; 396 | 397 | var vLen = this.vertices.length; 398 | var uvLen = this.uvs.length; 399 | 400 | for ( var vi = 0, l = vertices.length; vi < l; vi ++ ) { 401 | 402 | this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); 403 | 404 | } 405 | 406 | for ( var uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { 407 | 408 | this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); 409 | 410 | } 411 | 412 | } 413 | 414 | }; 415 | 416 | state.startObject( '', false ); 417 | 418 | return state; 419 | 420 | }, 421 | 422 | parse: function ( text ) { 423 | 424 | //console.time( 'OBJLoader' ); 425 | 426 | var state = this._createParserState(); 427 | 428 | if ( text.indexOf( '\r\n' ) !== - 1 ) { 429 | 430 | // This is faster than String.split with regex that splits on both 431 | text = text.replace( /\r\n/g, '\n' ); 432 | 433 | } 434 | 435 | if ( text.indexOf( '\\\n' ) !== - 1) { 436 | 437 | // join lines separated by a line continuation character (\) 438 | text = text.replace( /\\\n/g, '' ); 439 | 440 | } 441 | 442 | var lines = text.split( '\n' ); 443 | var line = '', lineFirstChar = '', lineSecondChar = ''; 444 | var lineLength = 0; 445 | var result = []; 446 | 447 | // Faster to just trim left side of the line. Use if available. 448 | var trimLeft = ( typeof ''.trimLeft === 'function' ); 449 | 450 | for ( var i = 0, l = lines.length; i < l; i ++ ) { 451 | 452 | line = lines[ i ]; 453 | 454 | line = trimLeft ? line.trimLeft() : line.trim(); 455 | 456 | lineLength = line.length; 457 | 458 | if ( lineLength === 0 ) continue; 459 | 460 | lineFirstChar = line.charAt( 0 ); 461 | 462 | // @todo invoke passed in handler if any 463 | if ( lineFirstChar === '#' ) continue; 464 | 465 | if ( lineFirstChar === 'v' ) { 466 | 467 | lineSecondChar = line.charAt( 1 ); 468 | 469 | if ( lineSecondChar === ' ' && ( result = this.regexp.vertex_pattern.exec( line ) ) !== null ) { 470 | 471 | // 0 1 2 3 472 | // ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 473 | 474 | state.vertices.push( 475 | parseFloat( result[ 1 ] ), 476 | parseFloat( result[ 2 ] ), 477 | parseFloat( result[ 3 ] ) 478 | ); 479 | 480 | } else if ( lineSecondChar === 'n' && ( result = this.regexp.normal_pattern.exec( line ) ) !== null ) { 481 | 482 | // 0 1 2 3 483 | // ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"] 484 | 485 | state.normals.push( 486 | parseFloat( result[ 1 ] ), 487 | parseFloat( result[ 2 ] ), 488 | parseFloat( result[ 3 ] ) 489 | ); 490 | 491 | } else if ( lineSecondChar === 't' && ( result = this.regexp.uv_pattern.exec( line ) ) !== null ) { 492 | 493 | // 0 1 2 494 | // ["vt 0.1 0.2", "0.1", "0.2"] 495 | 496 | state.uvs.push( 497 | parseFloat( result[ 1 ] ), 498 | parseFloat( result[ 2 ] ) 499 | ); 500 | 501 | } else { 502 | 503 | throw new Error( "Unexpected vertex/normal/uv line: '" + line + "'" ); 504 | 505 | } 506 | 507 | } else if ( lineFirstChar === "f" ) { 508 | 509 | if ( ( result = this.regexp.face_vertex_uv_normal.exec( line ) ) !== null ) { 510 | 511 | // f vertex/uv/normal vertex/uv/normal vertex/uv/normal 512 | // 0 1 2 3 4 5 6 7 8 9 10 11 12 513 | // ["f 1/1/1 2/2/2 3/3/3", "1", "1", "1", "2", "2", "2", "3", "3", "3", undefined, undefined, undefined] 514 | 515 | state.addFace( 516 | result[ 1 ], result[ 4 ], result[ 7 ], result[ 10 ], 517 | result[ 2 ], result[ 5 ], result[ 8 ], result[ 11 ], 518 | result[ 3 ], result[ 6 ], result[ 9 ], result[ 12 ] 519 | ); 520 | 521 | } else if ( ( result = this.regexp.face_vertex_uv.exec( line ) ) !== null ) { 522 | 523 | // f vertex/uv vertex/uv vertex/uv 524 | // 0 1 2 3 4 5 6 7 8 525 | // ["f 1/1 2/2 3/3", "1", "1", "2", "2", "3", "3", undefined, undefined] 526 | 527 | state.addFace( 528 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 529 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 530 | ); 531 | 532 | } else if ( ( result = this.regexp.face_vertex_normal.exec( line ) ) !== null ) { 533 | 534 | // f vertex//normal vertex//normal vertex//normal 535 | // 0 1 2 3 4 5 6 7 8 536 | // ["f 1//1 2//2 3//3", "1", "1", "2", "2", "3", "3", undefined, undefined] 537 | 538 | state.addFace( 539 | result[ 1 ], result[ 3 ], result[ 5 ], result[ 7 ], 540 | undefined, undefined, undefined, undefined, 541 | result[ 2 ], result[ 4 ], result[ 6 ], result[ 8 ] 542 | ); 543 | 544 | } else if ( ( result = this.regexp.face_vertex.exec( line ) ) !== null ) { 545 | 546 | // f vertex vertex vertex 547 | // 0 1 2 3 4 548 | // ["f 1 2 3", "1", "2", "3", undefined] 549 | 550 | state.addFace( 551 | result[ 1 ], result[ 2 ], result[ 3 ], result[ 4 ] 552 | ); 553 | 554 | } else { 555 | 556 | throw new Error( "Unexpected face line: '" + line + "'" ); 557 | 558 | } 559 | 560 | } else if ( lineFirstChar === "l" ) { 561 | 562 | var lineParts = line.substring( 1 ).trim().split( " " ); 563 | var lineVertices = [], lineUVs = []; 564 | 565 | if ( line.indexOf( "/" ) === - 1 ) { 566 | 567 | lineVertices = lineParts; 568 | 569 | } else { 570 | 571 | for ( var li = 0, llen = lineParts.length; li < llen; li ++ ) { 572 | 573 | var parts = lineParts[ li ].split( "/" ); 574 | 575 | if ( parts[ 0 ] !== "" ) lineVertices.push( parts[ 0 ] ); 576 | if ( parts[ 1 ] !== "" ) lineUVs.push( parts[ 1 ] ); 577 | 578 | } 579 | 580 | } 581 | state.addLineGeometry( lineVertices, lineUVs ); 582 | 583 | } else if ( ( result = this.regexp.object_pattern.exec( line ) ) !== null ) { 584 | 585 | // o object_name 586 | // or 587 | // g group_name 588 | 589 | // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 590 | // var name = result[ 0 ].substr( 1 ).trim(); 591 | var name = ( " " + result[ 0 ].substr( 1 ).trim() ).substr( 1 ); 592 | 593 | state.startObject( name ); 594 | 595 | } else if ( this.regexp.material_use_pattern.test( line ) ) { 596 | 597 | // material 598 | 599 | state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); 600 | 601 | } else if ( this.regexp.material_library_pattern.test( line ) ) { 602 | 603 | // mtl file 604 | 605 | state.materialLibraries.push( line.substring( 7 ).trim() ); 606 | 607 | } else if ( ( result = this.regexp.smoothing_pattern.exec( line ) ) !== null ) { 608 | 609 | // smooth shading 610 | 611 | // @todo Handle files that have varying smooth values for a set of faces inside one geometry, 612 | // but does not define a usemtl for each face set. 613 | // This should be detected and a dummy material created (later MultiMaterial and geometry groups). 614 | // This requires some care to not create extra material on each smooth value for "normal" obj files. 615 | // where explicit usemtl defines geometry groups. 616 | // Example asset: examples/models/obj/cerberus/Cerberus.obj 617 | 618 | var value = result[ 1 ].trim().toLowerCase(); 619 | state.object.smooth = ( value === '1' || value === 'on' ); 620 | 621 | var material = state.object.currentMaterial(); 622 | if ( material ) { 623 | 624 | material.smooth = state.object.smooth; 625 | 626 | } 627 | 628 | } else { 629 | 630 | // Handle null terminated files without exception 631 | if ( line === '\0' ) continue; 632 | 633 | throw new Error( "Unexpected line: '" + line + "'" ); 634 | 635 | } 636 | 637 | } 638 | 639 | state.finalize(); 640 | 641 | var container = new THREE.Group(); 642 | container.materialLibraries = [].concat( state.materialLibraries ); 643 | 644 | for ( var i = 0, l = state.objects.length; i < l; i ++ ) { 645 | 646 | var object = state.objects[ i ]; 647 | var geometry = object.geometry; 648 | var materials = object.materials; 649 | var isLine = ( geometry.type === 'Line' ); 650 | 651 | // Skip o/g line declarations that did not follow with any faces 652 | if ( geometry.vertices.length === 0 ) continue; 653 | 654 | var buffergeometry = new THREE.BufferGeometry(); 655 | 656 | buffergeometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( geometry.vertices ), 3 ) ); 657 | 658 | if ( geometry.normals.length > 0 ) { 659 | 660 | buffergeometry.addAttribute( 'normal', new THREE.BufferAttribute( new Float32Array( geometry.normals ), 3 ) ); 661 | 662 | } else { 663 | 664 | buffergeometry.computeVertexNormals(); 665 | 666 | } 667 | 668 | if ( geometry.uvs.length > 0 ) { 669 | 670 | buffergeometry.addAttribute( 'uv', new THREE.BufferAttribute( new Float32Array( geometry.uvs ), 2 ) ); 671 | 672 | } 673 | 674 | // Create materials 675 | 676 | var createdMaterials = []; 677 | 678 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 679 | 680 | var sourceMaterial = materials[mi]; 681 | var material = undefined; 682 | 683 | if ( this.materials !== null ) { 684 | 685 | material = this.materials.create( sourceMaterial.name ); 686 | 687 | // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. 688 | if ( isLine && material && ! ( material instanceof THREE.LineBasicMaterial ) ) { 689 | 690 | var materialLine = new THREE.LineBasicMaterial(); 691 | materialLine.copy( material ); 692 | materialLine.lights = false; 693 | material = materialLine; 694 | 695 | } 696 | 697 | } 698 | 699 | if ( ! material ) { 700 | 701 | material = ( ! isLine ? new THREE.MeshPhongMaterial() : new THREE.LineBasicMaterial() ); 702 | material.name = sourceMaterial.name; 703 | 704 | } 705 | 706 | material.shading = sourceMaterial.smooth ? THREE.SmoothShading : THREE.FlatShading; 707 | 708 | createdMaterials.push(material); 709 | 710 | } 711 | 712 | // Create mesh 713 | 714 | var mesh; 715 | 716 | if ( createdMaterials.length > 1 ) { 717 | 718 | for ( var mi = 0, miLen = materials.length; mi < miLen ; mi++ ) { 719 | 720 | var sourceMaterial = materials[mi]; 721 | buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); 722 | 723 | } 724 | 725 | var multiMaterial = new THREE.MultiMaterial( createdMaterials ); 726 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, multiMaterial ) : new THREE.LineSegments( buffergeometry, multiMaterial ) ); 727 | 728 | } else { 729 | 730 | mesh = ( ! isLine ? new THREE.Mesh( buffergeometry, createdMaterials[ 0 ] ) : new THREE.LineSegments( buffergeometry, createdMaterials[ 0 ] ) ); 731 | } 732 | 733 | mesh.name = object.name; 734 | 735 | container.add( mesh ); 736 | 737 | } 738 | 739 | //console.timeEnd( 'OBJLoader' ); 740 | 741 | return container; 742 | 743 | } 744 | 745 | }; 746 | 747 | module.exports = exports = OBJLoader; -------------------------------------------------------------------------------- /src/objects/objects.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils/utils.js"); 2 | var material = require("../utils/material.js"); 3 | 4 | const AnimationManager = require("../animation/AnimationManager.js"); 5 | 6 | 7 | function Objects(){ 8 | 9 | } 10 | 11 | Objects.prototype = { 12 | 13 | // standard 1px line with gl 14 | line: function(obj){ 15 | 16 | obj = utils._validate(obj, this._defaults.line); 17 | 18 | //project to world and normalize 19 | var straightProject = utils.lnglatsToWorld(obj.geometry); 20 | var normalized = utils.normalizeVertices(straightProject); 21 | 22 | //flatten array for buffergeometry 23 | var flattenedArray = utils.flattenVectors(normalized.vertices); 24 | 25 | var positions = new Float32Array(flattenedArray); // 3 vertices per point 26 | var geometry = new THREE.BufferGeometry(); 27 | geometry.addAttribute( 'position', new THREE.BufferAttribute( positions, 3 ) ); 28 | 29 | // material 30 | var material = new THREE.LineBasicMaterial( { color: 0xff0000, linewidth: 21 } ); 31 | var line = new THREE.Line( geometry, material ); 32 | 33 | line.options = options || {}; 34 | line.position.copy(normalized.position) 35 | 36 | return line 37 | }, 38 | 39 | 40 | extrusion: function(options){ 41 | 42 | }, 43 | 44 | _addMethods: function(obj, static){ 45 | 46 | var root = this; 47 | 48 | if (static) { 49 | 50 | } 51 | 52 | else { 53 | 54 | if (!obj.coordinates) obj.coordinates = [0,0,0]; 55 | 56 | // Bestow this mesh with animation superpowers and keeps track of its movements in the global animation queue 57 | root.animationManager.enroll(obj); 58 | obj.setCoords = function(lnglat){ 59 | 60 | /** Place the given object on the map, centered around the provided longitude and latitude 61 | The object's internal coordinates are assumed to be in meter-offset format, meaning 62 | 1 unit represents 1 meter distance away from the provided coordinate. 63 | */ 64 | 65 | // If object already added, scale the model so that its units are interpreted as meters at the given latitude 66 | if (obj.userData.units === 'meters'){ 67 | var s = utils.projectedUnitsPerMeter(lnglat[1]); 68 | obj.scale.set(s,s,s); 69 | } 70 | 71 | obj.coordinates = lnglat; 72 | obj.set({position:lnglat}) 73 | 74 | 75 | return obj; 76 | 77 | } 78 | 79 | obj.setRotation = function(xyz) { 80 | 81 | if (typeof xyz === 'number') xyz = {z: xyz} 82 | 83 | var r = { 84 | x: utils.radify(xyz.x) || obj.rotation.x, 85 | y: utils.radify(xyz.y) || obj.rotation.y, 86 | z: utils.radify(xyz.z) || obj.rotation.z 87 | } 88 | 89 | obj._setObject({rotation: [r.x, r.y, r.z]}) 90 | } 91 | 92 | } 93 | 94 | obj.add = function(){ 95 | root.world.add(obj); 96 | if (!static) obj.set({position:obj.coordinates}); 97 | return obj; 98 | } 99 | 100 | 101 | obj.remove = function(){ 102 | root.world.remove(obj); 103 | root.map.repaint = true; 104 | } 105 | 106 | obj.duplicate = function(a) { 107 | var dupe = obj.clone(); 108 | dupe.userData = obj.userData; 109 | root._addMethods(dupe); 110 | return dupe 111 | } 112 | 113 | return obj 114 | }, 115 | 116 | _makeGroup: function(obj, options){ 117 | var geoGroup = new THREE.Group(); 118 | geoGroup.userData = options || {}; 119 | geoGroup.userData.isGeoGroup = true; 120 | 121 | var isArrayOfObjects = obj.length; 122 | 123 | if (isArrayOfObjects) for (o of obj) geoGroup.add(o) 124 | 125 | 126 | else geoGroup.add(obj); 127 | 128 | utils._flipMaterialSides(obj); 129 | 130 | return geoGroup 131 | }, 132 | 133 | animationManager: new AnimationManager, 134 | 135 | _defaults: { 136 | 137 | line: { 138 | geometry: null, 139 | color: 'black', 140 | width:1, 141 | opacity:1 142 | }, 143 | 144 | sphere: { 145 | position: [0,0,0], 146 | radius: 1, 147 | sides: 20, 148 | units: 'scene', 149 | material: 'MeshBasicMaterial' 150 | }, 151 | 152 | tube: { 153 | geometry: null, 154 | radius: 1, 155 | sides:6, 156 | material: 'MeshBasicMaterial' 157 | }, 158 | 159 | extrusion:{ 160 | footprint: null, 161 | base: 0, 162 | top: 100, 163 | color:'black', 164 | material: 'MeshBasicMaterial', 165 | scaleToLatitude: false 166 | }, 167 | 168 | loadObj:{ 169 | obj: null, 170 | mtl: null, 171 | rotation: 0, 172 | scale: 1, 173 | units: 'scene' 174 | }, 175 | 176 | Object3D: { 177 | obj: null, 178 | units: 'scene' 179 | } 180 | }, 181 | 182 | geometries:{ 183 | line: ['LineString'], 184 | tube: ['LineString'], 185 | sphere: ['Point'], 186 | } 187 | } 188 | 189 | module.exports = exports = Objects; -------------------------------------------------------------------------------- /src/objects/sphere.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils/utils.js"); 2 | var material = require("../utils/material.js"); 3 | var Objects = require('./objects.js'); 4 | 5 | function Sphere(obj){ 6 | 7 | obj = utils._validate(obj, Objects.prototype._defaults.sphere); 8 | var geometry = new THREE.SphereBufferGeometry( obj.radius, obj.sides, obj.sides ); 9 | var mat = material(obj) 10 | var output = new THREE.Mesh( geometry, mat ); 11 | 12 | if (obj.units === 'meters') output = Objects.prototype._makeGroup(output, obj); 13 | Objects.prototype._addMethods(output); 14 | return output 15 | } 16 | 17 | 18 | module.exports = exports = Sphere; -------------------------------------------------------------------------------- /src/objects/tube.js: -------------------------------------------------------------------------------- 1 | var utils = require("../utils/utils.js"); 2 | var material = require("../utils/material.js"); 3 | var Objects = require('./objects.js'); 4 | var THREE = require("../three.js"); 5 | 6 | function tube(obj, world){ 7 | 8 | // validate and prep input geometry 9 | var obj = utils._validate(obj, Objects.prototype._defaults.tube); 10 | var straightProject = utils.lnglatsToWorld(obj.geometry); 11 | var normalized = utils.normalizeVertices(straightProject); 12 | 13 | var crossSection = tube.prototype.defineCrossSection(obj); 14 | var vertices = tube.prototype.buildVertices(crossSection, normalized.vertices, world); 15 | var geom = tube.prototype.buildFaces(vertices, normalized.vertices, obj); 16 | 17 | var mat = material(obj); 18 | 19 | var mesh = new THREE.Mesh( geom, mat ); 20 | mesh.position.copy(normalized.position); 21 | 22 | return mesh 23 | 24 | } 25 | 26 | tube.prototype = { 27 | 28 | buildVertices: function (crossSection, spine, world){ 29 | 30 | //create reusable plane for intersection calculations 31 | var geometry = new THREE.PlaneBufferGeometry(99999999999, 9999999999); 32 | var m = new THREE.MeshBasicMaterial( {color: 0xffffff, side: THREE.DoubleSide} ); 33 | m.opacity = 0 34 | var plane = new THREE.Mesh( geometry, m ); 35 | // world.add( plane ); 36 | 37 | var geom = new THREE.Geometry(); 38 | var lastElbow = false; 39 | 40 | 41 | // BUILD VERTICES: iterate through points in spine and position each vertex in cross section 42 | 43 | 44 | // get normalized vectors for each spine segment 45 | var spineSegments = [spine[0].clone().normalize()]; 46 | 47 | for (i in spine) { 48 | 49 | i = parseFloat(i); 50 | 51 | var segment; 52 | 53 | if (spine[i+1]){ 54 | segment = new THREE.Vector3() 55 | .subVectors( spine[i+1], spine[i]) 56 | .normalize(); 57 | 58 | } 59 | 60 | spineSegments.push(segment); 61 | } 62 | 63 | spineSegments.push(new THREE.Vector3()); 64 | 65 | for (i in spine) { 66 | 67 | i = parseFloat(i); 68 | var lineVertex = spine[i]; 69 | 70 | // ROTATE cross section 71 | 72 | var humerus = spineSegments[i] 73 | 74 | var forearm = spineSegments[i+1] 75 | 76 | var midpointToLookAt = humerus.clone() 77 | .add(forearm) 78 | .normalize(); 79 | 80 | if (i === 0) midpointToLookAt = forearm; 81 | 82 | else if (i === spine.length - 1) midpointToLookAt = humerus; 83 | 84 | 85 | // if first point in input line, rotate and translate it to position 86 | if (!lastElbow) { 87 | 88 | var elbow = crossSection.clone(); 89 | 90 | elbow 91 | .lookAt(midpointToLookAt) 92 | 93 | elbow.vertices.forEach(function(vertex){ 94 | geom.vertices 95 | .push(vertex.add(lineVertex)); 96 | }) 97 | 98 | lastElbow = elbow.vertices; 99 | 100 | } 101 | 102 | else { 103 | 104 | var elbow = []; 105 | plane.position.copy(lineVertex); 106 | plane.lookAt(midpointToLookAt.clone().add(lineVertex)); 107 | plane.updateMatrixWorld(); 108 | 109 | lastElbow.forEach(function(v3){ 110 | 111 | var raycaster = new THREE.Raycaster(v3, humerus); 112 | 113 | var intersection = raycaster 114 | .intersectObject(plane)[0]; 115 | 116 | if (intersection) { 117 | geom.vertices.push(intersection.point); 118 | elbow.push(intersection.point); 119 | } 120 | 121 | else console.error('Tube geometry failed at vertex '+i+'. Consider reducing tube radius, or smoothening out the sharp angle at this vertex') 122 | }) 123 | 124 | lastElbow = elbow 125 | } 126 | 127 | } 128 | 129 | world.remove(plane); 130 | 131 | return geom 132 | }, 133 | 134 | defineCrossSection: function(obj){ 135 | var crossSection = new THREE.Geometry(); 136 | var count = obj.sides; 137 | 138 | for ( var i = 0; i < count; i ++ ) { 139 | 140 | var l = obj.radius; 141 | var a = (i+0.5) / count * Math.PI; 142 | 143 | crossSection.vertices.push( 144 | new THREE.Vector3 ( 145 | -Math.sin( 2 * a ), 146 | Math.cos( 2 * a ), 147 | 0 148 | ) 149 | .multiplyScalar(l) 150 | ); 151 | } 152 | 153 | return crossSection 154 | }, 155 | 156 | //build faces between vertices 157 | 158 | buildFaces: function(geom, spine, obj){ 159 | 160 | for (var i in spine) { 161 | 162 | i = parseFloat(i); 163 | var vertex = spine[i]; 164 | 165 | if (i < spine.length - 1) { 166 | 167 | for (var p = 0; p < obj.sides; p++) { 168 | 169 | var b1 = i * obj.sides + p; 170 | var b2 = i * obj.sides + (p+1) % obj.sides 171 | var t1 = b1 + obj.sides 172 | var t2 = b2 + obj.sides; 173 | 174 | var triangle1 = new THREE.Face3(t1, b1, b2); 175 | var triangle2 = new THREE.Face3(t1, b2, t2); 176 | geom.faces.push(triangle1, triangle2) 177 | } 178 | } 179 | } 180 | 181 | //add endcaps 182 | var v = geom.vertices.length; 183 | 184 | for (var c = 0; c+2 90) { 29 | console.error("Latitude must be between -90 and 90") 30 | return 31 | } 32 | 33 | return input 34 | }, 35 | 36 | Line: function(input) { 37 | 38 | var scope = this; 39 | 40 | if (input.constructor !== Array) { 41 | console.error("Line must be an array") 42 | return 43 | } 44 | 45 | for (coord of input){ 46 | if (!scope.Coords(coord)) { 47 | console.error("Each coordinate in a line must be a valid Coords type") 48 | return 49 | } 50 | 51 | } 52 | 53 | return input 54 | }, 55 | 56 | Rotation: function(input) { 57 | 58 | if (input.constructor === Number) input = {z: input} 59 | 60 | else if (input.constructor === Object) { 61 | 62 | for (key of Object.keys(input)){ 63 | 64 | if (!['x', 'y', 'z'].includes(key)) { 65 | console.error('Rotation parameters must be x, y, or z') 66 | return 67 | } 68 | if (input[key].constructor !== Number) { 69 | console.error('Individual rotation values must be numbers') 70 | return 71 | } 72 | } 73 | } 74 | 75 | else { 76 | console.error('Rotation must be an object or a number') 77 | return 78 | } 79 | 80 | return input 81 | }, 82 | 83 | Scale: function(input) { 84 | 85 | if (input.constructor === Number) { 86 | input = {x:input, y:input, z: input} 87 | } 88 | 89 | else if (input.constructor === Object) { 90 | 91 | for (key of Object.keys(input)){ 92 | 93 | if (!['x', 'y', 'z'].includes(key)) { 94 | console.error('Scale parameters must be x, y, or z') 95 | return 96 | } 97 | if (input[key].constructor !== Number) { 98 | console.error('Individual scale values must be numbers') 99 | return 100 | } 101 | } 102 | } 103 | 104 | else { 105 | console.error('Scale must be an object or a number') 106 | return 107 | } 108 | 109 | return input 110 | } 111 | 112 | } 113 | 114 | 115 | module.exports = exports = Validate; -------------------------------------------------------------------------------- /tests/threebox-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Threebox tests 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 26 | 27 | 28 | Open the console to see test results 29 |
30 | 31 | 97 | -------------------------------------------------------------------------------- /tests/threebox-tests.js: -------------------------------------------------------------------------------- 1 | window.test = require('tape'); 2 | // Threebox = require("../src/Threebox.js"); 3 | // THREE = require("../src/three.js"); 4 | 5 | // window.runTests = function () { 6 | // // material(instance); 7 | // } 8 | 9 | function vector3Equals(t, input, expected, allowableError, epsilon) { 10 | // Check that two Vector3s are equal to each other, allowing for a certain percentage of error due to floating point math 11 | if (allowableError === undefined) allowableError = 0.0000001; 12 | if (epsilon === undefined) epsilon = 0.00000000000001; 13 | var dX, dY, dZ; 14 | dX = Math.abs(input.x - expected.x) / (expected.x === 0 ? 1 : expected.x); 15 | dY = Math.abs(input.y - expected.y) / (expected.y === 0 ? 1 : expected.y); 16 | dZ = Math.abs(input.z - expected.z) / (expected.z === 0 ? 1 : expected.z); 17 | 18 | if (dX < epsilon) dX = 0; 19 | if (dY < epsilon) dY = 0; 20 | if (dZ < epsilon) dZ = 0; 21 | 22 | if(dX > allowableError || dY > allowableError || dZ > allowableError) { 23 | t.fail("Vector3 Equivalence failed: (" + input.x + ", " + input.y + ", " + input.z + ") expected: (" + expected.x + ", " + expected.y + ", " + expected.z + ")"); 24 | console.log(dY); 25 | } 26 | t.pass("ok Vector3 equivalence"); 27 | } 28 | 29 | 30 | function precisionRound(number, precision) { 31 | var factor = Math.pow(10, precision); 32 | var tempNumber = number * factor; 33 | var roundedTempNumber = Math.round(tempNumber); 34 | return roundedTempNumber / factor; 35 | }; -------------------------------------------------------------------------------- /tests/unit/camera.test.js: -------------------------------------------------------------------------------- 1 | function cameraTest(instance){ 2 | 3 | 4 | } 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/unit/material.test.js: -------------------------------------------------------------------------------- 1 | function materialTest(instance){ 2 | 3 | var material; 4 | 5 | test('MATERIAL default type, color, opacity', function(t) { 6 | material = instance.material(); 7 | t.equal(material.type, 'MeshBasicMaterial'); 8 | t.deepEqual(material.color, { r: 0, g: 0, b: 0 }); 9 | t.equal(material.opacity, 1); 10 | t.end(); 11 | }); 12 | 13 | test('MATERIAL custom type', function(t) { 14 | material = instance.material({material:'MeshPhysicalMaterial'}); 15 | t.equal(material.opacity, 1); 16 | t.equal(material.type, 'MeshPhysicalMaterial'); 17 | t.deepEqual(material.color, { r: 0, g: 0, b: 0 }); 18 | t.end(); 19 | }); 20 | 21 | test('MATERIAL custom color', function(t) { 22 | material = instance.material({color:'red'}); 23 | t.equal(material.opacity, 1); 24 | t.equal(material.type, 'MeshBasicMaterial'); 25 | t.deepEqual(material.color, { r: 1, g: 0, b: 0 }); 26 | t.end(); 27 | }); 28 | 29 | test('MATERIAL custom opacity', function(t) { 30 | material = instance.material({opacity:0.5}); 31 | t.equal(material.opacity, 0.5); 32 | t.equal(material.type, 'MeshBasicMaterial'); 33 | t.deepEqual(material.color, { r: 0, g: 0, b: 0 }); 34 | t.end(); 35 | }); 36 | 37 | test('MATERIAL custom color, opacity, type', function(t) { 38 | allCustom = instance.material({ 39 | material: 'MeshBasicMaterial', 40 | opacity: 0.5, 41 | color: 'blue' 42 | }); 43 | t.equal(allCustom.opacity, 0.5); 44 | t.equal(allCustom.type, 'MeshBasicMaterial'); 45 | t.deepEqual(allCustom.color, { r: 0, g: 0, b: 1 }); 46 | t.end(); 47 | }); 48 | 49 | test('MATERIAL when THREE.Material provided, other material params ignored except opacity', function(t) { 50 | threeMaterial = instance.material({ 51 | material: new THREE.MeshBasicMaterial({color: 'cyan'}), 52 | opacity: 0.5, 53 | color: 'blue' 54 | }); 55 | 56 | t.equal(threeMaterial.opacity, 0.5); 57 | t.equal(threeMaterial.type, 'MeshBasicMaterial'); 58 | t.deepEqual(threeMaterial.color, { r: 0, g: 1, b: 1 }); 59 | t.end(); 60 | }); 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /tests/unit/object.test.js: -------------------------------------------------------------------------------- 1 | function objectTest(instance){ 2 | 3 | var mesh = new THREE.Mesh(); 4 | var group 5 | 6 | test('OBJECT _makeGroup from one object', function(t){ 7 | group = instance.objects._makeGroup(mesh, {foo: true}); 8 | 9 | t.equal(group.userData.foo, true); 10 | t.equal(group.type, 'Group'); 11 | t.equal(group.children.length, 1); 12 | t.end(); 13 | 14 | }); 15 | 16 | test('OBJECT _makeGroup from multiple objects', function(t){ 17 | var mesh2 = new THREE.Mesh(); 18 | group = instance.objects._makeGroup([mesh, mesh2], {foo: false}); 19 | 20 | t.equal(group.userData.foo, false); 21 | t.equal(group.type, 'Group'); 22 | t.equal(group.children.length, 2); 23 | t.end(); 24 | 25 | }) 26 | 27 | test('OBJECT _addMethods static', function(t){ 28 | 29 | group = instance.objects._makeGroup(mesh, {}); 30 | var addedMethods = instance.objects._addMethods(group, true); 31 | 32 | t.equal(addedMethods.setCoords, undefined); 33 | t.equal(addedMethods.type, 'Group'); 34 | t.end(); 35 | 36 | }) 37 | 38 | test('OBJECT _addMethods dynamic', function(t){ 39 | 40 | var sphere = instance.sphere({units: 'meters'}); 41 | 42 | t.equal(sphere.type, 'Group'); 43 | t.equal(typeof sphere.followPath, 'function'); 44 | t.deepEqual(sphere.coordinates, [0,0,0]); 45 | t.end(); 46 | 47 | }) 48 | 49 | test('OBJECT setCoords updates both position and scale', function(t){ 50 | 51 | var sphere = instance.sphere({units: 'meters'}); 52 | var newPosition = [0,-20]; 53 | 54 | sphere.setCoords(newPosition) 55 | var scaleFactor = instance.projectedUnitsPerMeter(newPosition[1]); 56 | 57 | vector3Equals(t, sphere.position, instance.projectToWorld(newPosition)); 58 | t.equal(sphere.scale.x, scaleFactor); 59 | t.deepEqual(sphere.coordinates, newPosition) 60 | t.end(); 61 | 62 | }) 63 | 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/unit/utilities.test.js: -------------------------------------------------------------------------------- 1 | function utilitiesTest(instance){ 2 | 3 | test('PROJECTION project / unproject', function(t) { 4 | 5 | var coord, projected, expected; 6 | 7 | coord = [0,0,0]; 8 | var projected = instance.projectToWorld(coord); 9 | var unprojected = instance.unprojectFromWorld(projected); 10 | var expected = new THREE.Vector3(0,0,0); 11 | vector3Equals(t, projected, expected); 12 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 13 | 14 | coord = [30,30,0]; 15 | var projected = instance.projectToWorld(coord); 16 | var unprojected = instance.unprojectFromWorld(projected); 17 | var expected = new THREE.Vector3(-85333.33333333333, -89522.98305691125, 0); 18 | vector3Equals(t, projected, expected); 19 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 20 | 21 | coord = [30,-30,0]; 22 | var projected = instance.projectToWorld(coord); 23 | var unprojected = instance.unprojectFromWorld(projected); 24 | var expected = new THREE.Vector3(-85333.33333333333, -89522.9830569113, 0); 25 | vector3Equals(t, projected, expected); 26 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 27 | 28 | coord = [-30,30,0]; 29 | var projected = instance.projectToWorld(coord); 30 | var unprojected = instance.unprojectFromWorld(projected); 31 | var expected = new THREE.Vector3(-85333.33333333333, -89522.9830569113, 0); 32 | vector3Equals(t, projected, expected); 33 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 34 | 35 | coord = [-30,-30,0]; 36 | var projected = instance.projectToWorld(coord); 37 | var unprojected = instance.unprojectFromWorld(projected); 38 | var expected = new THREE.Vector3(-85333.33333333333, 89522.9830569113, 0); 39 | vector3Equals(t, projected, expected); 40 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 41 | 42 | t.end(); 43 | }); 44 | 45 | test('PROJECTION project / unproject extended lat/lng range', function(t) { 46 | var coord, projected, expected; 47 | 48 | coord = [180,0,0]; 49 | var projected = instance.projectToWorld(coord); 50 | var unprojected = instance.unprojectFromWorld(projected); 51 | var expected = new THREE.Vector3(-511999.99999999994,1.8093822187881337e-11,0); 52 | vector3Equals(t, projected, expected); 53 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 54 | 55 | coord = [-180,0,0]; 56 | var projected = instance.projectToWorld(coord); 57 | var unprojected = instance.unprojectFromWorld(projected); 58 | var expected = new THREE.Vector3(511999.99999999994,1.8093822187881337e-11,0); 59 | vector3Equals(t, projected, expected); 60 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 61 | 62 | coord = [0,90,0]; 63 | var projected = instance.projectToWorld(coord); 64 | var unprojected = instance.unprojectFromWorld(projected); 65 | var expected = new THREE.Vector3(0,-3042.073317352722,0); 66 | vector3Equals(t, projected, expected); 67 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 68 | 69 | 70 | coord = [0, 85.051129,0]; 71 | var projected = instance.projectToWorld(coord); 72 | var unprojected = instance.unprojectFromWorld(projected); 73 | var expected = new THREE.Vector3(0, -512000.00726036413, 0); 74 | vector3Equals(t, projected, expected); 75 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 76 | 77 | coord = [0, -85.051129,0]; 78 | var projected = instance.projectToWorld(coord); 79 | var unprojected = instance.unprojectFromWorld(projected); 80 | var expected = new THREE.Vector3(0, -512000.00726036413, 0); 81 | vector3Equals(t, projected, expected); 82 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 83 | 84 | 85 | coord = [300,0,0]; 86 | var projected = instance.projectToWorld(coord); 87 | var unprojected = instance.unprojectFromWorld(projected); 88 | var expected = new THREE.Vector3(-853333.3333333333,1.8093822187881337e-11,0); 89 | vector3Equals(t, projected, expected); 90 | vector3Equals(t, new THREE.Vector3(unprojected), new THREE.Vector3(expected)); 91 | 92 | t.end(); 93 | }); 94 | 95 | 96 | test('PROJECTION with altitude', function(t) { 97 | var coord, projected, expected; 98 | 99 | coord = [0,0,10000]; 100 | var projected = instance.projectToWorld(coord); 101 | var expected = new THREE.Vector3(0,0,255.52089831565812); 102 | vector3Equals(t, projected, expected); 103 | 104 | 105 | coord = [0,0,-10000]; 106 | var projected = instance.projectToWorld(coord); 107 | var expected = new THREE.Vector3(0,0,-255.52089831565812); 108 | vector3Equals(t, projected, expected); 109 | 110 | t.end(); 111 | }); 112 | 113 | test('PROJECTION projectedUnitsPerMeter', function(t) { 114 | 115 | var pupm1 = instance.projectedUnitsPerMeter(10); 116 | var pupm2 = instance.projectedUnitsPerMeter(-35); 117 | 118 | t.equals(pupm1, 0.025946272004267072); 119 | t.equals(pupm2, 0.03119334195612554); 120 | 121 | t.end(); 122 | }); 123 | 124 | 125 | // test('PROJECTION project / unproject invalid input', function(t) { 126 | // // TODO: Check for null/undefined/NaN values 127 | // t.end(); 128 | // }); 129 | 130 | 131 | test('NORMALIZEVERTICES', function(t){ 132 | 133 | var input = [ 134 | new THREE.Vector3(100, 101, 102), 135 | new THREE.Vector3(103, 104, 105) 136 | ]; 137 | 138 | var normalized = instance.utils.normalizeVertices(input); 139 | 140 | t.deepEqual( 141 | normalized.position, 142 | { 143 | x: 101.5, 144 | y: 102.5, 145 | z: 103.5 146 | } 147 | ); 148 | 149 | t.deepEqual( 150 | normalized.vertices, 151 | [ 152 | {x: -1.5, y: -1.5, z: -1.5}, 153 | {x: 1.5, y: 1.5, z: 1.5} 154 | ] 155 | ); 156 | 157 | t.end(); 158 | }) 159 | 160 | var defaults = { 161 | foo: 'bar', 162 | biz: false 163 | } 164 | 165 | test('VALIDATOR empty input', function(t) { 166 | var output = instance.utils._validate({}, defaults); 167 | t.deepEqual(output, defaults); 168 | t.end(); 169 | }); 170 | 171 | test('VALIDATOR does not overwrite unknown props', function(t) { 172 | var output = instance.utils._validate({a:true}, defaults); 173 | 174 | t.deepEqual(output, { 175 | foo: 'bar', 176 | biz: false, 177 | a: true 178 | }); 179 | 180 | t.end(); 181 | }); 182 | 183 | test('VALIDATOR missing required params throw error', function(t) { 184 | var output = instance.utils._validate({}, {b: null}); 185 | t.error(output, 'proper error'); 186 | t.end(); 187 | }); 188 | } 189 | -------------------------------------------------------------------------------- /tests/unit/validate.test.js: -------------------------------------------------------------------------------- 1 | function validateTest(instance){ 2 | 3 | var v = instance.utils.Validator; 4 | 5 | // Coords validation 6 | 7 | test('VALIDATE invalid Coords', function(t){ 8 | 9 | t.error(v.Coords(true), 'error'); 10 | t.error(v.Coords(['a']), 'error'); 11 | t.error(v.Coords([1]), 'error'); 12 | t.error(v.Coords([3, false]), 'error'); 13 | 14 | t.end(); 15 | 16 | }); 17 | 18 | test('VALIDATE valid Coords', function(t){ 19 | 20 | t.deepEqual(v.Coords([22,33]), [22,33]); 21 | t.deepEqual(v.Coords([22,33,-10]), [22,33, -10]); 22 | t.end(); 23 | 24 | }); 25 | 26 | // Line validation 27 | 28 | test('VALIDATE invalid Line', function(t){ 29 | 30 | t.error(v.Line([[1,2], [false, 4]]), 'error'); 31 | t.end(); 32 | 33 | }); 34 | 35 | test('VALIDATE valid Line', function(t){ 36 | 37 | t.deepEqual(v.Line([[-100, 20], [22,33]]), [[-100, 20], [22,33]]); 38 | t.end(); 39 | 40 | }); 41 | 42 | // Rotation validation 43 | 44 | test('VALIDATE invalid Rotation', function(t){ 45 | 46 | t.error(v.Rotation('rotate'), 'error'); 47 | t.end(); 48 | 49 | }); 50 | 51 | test('VALIDATE valid Rotation', function(t){ 52 | 53 | t.deepEqual(v.Rotation(40), {z:40}); 54 | t.deepEqual(v.Rotation({x:20, y:10, z:90}), {x:20, y:10, z:90}); 55 | 56 | t.end(); 57 | 58 | }); 59 | 60 | // Scale validation 61 | 62 | test('VALIDATE invalid Scale', function(t){ 63 | 64 | t.error(v.Scale('scale'), 'error'); 65 | t.end(); 66 | 67 | }); 68 | 69 | test('VALIDATE valid Scale', function(t){ 70 | 71 | t.deepEqual(v.Scale(22), {x:22, y:22, z:22}); 72 | t.deepEqual(v.Scale({x:20, y:10, z:90}), {x:20, y:10, z:90}); 73 | 74 | t.end(); 75 | 76 | }); 77 | } 78 | 79 | 80 | --------------------------------------------------------------------------------