소스 검색

Merge remote-tracking branch 'upstream/update2017'

Nikolay Suslov 7 년 전
부모
커밋
d3e98b293d
92개의 변경된 파일49957개의 추가작업 그리고 35188개의 파일을 삭제
  1. 1 1
      lib/nodejs/file-cache.js
  2. 5 1
      lib/nodejs/reflector.js
  3. 346 197
      package-lock.json
  4. 6 6
      package.json
  5. 10 9
      public/aframe/index.vwf.yaml
  6. 9 1
      public/aframe2/assets.json
  7. 9 20
      public/aframe2/index.vwf.yaml
  8. BIN
      public/assets/images/planeDiffuse.png
  9. BIN
      public/assets/models/plane/PUSHILIN_PLANE.png
  10. 13 0
      public/assets/models/plane/PUSHILIN_Plane.mtl
  11. 4610 0
      public/assets/models/plane/PUSHILIN_Plane.obj
  12. 0 225
      public/assets/plane.dae
  13. 198 0
      public/assets/test.dae
  14. 15 0
      public/gearvr/appui.js
  15. 0 4
      public/gearvr/assets.json
  16. 1 1
      public/gearvr/index.vwf.config.yaml
  17. 18 109
      public/gearvr/index.vwf.yaml
  18. BIN
      public/gearvr/webimg.jpg
  19. 2 1
      public/ohmlang-calc/index.vwf.yaml
  20. 2 2
      public/ohmlang-lsys/index.vwf.yaml
  21. 0 8200
      public/web/lib/socketio/socket.io.js
  22. 0 0
      public/web/lib/socketio/socket.io.js.map
  23. 0 0
      public/web/lib/socketio/socket.io.min.js
  24. 0 6052
      public/web/lib/socketio/socket.io.slim.js
  25. 0 0
      public/web/lib/socketio/socket.io.slim.js.map
  26. 0 0
      public/web/lib/socketio/socket.io.slim.min.js
  27. 6 0
      public/webapps.json
  28. 1 1
      public/webrtc/index.vwf.config.yaml
  29. 1 0
      public/webrtc/index.vwf.yaml
  30. 1 1
      support/client/lib/index.html
  31. 0 8200
      support/client/lib/socket.io/socket.io.js
  32. 0 0
      support/client/lib/socket.io/socket.io.js.map
  33. 0 0
      support/client/lib/socket.io/socket.io.min.js
  34. 0 6052
      support/client/lib/socket.io/socket.io.slim.js
  35. 0 0
      support/client/lib/socket.io/socket.io.slim.js.map
  36. 0 0
      support/client/lib/socket.io/socket.io.slim.min.js
  37. 5 5
      support/client/lib/vwf.js
  38. 181 23
      support/client/lib/vwf/model/aframe.js
  39. 130 4
      support/client/lib/vwf/model/aframe/addon/aframe-components.js
  40. 17994 0
      support/client/lib/vwf/model/aframe/addon/aframe-extras.controls.js
  41. 0 0
      support/client/lib/vwf/model/aframe/addon/aframe-extras.controls.min.js
  42. 5 226
      support/client/lib/vwf/model/aframe/addon/aframe-extras.loaders.js
  43. 0 0
      support/client/lib/vwf/model/aframe/addon/aframe-extras.loaders.min.js
  44. 342 0
      support/client/lib/vwf/model/aframe/addon/aframe-interpolation 2.js
  45. 96 235
      support/client/lib/vwf/model/aframe/addon/aframe-interpolation.js
  46. 616 729
      support/client/lib/vwf/model/aframe/aframe-master.js
  47. 1 2
      support/client/lib/vwf/model/aframe/aframe-master.js.map
  48. 0 0
      support/client/lib/vwf/model/aframe/aframe-master.min.js
  49. 0 0
      support/client/lib/vwf/model/aframe/aframe-master.min.js.map
  50. 11 4
      support/client/lib/vwf/model/aframe/extras/aframe-extras.controls.js
  51. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.controls.min.js
  52. 3578 3828
      support/client/lib/vwf/model/aframe/extras/aframe-extras.js
  53. 7 270
      support/client/lib/vwf/model/aframe/extras/aframe-extras.loaders.js
  54. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.loaders.min.js
  55. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.min.js
  56. 12 6
      support/client/lib/vwf/model/aframe/extras/aframe-extras.misc.js
  57. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.misc.min.js
  58. 222 222
      support/client/lib/vwf/model/aframe/extras/aframe-extras.pathfinding.js
  59. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.pathfinding.min.js
  60. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.primitives.min.js
  61. 7 3
      support/client/lib/vwf/model/aframe/extras/components/sphere-collider.js
  62. 0 0
      support/client/lib/vwf/model/aframe/extras/components/sphere-collider.min.js
  63. 0 150
      support/client/lib/vwf/model/aframe/extras/components/three-model.js
  64. 0 0
      support/client/lib/vwf/model/aframe/extras/components/three-model.min.js
  65. 9 60
      support/client/lib/vwf/model/aframeComponent.js
  66. 225 50
      support/client/lib/vwf/view/aframe.js
  67. 75 6
      support/client/lib/vwf/view/aframeComponent.js
  68. 226 57
      support/client/lib/vwf/view/editor-new.js
  69. 1102 0
      support/client/lib/vwf/view/webrtc.1.js
  70. 328 193
      support/client/lib/vwf/view/webrtc.js
  71. 4471 0
      support/client/lib/vwf/view/webrtc/dist/adapter-latest.js
  72. 4471 0
      support/client/lib/vwf/view/webrtc/dist/adapter.js
  73. 2795 0
      support/client/lib/vwf/view/webrtc/dist/adapter_no_edge.js
  74. 2794 0
      support/client/lib/vwf/view/webrtc/dist/adapter_no_edge_no_global.js
  75. 4470 0
      support/client/lib/vwf/view/webrtc/dist/adapter_no_global.js
  76. 83 0
      support/client/lib/vwf/view/widgets.js
  77. 6 1
      support/proxy/vwf.example.com/aframe/acamera.vwf.yaml
  78. 1 0
      support/proxy/vwf.example.com/aframe/aentity.vwf.yaml
  79. 7 0
      support/proxy/vwf.example.com/aframe/aobjmodel.vwf.yaml
  80. 16 0
      support/proxy/vwf.example.com/aframe/ascene.js
  81. 2 0
      support/proxy/vwf.example.com/aframe/ascene.vwf.yaml
  82. 4 1
      support/proxy/vwf.example.com/aframe/asky.vwf.yaml
  83. 8 0
      support/proxy/vwf.example.com/aframe/asound.vwf.yaml
  84. 74 17
      support/proxy/vwf.example.com/aframe/avatar.js
  85. 16 0
      support/proxy/vwf.example.com/aframe/avatar.vwf.yaml
  86. 0 12
      support/proxy/vwf.example.com/aframe/gearvr-controlsComponent.vwf.yaml
  87. 129 0
      support/proxy/vwf.example.com/aframe/gearvrcontroller.js
  88. 24 0
      support/proxy/vwf.example.com/aframe/gearvrcontroller.vwf.yaml
  89. 0 1
      support/proxy/vwf.example.com/aframe/interpolation-component.vwf.yaml
  90. 5 0
      support/proxy/vwf.example.com/aframe/streamSoundComponent.vwf.yaml
  91. 131 0
      support/proxy/vwf.example.com/aframe/wmrvrcontroller.js
  92. 24 0
      support/proxy/vwf.example.com/aframe/wmrvrcontroller.vwf.yaml

+ 1 - 1
lib/nodejs/file-cache.js

@@ -99,7 +99,7 @@ function _FileCache( ) {
                 return;
             }
 
-            var type = mime.lookup( filename );
+            var type = mime.getType( filename );
 
             if ( request.headers[ 'if-none-match' ] === file.hash ) {
                 response.writeHead( 304, {

+ 5 - 1
lib/nodejs/reflector.js

@@ -212,9 +212,11 @@ function OnConnection( socket ) {
                     client.emit( 'message', message );
                 }
             }
+                if(global.instances[ namespace ]){
             if ( global.instances[ namespace ].pendingList.pending ) {
                 global.instances[ namespace ].pendingList.push( message );
             }
+        }
         }, 50 );
 
     }
@@ -327,10 +329,12 @@ function OnConnection( socket ) {
                             }
                         }
             
+                        if (global.instances[ namespace ]) {
                         if ( global.instances[ namespace ].pendingList.pending ) {
                             global.instances[ namespace ].pendingList.push( message );
                         }
-            
+                    }
+
                     } else if ( message.action == "getState" ) {
             
                         //distribute message to all clients on given instance

+ 346 - 197
package-lock.json

@@ -2,30 +2,51 @@
   "name": "livecodingspace",
   "version": "0.0.1",
   "lockfileVersion": 1,
+  "requires": true,
   "dependencies": {
     "accepts": {
-      "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
-      "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo="
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
+      "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
+      "requires": {
+        "mime-types": "2.1.17",
+        "negotiator": "0.6.1"
+      }
     },
     "after": {
-      "version": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
       "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
     },
     "argparse": {
-      "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
-      "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY="
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
+      "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
+      "requires": {
+        "sprintf-js": "1.0.3"
+      }
     },
     "arraybuffer.slice": {
-      "version": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
       "integrity": "sha1-8zshWfBTKj8xB6JywMz70a0peco="
     },
     "async": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/async/-/async-2.4.1.tgz",
-      "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c="
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
+      "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==",
+      "requires": {
+        "lodash": "4.17.4"
+      }
+    },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
     },
     "backo2": {
-      "version": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
       "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
     },
     "balanced-match": {
@@ -34,25 +55,36 @@
       "dev": true
     },
     "base64-arraybuffer": {
-      "version": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
       "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
     },
     "base64id": {
-      "version": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
       "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
     },
     "better-assert": {
-      "version": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
-      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI="
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "requires": {
+        "callsite": "1.0.0"
+      }
     },
     "blob": {
-      "version": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
       "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE="
     },
     "brace-expansion": {
       "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz",
       "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "balanced-match": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz",
+        "concat-map": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+      }
     },
     "browser-stdout": {
       "version": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
@@ -60,20 +92,31 @@
       "dev": true
     },
     "callsite": {
-      "version": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
       "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
     },
     "commander": {
       "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
       "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "graceful-readlink": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz"
+      }
     },
     "component-bind": {
-      "version": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
       "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
     },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+    },
     "component-inherit": {
-      "version": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
       "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
     },
     "concat-map": {
@@ -82,36 +125,106 @@
       "dev": true
     },
     "cookie": {
-      "version": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
       "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
     },
     "crypto": {
-      "version": "https://registry.npmjs.org/crypto/-/crypto-0.0.3.tgz",
-      "integrity": "sha1-RwqBuGvkxe4XrMggeh9TFa4g27A="
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
+      "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
+    },
+    "debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "requires": {
+        "ms": "2.0.0"
+      },
+      "dependencies": {
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
     },
     "diff": {
       "version": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
       "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=",
       "dev": true
     },
+    "engine.io": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.4.tgz",
+      "integrity": "sha1-PQIRtwpVLOhB/8fahiezAamkFi4=",
+      "requires": {
+        "accepts": "1.3.3",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "2.6.9",
+        "engine.io-parser": "2.1.1",
+        "uws": "0.14.5",
+        "ws": "3.3.3"
+      }
+    },
+    "engine.io-client": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.4.tgz",
+      "integrity": "sha1-T88TcLRxY70s6b4nM5ckMDUNTqE=",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "2.6.9",
+        "engine.io-parser": "2.1.1",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "3.3.3",
+        "xmlhttprequest-ssl": "1.5.4",
+        "yeast": "0.1.2"
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz",
+      "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg=",
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "0.0.6",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.4",
+        "has-binary2": "1.0.2"
+      }
+    },
     "escape-string-regexp": {
       "version": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
       "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
       "dev": true
     },
     "esprima": {
-      "version": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz",
-      "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM="
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz",
+      "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw=="
     },
     "fs-extra": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
       "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
+      "requires": {
+        "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+        "jsonfile": "3.0.0",
+        "universalify": "0.1.0"
+      },
       "dependencies": {
         "jsonfile": {
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.0.tgz",
-          "integrity": "sha1-kufHRE5f/V+jLmqa6LhQNN+DR9A="
+          "integrity": "sha1-kufHRE5f/V+jLmqa6LhQNN+DR9A=",
+          "requires": {
+            "graceful-fs": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz"
+          }
         }
       }
     },
@@ -123,7 +236,15 @@
     "glob": {
       "version": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
       "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+        "inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+        "inherits": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+        "minimatch": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+        "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+        "path-is-absolute": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
+      }
     },
     "graceful-fs": {
       "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
@@ -143,16 +264,13 @@
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz",
       "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=",
-      "dependencies": {
-        "isarray": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-        }
+      "requires": {
+        "isarray": "2.0.1"
       }
     },
     "has-cors": {
-      "version": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
       "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
     },
     "has-flag": {
@@ -161,23 +279,37 @@
       "dev": true
     },
     "indexof": {
-      "version": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
       "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
     },
     "inflight": {
       "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "once": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+        "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+      }
     },
     "inherits": {
       "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
       "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
       "dev": true
     },
+    "isarray": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
+      "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
+    },
     "js-yaml": {
-      "version": "3.8.4",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz",
-      "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY="
+      "version": "3.10.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz",
+      "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==",
+      "requires": {
+        "argparse": "1.0.9",
+        "esprima": "4.0.0"
+      }
     },
     "json3": {
       "version": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
@@ -185,13 +317,18 @@
       "dev": true
     },
     "lodash": {
-      "version": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
+      "version": "4.17.4",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
       "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
     },
     "lodash._baseassign": {
       "version": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
       "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "lodash._basecopy": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
+        "lodash.keys": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz"
+      }
     },
     "lodash._basecopy": {
       "version": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
@@ -216,7 +353,12 @@
     "lodash.create": {
       "version": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
       "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "lodash._baseassign": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
+        "lodash._basecreate": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz",
+        "lodash._isiterateecall": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz"
+      }
     },
     "lodash.isarguments": {
       "version": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -231,25 +373,38 @@
     "lodash.keys": {
       "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
       "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "lodash._getnative": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+        "lodash.isarguments": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
+        "lodash.isarray": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz"
+      }
     },
     "mime": {
-      "version": "1.3.6",
-      "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz",
-      "integrity": "sha1-WR2E02U6awtKO5343lqoEI5y5eA="
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-2.1.0.tgz",
+      "integrity": "sha512-jPEuocEVyg24I7hWcF6EL5qH0OQ3Ficy95tXA9eNBN6qXsIopYi/CJl3ldTUR+Sljt2rP2SkWpeTcAMon/pjKA=="
     },
     "mime-db": {
-      "version": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz",
-      "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE="
+      "version": "1.30.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
+      "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
     },
     "mime-types": {
-      "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz",
-      "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0="
+      "version": "2.1.17",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
+      "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
+      "requires": {
+        "mime-db": "1.30.0"
+      }
     },
     "minimatch": {
       "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "brace-expansion": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz"
+      }
     },
     "minimist": {
       "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
@@ -259,6 +414,9 @@
       "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
       "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
       "dev": true,
+      "requires": {
+        "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+      },
       "dependencies": {
         "minimist": {
           "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
@@ -271,11 +429,27 @@
       "version": "https://registry.npmjs.org/mocha/-/mocha-3.3.0.tgz",
       "integrity": "sha1-0pt0KNP1LILi5l3x7LcGThqrv7U=",
       "dev": true,
+      "requires": {
+        "browser-stdout": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
+        "commander": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
+        "debug": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz",
+        "diff": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz",
+        "escape-string-regexp": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+        "glob": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
+        "growl": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",
+        "json3": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
+        "lodash.create": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
+        "mkdirp": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+        "supports-color": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz"
+      },
       "dependencies": {
         "debug": {
           "version": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz",
           "integrity": "sha1-vFlryr52F/Edn6FTYe3tVgi4SZs=",
-          "dev": true
+          "dev": true,
+          "requires": {
+            "ms": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
+          }
         }
       }
     },
@@ -285,33 +459,46 @@
       "dev": true
     },
     "negotiator": {
-      "version": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
       "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
     },
     "object-component": {
-      "version": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
       "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
     },
     "once": {
       "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+      }
     },
     "optimist": {
       "version": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
-      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY="
-    },
-    "parsejson": {
-      "version": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
-      "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs="
+      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+      "requires": {
+        "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+        "wordwrap": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz"
+      }
     },
     "parseqs": {
-      "version": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
-      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0="
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "requires": {
+        "better-assert": "1.0.2"
+      }
     },
     "parseuri": {
-      "version": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
-      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo="
+      "version": "0.0.5",
+      "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "requires": {
+        "better-assert": "1.0.2"
+      }
     },
     "path-is-absolute": {
       "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -319,24 +506,38 @@
       "dev": true
     },
     "safe-buffer": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz",
-      "integrity": "sha1-0mPKVGls2KMGtcplUekt5XkY++c="
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+      "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
     },
     "should": {
       "version": "https://registry.npmjs.org/should/-/should-11.2.1.tgz",
       "integrity": "sha1-kPVRRVUtAc/CAGZuToGKHJZw7aI=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "should-equal": "https://registry.npmjs.org/should-equal/-/should-equal-1.0.1.tgz",
+        "should-format": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
+        "should-type": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
+        "should-type-adaptors": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.0.1.tgz",
+        "should-util": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz"
+      }
     },
     "should-equal": {
       "version": "https://registry.npmjs.org/should-equal/-/should-equal-1.0.1.tgz",
       "integrity": "sha1-C26VFvJgGp+wuy3MNpr6HH4gCvc=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "should-type": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz"
+      }
     },
     "should-format": {
       "version": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
       "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "should-type": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
+        "should-type-adaptors": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.0.1.tgz"
+      }
     },
     "should-type": {
       "version": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
@@ -346,7 +547,11 @@
     "should-type-adaptors": {
       "version": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.0.1.tgz",
       "integrity": "sha1-7+VVPN9oz/ZuXF9RtxLcNRx3vqo=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "should-type": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
+        "should-util": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz"
+      }
     },
     "should-util": {
       "version": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz",
@@ -354,144 +559,76 @@
       "dev": true
     },
     "socket.io": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.2.tgz",
-      "integrity": "sha1-EzvzobZ9AvKsZRA8EfeObyxPOzo=",
-      "dependencies": {
-        "component-emitter": {
-          "version": "1.2.1",
-          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
-          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
-        },
-        "debug": {
-          "version": "2.6.8",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
-          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw="
-        },
-        "engine.io": {
-          "version": "3.1.0",
-          "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.0.tgz",
-          "integrity": "sha1-XKQ4486f28kVxKIcjdnhJmcG5X4="
-        },
-        "engine.io-parser": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz",
-          "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg="
-        },
-        "isarray": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-        },
-        "object-assign": {
-          "version": "4.1.1",
-          "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-          "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
-        },
-        "socket.io-adapter": {
-          "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.0.tgz",
-          "integrity": "sha1-x6pGUB3VVsLLiiivj/lcC14dqkw=",
-          "dependencies": {
-            "debug": {
-              "version": "2.3.3",
-              "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
-              "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w="
-            },
-            "ms": {
-              "version": "0.7.2",
-              "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz",
-              "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U="
-            }
-          }
-        },
-        "socket.io-parser": {
-          "version": "3.1.2",
-          "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
-          "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I="
-        },
-        "ultron": {
-          "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz",
-          "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ="
-        },
-        "ws": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz",
-          "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA="
-        }
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz",
+      "integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=",
+      "requires": {
+        "debug": "2.6.9",
+        "engine.io": "3.1.4",
+        "socket.io-adapter": "1.1.1",
+        "socket.io-client": "2.0.4",
+        "socket.io-parser": "3.1.2"
       }
     },
+    "socket.io-adapter": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
+      "integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
+    },
     "socket.io-client": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.2.tgz",
-      "integrity": "sha1-hvWdtZuhFockIg85viga1Jr3QsE=",
-      "dependencies": {
-        "component-emitter": {
-          "version": "1.2.1",
-          "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
-          "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
-        },
-        "debug": {
-          "version": "2.6.8",
-          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
-          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw="
-        },
-        "engine.io-client": {
-          "version": "3.1.1",
-          "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.1.tgz",
-          "integrity": "sha1-QVqYUrrbFPoAj6PvHjFgjbZ2EyU="
-        },
-        "engine.io-parser": {
-          "version": "2.1.1",
-          "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz",
-          "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg="
-        },
-        "isarray": {
-          "version": "2.0.1",
-          "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
-          "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
-        },
-        "ms": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
-          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
-        },
-        "socket.io-parser": {
-          "version": "3.1.2",
-          "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
-          "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I="
-        },
-        "ultron": {
-          "version": "1.1.0",
-          "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz",
-          "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ="
-        },
-        "ws": {
-          "version": "2.3.1",
-          "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz",
-          "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA="
-        }
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz",
+      "integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=",
+      "requires": {
+        "backo2": "1.0.2",
+        "base64-arraybuffer": "0.1.5",
+        "component-bind": "1.0.0",
+        "component-emitter": "1.2.1",
+        "debug": "2.6.9",
+        "engine.io-client": "3.1.4",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "object-component": "0.0.3",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "socket.io-parser": "3.1.2",
+        "to-array": "0.1.4"
+      }
+    },
+    "socket.io-parser": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
+      "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=",
+      "requires": {
+        "component-emitter": "1.2.1",
+        "debug": "2.6.9",
+        "has-binary2": "1.0.2",
+        "isarray": "2.0.1"
       }
     },
     "sprintf-js": {
-      "version": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
       "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
     },
     "supports-color": {
       "version": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz",
       "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "has-flag": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz"
+      }
     },
     "to-array": {
-      "version": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
       "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
     },
+    "ultron": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
+      "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
+    },
     "universalify": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.0.tgz",
@@ -512,12 +649,24 @@
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
       "dev": true
     },
+    "ws": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
+      "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
+      "requires": {
+        "async-limiter": "1.0.0",
+        "safe-buffer": "5.1.1",
+        "ultron": "1.1.1"
+      }
+    },
     "xmlhttprequest-ssl": {
-      "version": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz",
-      "integrity": "sha1-GFqIjATspGw+QHDZn3tJ3jUomS0="
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz",
+      "integrity": "sha1-BPVgkVcks4kIhxXMDteBPpZ3v1c="
     },
     "yeast": {
-      "version": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
       "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
     }
   }

+ 6 - 6
package.json

@@ -18,12 +18,12 @@
     "url": "https://github.com/NikolaySuslov/livecodingspace.git"
   },
   "dependencies": {
-    "crypto": "0.0.x",
-    "socket.io": "2.0.2",
-    "socket.io-client": "^2.0.2",
-    "async": "2.4.1",
-    "mime": "1.3.6",
-    "js-yaml": "3.8.4",
+    "crypto": "1.0.1",
+    "socket.io": "2.0.4",
+    "socket.io-client": "^2.0.4",
+    "async": "2.6.0",
+    "mime": "2.1.0",
+    "js-yaml": "3.10.0",
     "optimist": "0.6.1",
     "fs-extra": "3.0.1"
   },

+ 10 - 9
public/aframe/index.vwf.yaml

@@ -4,7 +4,14 @@
 extends: http://vwf.example.com/aframe/ascene.vwf
 properties:
   fog: "type: linear; color: #ECECEC; far: 9; near: 0"
+  transparent: true
 children:
+  sky:
+    extends: http://vwf.example.com/aframe/asky.vwf
+    properties:
+      color: "#ECECEC"
+      side: "back"
+      fog: false
   spaceText:
     extends: http://vwf.example.com/aframe/atext.vwf
     properties:
@@ -84,7 +91,6 @@ children:
       box2:
         extends: http://vwf.example.com/aframe/abox.vwf
         properties:
-          src: "#bg"
           position: "2 -0.75 0"
           color: "#2167a5"
           depth: 1
@@ -93,20 +99,15 @@ children:
             extends: http://vwf.example.com/aframe/interpolation-component.vwf
             properties:
               enabled: true
-              duration: 50
-              deltaPos: 0.1
-              deltaRot: 1
         methods:
           run:
             body: |
               var time = vwf.now;
+              let rot = AFRAME.utils.coordinates.parse(this.rotation);
               let pos = AFRAME.utils.coordinates.parse(this.position);
-              this.position = [pos.x, pos.y, Math.sin(time)]
+              this.position = [pos.x, pos.y, Math.sin(time)];
+              this.rotation = [rot.x, rot.y, Math.sin(time)*100];
               this.future( 0.01 ).run();  // schedule the next step
-  sky:
-    extends: http://vwf.example.com/aframe/asky.vwf
-    properties:
-      color: "#ECECEC"
 methods:
   initialize:
     body: |

+ 9 - 1
public/aframe2/assets.json

@@ -9,7 +9,15 @@
     },
     "plane":{
         "tag": "a-asset-item",
-        "src": "/../assets/plane.dae"
+        "src": "/../assets/test.dae"
+    },
+    "plane-obj":{
+        "tag": "a-asset-item",
+        "src": "/../assets/models/plane/PUSHILIN_Plane.obj"
+    },
+    "plane-mtl":{
+        "tag": "a-asset-item",
+        "src": "/../assets/models/plane/PUSHILIN_Plane.mtl"
     },
      "bg2":{
         "tag": "img",

+ 9 - 20
public/aframe2/index.vwf.yaml

@@ -4,6 +4,7 @@
 extends: http://vwf.example.com/aframe/ascene.vwf
 properties:
   fog: "type: linear; color: #ECECEC; far: 30; near: 0"
+  transparent: true
   assets: "assets.json"
 children:
   myLight:
@@ -14,12 +15,13 @@ children:
       position: "0 10 5"
       rotation: "0 0 0"
   model:
-    extends: http://vwf.example.com/aframe/acolladamodel.vwf
+    extends: http://vwf.example.com/aframe/aobjmodel.vwf
     properties:
-      src: "#plane"
-      position: "-1.0 1.7 -3"
+      src: "#plane-obj"
+      mtl: "#plane-mtl"
+      position: "-1.2 1.7 -2.5"
       rotation: "0 -45 0"
-      scale: "10 10 10"
+      scale: "0.5 0.5 0.5"
   spaceText:
     extends: http://vwf.example.com/aframe/atext.vwf
     properties:
@@ -41,19 +43,6 @@ children:
       depth: 2
       height: 1
       width: 1
-    methods:
-      createGizmo:
-        body: |
-          let gizmoNode = 
-            {
-            "extends": "http://vwf.example.com/aframe/gizmoComponent.vwf",
-            "type": "component",
-            "properties":
-            {
-              "mode": "translate"
-            }
-              }
-            this.children.create("gizmo", gizmoNode);
   box:
     extends: http://vwf.example.com/aframe/abox.vwf
     properties:
@@ -131,8 +120,8 @@ children:
             properties:
               enabled: true
               duration: 50
-              deltaPos: 0.1
-              deltaRot: 1
+              deltaPos: 0.001
+              deltaRot: 0.1
         methods:
           run:
             body: |
@@ -143,8 +132,8 @@ children:
   sky:
     extends: http://vwf.example.com/aframe/asky.vwf
     properties:
-      color: "#ECECEC"
       src: "#sky"
+      side: "back"
       fog: false
   groundPlane:
     extends: http://vwf.example.com/aframe/aplane.vwf

BIN
public/assets/images/planeDiffuse.png


BIN
public/assets/models/plane/PUSHILIN_PLANE.png


+ 13 - 0
public/assets/models/plane/PUSHILIN_Plane.mtl

@@ -0,0 +1,13 @@
+# Blender MTL File: '1 iternal.blend'
+# Material Count: 1
+
+newmtl plane
+Ns 96.078431
+Ka 1.000000 1.000000 1.000000
+Kd 0.640000 0.640000 0.640000
+Ks 0.500000 0.500000 0.500000
+Ke 0.000000 0.000000 0.000000
+Ni 1.000000
+d 1.000000
+illum 0
+map_Kd PUSHILIN_PLANE.png

+ 4610 - 0
public/assets/models/plane/PUSHILIN_Plane.obj

@@ -0,0 +1,4610 @@
+# Blender v2.78 (sub 0) OBJ File: '1 iternal.blend'
+# www.blender.org
+mtllib PUSHILIN_Plane.mtl
+o PUSHILIN_Plane_Circle.000
+v 0.892075 -0.103654 0.199111
+v 0.849999 0.060389 0.322168
+v 0.797989 0.263158 0.322168
+v 0.755913 0.427201 0.199111
+v 0.860313 0.020178 0.106218
+v 0.837866 0.107689 0.171864
+v 0.810122 0.215858 0.171864
+v 0.787675 0.303369 0.106218
+v 0.476754 -0.232155 0.214577
+v 0.431409 -0.055370 0.347192
+v 0.375360 0.163148 0.347192
+v 0.330016 0.339933 0.214577
+v 0.106471 -0.281119 0.182191
+v 0.067970 -0.131016 0.294791
+v -0.538795 -0.373472 0.103245
+v -0.566415 -0.265790 0.167054
+v -0.628175 -0.025006 0.103245
+v -0.845080 -0.419006 0.031253
+v -0.867788 -0.330476 0.032004
+v -0.322608 -0.213994 0.216146
+v -0.290806 -0.337979 0.133586
+v 0.739841 0.489860 0.000001
+v 0.908147 -0.166313 0.000001
+v 0.892075 -0.103654 -0.199110
+v 0.849998 0.060389 -0.322166
+v 0.797989 0.263158 -0.322166
+v 0.755913 0.427201 -0.199109
+v 0.779102 0.336795 0.000001
+v 0.868886 -0.013248 0.000001
+v 0.860312 0.020178 -0.106217
+v 0.837866 0.107689 -0.171863
+v 0.810121 0.215858 -0.171863
+v 0.787675 0.303369 -0.106217
+v 0.312696 0.407459 0.000001
+v 0.494074 -0.299681 0.000001
+v 0.476754 -0.232155 -0.214575
+v 0.431409 -0.055370 -0.347190
+v 0.375360 0.163148 -0.347190
+v 0.330016 0.339933 -0.214575
+v 0.121177 -0.338453 0.000001
+v 0.106471 -0.281119 -0.182190
+v 0.067970 -0.131016 -0.294789
+v -0.638725 0.016125 0.000001
+v -0.528245 -0.414603 0.000001
+v -0.538795 -0.373472 -0.103244
+v -0.566415 -0.265790 -0.167053
+v -0.628175 -0.025006 -0.103244
+v -0.836406 -0.452822 0.000001
+v -0.845080 -0.419006 -0.031252
+v -0.867788 -0.330476 -0.032003
+v -0.895856 -0.221047 0.000001
+v -0.867788 -0.330476 0.000001
+v -0.845080 -0.419006 0.000001
+v -0.322608 -0.213994 -0.216145
+v -0.278659 -0.385337 0.000001
+v -0.290806 -0.337979 -0.133584
+v 0.915103 -0.097747 0.199111
+v 0.873026 0.066296 0.322168
+v 0.821017 0.269064 0.322168
+v 0.778941 0.433107 0.199111
+v 1.034650 -0.044177 0.182988
+v 0.995981 0.106583 0.296080
+v 0.948184 0.292932 0.296080
+v 0.909514 0.443692 0.182988
+v 1.062472 -0.001100 0.157691
+v 1.029149 0.128818 0.255149
+v 0.987959 0.289406 0.255149
+v 0.954635 0.419324 0.157691
+v 1.024409 0.147297 0.046371
+v 1.014610 0.185501 0.075030
+v 1.002498 0.232724 0.075030
+v 0.992698 0.270928 0.046371
+v 0.902890 -0.050134 0.163394
+v 0.868362 0.084482 0.264376
+v 0.825682 0.250877 0.264376
+v 0.791154 0.385494 0.163394
+v -0.359435 0.114112 0.093738
+v -0.315628 0.125348 0.093738
+v -0.287870 -0.116624 0.093738
+v -0.244063 -0.105388 0.093738
+v -0.310619 -0.064857 0.093738
+v -0.275705 -0.018949 0.093738
+v -0.035649 -0.051651 0.093738
+v -0.067291 0.034787 0.093738
+v 0.837355 -0.147998 0.104938
+v 0.831113 -0.156310 0.086465
+v 0.812815 -0.163784 0.078813
+v 0.793178 -0.166041 0.086465
+v 0.783706 -0.161758 0.104938
+v 0.789947 -0.153446 0.123411
+v 0.808246 -0.145972 0.131063
+v 0.827883 -0.143715 0.123411
+v 0.974202 -0.408687 0.205797
+v 0.967961 -0.417000 0.187324
+v 0.949662 -0.424473 0.179672
+v 0.930026 -0.426730 0.187324
+v 0.920554 -0.422447 0.205797
+v 0.926795 -0.414135 0.224270
+v 0.945094 -0.406661 0.231922
+v 0.964730 -0.404404 0.224270
+v 1.040530 -0.391849 0.261573
+v 1.009309 -0.342626 0.261573
+v 0.955119 -0.321156 0.261573
+v 0.898658 -0.335638 0.261573
+v 0.861492 -0.380541 0.261573
+v 0.857818 -0.438714 0.261573
+v 0.889039 -0.487936 0.261573
+v 0.943229 -0.509407 0.261573
+v 0.999690 -0.494925 0.261573
+v 1.036856 -0.450022 0.261573
+v 1.004089 -0.401196 0.282305
+v 0.985322 -0.371608 0.282305
+v 0.952748 -0.358702 0.282305
+v 0.918809 -0.367407 0.282305
+v 0.896468 -0.394399 0.282305
+v 0.894259 -0.429367 0.282305
+v 0.913026 -0.458955 0.282305
+v 0.945601 -0.471861 0.282305
+v 0.979540 -0.463156 0.282305
+v 1.001880 -0.436164 0.282305
+v 0.949174 -0.415281 0.290319
+v 1.008123 -0.508220 0.281296
+v 0.942237 -0.525119 0.281296
+v 0.879001 -0.500064 0.281296
+v 0.842568 -0.442625 0.281296
+v 0.846856 -0.374742 0.281296
+v 0.890226 -0.322343 0.281296
+v 0.956111 -0.305443 0.281296
+v 1.019348 -0.330498 0.281296
+v 1.055780 -0.387937 0.281296
+v 1.051492 -0.455821 0.281296
+v 1.023922 -0.533129 0.281296
+v 0.940377 -0.554558 0.281296
+v 0.860193 -0.522788 0.281296
+v 0.813996 -0.449954 0.281296
+v 0.819433 -0.363876 0.281296
+v 0.874426 -0.297434 0.281296
+v 0.957971 -0.276005 0.281296
+v 1.038155 -0.307775 0.281296
+v 1.084352 -0.380609 0.281296
+v 1.078915 -0.466686 0.281296
+v 1.037447 -0.554452 0.257933
+v 0.938786 -0.579758 0.257933
+v 0.844092 -0.542240 0.257933
+v 0.789537 -0.456228 0.257933
+v 0.795957 -0.354575 0.257933
+v 0.860901 -0.276110 0.257933
+v 0.959563 -0.250804 0.257933
+v 1.054256 -0.288323 0.257933
+v 1.108811 -0.374335 0.257933
+v 1.102391 -0.475987 0.257933
+v 1.037447 -0.554452 0.218437
+v 0.938786 -0.579758 0.218437
+v 0.844092 -0.542240 0.218437
+v 0.789537 -0.456228 0.218437
+v 0.795957 -0.354575 0.218437
+v 0.860901 -0.276110 0.218437
+v 0.959563 -0.250804 0.218437
+v 1.054256 -0.288323 0.218437
+v 1.108811 -0.374335 0.218437
+v 1.102391 -0.475987 0.218437
+v 1.026304 -0.536884 0.196773
+v 0.940097 -0.558996 0.196773
+v 0.857357 -0.526214 0.196773
+v 0.809688 -0.451059 0.196773
+v 0.815298 -0.362238 0.196773
+v 0.872044 -0.293678 0.196773
+v 0.958251 -0.271566 0.196773
+v 1.040991 -0.304349 0.196773
+v 1.088660 -0.379504 0.196773
+v 1.083050 -0.468324 0.196773
+v 1.005991 -0.504859 0.196773
+v 0.942487 -0.521148 0.196773
+v 0.881538 -0.496999 0.196773
+v 0.846423 -0.441637 0.196773
+v 0.850555 -0.376207 0.196773
+v 0.892357 -0.325703 0.196773
+v 0.955861 -0.309415 0.196773
+v 1.016810 -0.333564 0.196773
+v 1.051925 -0.388926 0.196773
+v 1.047793 -0.454355 0.196773
+v 0.996834 -0.490422 0.216793
+v 0.943565 -0.504085 0.216793
+v 0.892439 -0.483828 0.216793
+v 0.862984 -0.437389 0.216793
+v 0.866450 -0.382505 0.216793
+v 0.901514 -0.340141 0.216793
+v 0.954783 -0.326478 0.216793
+v 1.005909 -0.346735 0.216793
+v 1.035364 -0.393174 0.216793
+v 1.031898 -0.448057 0.216793
+v 0.948893 -0.415354 0.214574
+v 1.044985 0.121392 0.009480
+v 1.039492 0.144269 0.063320
+v 1.027417 0.193486 0.093048
+v 1.013372 0.250243 0.087308
+v 1.121671 0.140345 0.009251
+v 1.116178 0.163222 0.063091
+v 1.104103 0.212438 0.092819
+v 1.090058 0.269196 0.087079
+v 1.180736 0.214811 0.003385
+v 1.178614 0.223645 0.024093
+v 1.173962 0.242608 0.035546
+v 1.168556 0.264455 0.033371
+v 1.139047 0.227527 0.069435
+v 1.127877 0.272655 0.063746
+v 1.153194 0.169527 0.006705
+v 1.148865 0.187551 0.048676
+v -0.624846 -0.529451 0.019564
+v -0.632969 -0.516646 0.019564
+v -0.647067 -0.511060 0.019564
+v -0.661756 -0.514827 0.019564
+v -0.671425 -0.526510 0.019564
+v -0.672381 -0.541644 0.019564
+v -0.664259 -0.554450 0.019564
+v -0.650160 -0.560036 0.019564
+v -0.635471 -0.556268 0.019564
+v -0.625802 -0.544586 0.019564
+v -0.625547 -0.529631 0.028272
+v -0.633430 -0.517202 0.028272
+v -0.647113 -0.511781 0.028272
+v -0.661369 -0.515438 0.028272
+v -0.670753 -0.526776 0.028272
+v -0.671681 -0.541464 0.028272
+v -0.663798 -0.553893 0.028272
+v -0.650115 -0.559314 0.028272
+v -0.635859 -0.555658 0.028272
+v -0.626474 -0.544320 0.028272
+v -0.648614 -0.535548 0.031639
+v -0.630588 -0.563967 0.032291
+v -0.650735 -0.569135 0.032291
+v -0.670072 -0.561473 0.032291
+v -0.681213 -0.543909 0.032291
+v -0.679902 -0.523151 0.032291
+v -0.666640 -0.507128 0.032291
+v -0.646492 -0.501961 0.032291
+v -0.627156 -0.509622 0.032291
+v -0.616015 -0.527186 0.032291
+v -0.617326 -0.547944 0.032291
+v -0.621757 -0.577891 0.034501
+v -0.651775 -0.585591 0.034501
+v -0.680585 -0.574176 0.034501
+v -0.697184 -0.548006 0.034501
+v -0.695231 -0.517078 0.034501
+v -0.675471 -0.493204 0.034501
+v -0.645453 -0.485505 0.034501
+v -0.616642 -0.496920 0.034501
+v -0.600043 -0.523090 0.034501
+v -0.601997 -0.554018 0.034501
+v -0.611660 -0.593808 0.018035
+v -0.652963 -0.604402 0.018035
+v -0.692604 -0.588696 0.018035
+v -0.715442 -0.552689 0.018035
+v -0.712754 -0.510134 0.018035
+v -0.685567 -0.477287 0.018035
+v -0.644265 -0.466693 0.018035
+v -0.604624 -0.482399 0.018035
+v -0.581786 -0.518407 0.018035
+v -0.584473 -0.560961 0.018035
+v -0.244063 -0.105388 0.146947
+v -0.275705 -0.018949 0.146947
+v -0.035649 -0.051651 0.146947
+v -0.067291 0.034787 0.146947
+v -0.083641 0.098194 0.093738
+v -0.292055 0.044457 0.093738
+v -0.083641 0.098194 0.146947
+v -0.292055 0.044457 0.146947
+v 0.762869 0.495766 0.000001
+v 0.931175 -0.160406 0.000001
+v 0.915103 -0.097747 -0.199110
+v 0.873026 0.066296 -0.322166
+v 0.821017 0.269064 -0.322166
+v 0.778941 0.433107 -0.199109
+v 0.894744 0.501276 0.000001
+v 1.049421 -0.101762 0.000001
+v 1.034650 -0.044177 -0.182986
+v 0.995981 0.106583 -0.296078
+v 0.948183 0.292932 -0.296078
+v 0.909514 0.443692 -0.182986
+v 0.941907 0.468948 0.000001
+v 1.075201 -0.050724 0.000001
+v 1.062472 -0.001100 -0.157690
+v 1.029149 0.128818 -0.255148
+v 0.987959 0.289406 -0.255148
+v 0.954635 0.419324 -0.157689
+v 0.988955 0.285520 0.000001
+v 1.028152 0.132704 0.000001
+v 1.024409 0.147297 -0.046370
+v 1.014610 0.185501 -0.075029
+v 1.002498 0.232724 -0.075029
+v 0.992698 0.270928 -0.046370
+v 0.777965 0.436913 0.000001
+v 0.916079 -0.101553 0.000001
+v 0.902890 -0.050134 -0.163392
+v 0.868362 0.084482 -0.264375
+v 0.825682 0.250877 -0.264375
+v 0.791153 0.385494 -0.163392
+v -0.359435 0.114112 -0.093737
+v -0.315628 0.125348 -0.093737
+v -0.287870 -0.116624 -0.093737
+v -0.244063 -0.105388 -0.093737
+v -0.310619 -0.064857 -0.093737
+v -0.275705 -0.018949 -0.093737
+v -0.035649 -0.051651 -0.093737
+v -0.067291 0.034787 -0.093737
+v 0.835172 -0.143839 -0.103328
+v 0.828930 -0.152152 -0.084855
+v 0.810632 -0.159626 -0.077203
+v 0.790995 -0.161882 -0.084855
+v 0.781523 -0.157600 -0.103328
+v 0.787764 -0.149287 -0.121801
+v 0.806063 -0.141814 -0.129453
+v 0.825700 -0.139557 -0.121801
+v 0.974202 -0.408687 -0.205796
+v 0.967961 -0.417000 -0.187323
+v 0.949662 -0.424473 -0.179671
+v 0.930025 -0.426730 -0.187323
+v 0.920554 -0.422447 -0.205796
+v 0.926795 -0.414135 -0.224269
+v 0.945093 -0.406661 -0.231921
+v 0.964730 -0.404404 -0.224269
+v 1.040530 -0.391849 -0.261572
+v 1.009309 -0.342626 -0.261572
+v 0.955119 -0.321156 -0.261572
+v 0.898658 -0.335638 -0.261572
+v 0.861492 -0.380541 -0.261572
+v 0.857818 -0.438714 -0.261572
+v 0.889039 -0.487936 -0.261572
+v 0.943229 -0.509407 -0.261572
+v 0.999690 -0.494925 -0.261572
+v 1.036856 -0.450022 -0.261572
+v 1.004089 -0.401196 -0.282303
+v 0.985322 -0.371608 -0.282303
+v 0.952747 -0.358702 -0.282303
+v 0.918808 -0.367407 -0.282303
+v 0.896468 -0.394399 -0.282303
+v 0.894259 -0.429367 -0.282303
+v 0.913026 -0.458955 -0.282303
+v 0.945600 -0.471861 -0.282303
+v 0.979540 -0.463156 -0.282303
+v 1.001880 -0.436164 -0.282303
+v 0.949174 -0.415281 -0.290318
+v 1.008122 -0.508220 -0.281295
+v 0.942237 -0.525119 -0.281295
+v 0.879000 -0.500064 -0.281295
+v 0.842568 -0.442625 -0.281295
+v 0.846856 -0.374742 -0.281295
+v 0.890225 -0.322343 -0.281295
+v 0.956111 -0.305443 -0.281295
+v 1.019347 -0.330498 -0.281295
+v 1.055780 -0.387937 -0.281295
+v 1.051492 -0.455821 -0.281295
+v 1.023922 -0.533129 -0.281295
+v 0.940377 -0.554558 -0.281295
+v 0.860193 -0.522788 -0.281295
+v 0.813996 -0.449954 -0.281295
+v 0.819433 -0.363876 -0.281295
+v 0.874426 -0.297434 -0.281295
+v 0.957971 -0.276005 -0.281295
+v 1.038155 -0.307775 -0.281295
+v 1.084352 -0.380609 -0.281295
+v 1.078915 -0.466686 -0.281295
+v 1.037447 -0.554452 -0.257931
+v 0.938786 -0.579758 -0.257931
+v 0.844092 -0.542240 -0.257931
+v 0.789537 -0.456228 -0.257931
+v 0.795957 -0.354575 -0.257931
+v 0.860901 -0.276110 -0.257931
+v 0.959562 -0.250804 -0.257931
+v 1.054255 -0.288323 -0.257931
+v 1.108811 -0.374335 -0.257931
+v 1.102391 -0.475987 -0.257931
+v 1.037447 -0.554452 -0.218436
+v 0.938786 -0.579758 -0.218436
+v 0.844092 -0.542240 -0.218436
+v 0.789537 -0.456228 -0.218436
+v 0.795957 -0.354575 -0.218436
+v 0.860901 -0.276110 -0.218436
+v 0.959562 -0.250804 -0.218436
+v 1.054255 -0.288323 -0.218436
+v 1.108811 -0.374335 -0.218436
+v 1.102391 -0.475987 -0.218436
+v 1.026304 -0.536884 -0.196771
+v 0.940097 -0.558996 -0.196771
+v 0.857357 -0.526214 -0.196771
+v 0.809688 -0.451059 -0.196771
+v 0.815298 -0.362238 -0.196771
+v 0.872044 -0.293678 -0.196771
+v 0.958251 -0.271566 -0.196771
+v 1.040991 -0.304349 -0.196771
+v 1.088660 -0.379504 -0.196771
+v 1.083050 -0.468324 -0.196771
+v 1.005991 -0.504859 -0.196771
+v 0.942487 -0.521148 -0.196771
+v 0.881538 -0.496999 -0.196771
+v 0.846423 -0.441637 -0.196771
+v 0.850555 -0.376207 -0.196771
+v 0.892357 -0.325703 -0.196771
+v 0.955860 -0.309415 -0.196771
+v 1.016810 -0.333564 -0.196771
+v 1.051925 -0.388926 -0.196771
+v 1.047793 -0.454355 -0.196771
+v 0.996834 -0.490422 -0.216792
+v 0.943565 -0.504085 -0.216792
+v 0.892439 -0.483828 -0.216792
+v 0.862984 -0.437389 -0.216792
+v 0.866450 -0.382505 -0.216792
+v 0.901514 -0.340141 -0.216792
+v 0.954783 -0.326478 -0.216792
+v 1.005909 -0.346735 -0.216792
+v 1.035364 -0.393174 -0.216792
+v 1.031898 -0.448057 -0.216792
+v 0.948892 -0.415354 -0.214572
+v 1.002722 0.292862 0.048293
+v 1.041797 0.133594 -0.047907
+v 1.031147 0.176213 -0.086922
+v 1.017103 0.232970 -0.092662
+v 1.005028 0.282187 -0.062934
+v 0.999535 0.305063 -0.009094
+v 1.079408 0.311815 0.048064
+v 1.118483 0.152547 -0.048137
+v 1.107833 0.195166 -0.087152
+v 1.093788 0.251923 -0.092892
+v 1.081714 0.301140 -0.063164
+v 1.076221 0.324016 -0.009324
+v 1.164444 0.280911 0.018277
+v 1.179499 0.219547 -0.018788
+v 1.175387 0.236003 -0.033882
+v 1.169981 0.257850 -0.036057
+v 1.165329 0.276813 -0.024603
+v 1.163207 0.285647 -0.003896
+v 1.122414 0.293425 -0.046447
+v 1.118284 0.310661 -0.003089
+v 1.142479 0.211977 -0.067945
+v 1.131508 0.256316 -0.072248
+v 1.120158 0.303568 0.037522
+v 1.150759 0.178842 -0.037814
+v -0.624846 -0.529451 -0.019563
+v -0.632969 -0.516646 -0.019563
+v -0.647067 -0.511060 -0.019563
+v -0.661756 -0.514827 -0.019563
+v -0.671425 -0.526510 -0.019563
+v -0.672381 -0.541644 -0.019563
+v -0.664259 -0.554450 -0.019563
+v -0.650160 -0.560036 -0.019563
+v -0.635471 -0.556268 -0.019563
+v -0.625802 -0.544586 -0.019563
+v -0.625547 -0.529631 -0.028271
+v -0.633430 -0.517202 -0.028271
+v -0.647113 -0.511781 -0.028271
+v -0.661369 -0.515438 -0.028271
+v -0.670753 -0.526776 -0.028271
+v -0.671681 -0.541464 -0.028271
+v -0.663798 -0.553893 -0.028271
+v -0.650115 -0.559314 -0.028271
+v -0.635859 -0.555658 -0.028271
+v -0.626474 -0.544320 -0.028271
+v -0.648614 -0.535548 -0.031638
+v -0.630588 -0.563967 -0.032289
+v -0.650735 -0.569135 -0.032289
+v -0.670072 -0.561473 -0.032289
+v -0.681213 -0.543909 -0.032289
+v -0.679902 -0.523151 -0.032289
+v -0.666640 -0.507128 -0.032289
+v -0.646492 -0.501961 -0.032289
+v -0.627156 -0.509622 -0.032289
+v -0.616015 -0.527186 -0.032289
+v -0.617326 -0.547944 -0.032289
+v -0.621757 -0.577891 -0.034500
+v -0.651775 -0.585591 -0.034500
+v -0.680585 -0.574176 -0.034500
+v -0.697184 -0.548006 -0.034500
+v -0.695231 -0.517078 -0.034500
+v -0.675471 -0.493204 -0.034500
+v -0.645453 -0.485505 -0.034500
+v -0.616642 -0.496920 -0.034500
+v -0.600043 -0.523090 -0.034500
+v -0.601997 -0.554018 -0.034500
+v -0.611660 -0.593808 -0.018034
+v -0.652963 -0.604402 -0.018034
+v -0.692604 -0.588696 -0.018034
+v -0.715442 -0.552689 -0.018034
+v -0.712754 -0.510134 -0.018034
+v -0.685567 -0.477287 -0.018034
+v -0.644265 -0.466693 -0.018034
+v -0.604624 -0.482399 -0.018034
+v -0.581786 -0.518407 -0.018034
+v -0.584473 -0.560961 -0.018034
+v -0.611535 -0.594007 0.000001
+v -0.652977 -0.604637 0.000001
+v -0.692754 -0.588877 0.000001
+v -0.715670 -0.552747 0.000001
+v -0.712973 -0.510048 0.000001
+v -0.685693 -0.477088 0.000001
+v -0.644250 -0.466459 0.000001
+v -0.604474 -0.482218 0.000001
+v -0.581558 -0.518348 0.000001
+v -0.584255 -0.561047 0.000001
+v -0.244063 -0.105388 -0.146945
+v -0.275705 -0.018949 -0.146945
+v -0.035649 -0.051651 -0.146945
+v -0.067291 0.034787 -0.146945
+v -0.083641 0.098194 -0.093737
+v -0.292055 0.044457 -0.093737
+v -0.083641 0.098194 -0.146945
+v -0.292055 0.044457 -0.146945
+v 0.020381 0.054522 0.294790
+v -0.018120 0.204625 0.182190
+v -0.600555 -0.132688 0.167054
+v -0.895856 -0.221047 0.032004
+v -0.918563 -0.132516 0.031252
+v -1.020322 0.264209 0.031252
+v -0.942985 -0.037302 0.031252
+v -0.814187 0.063500 0.032215
+v -0.951527 0.214682 0.032215
+v -0.981654 0.113453 0.031252
+v -0.858562 -0.422464 0.031252
+v -0.881269 -0.333934 0.032004
+v -0.909338 -0.224505 0.032004
+v -0.932045 -0.135974 0.031252
+v -1.033804 0.260751 0.031252
+v -0.956467 -0.040760 0.031252
+v -0.995135 0.109995 0.031252
+v -1.061334 -0.474475 0.015865
+v -1.151131 -0.403153 0.016247
+v -1.186759 -0.295662 0.016247
+v -1.209467 -0.207132 0.015865
+v -1.135139 0.189626 0.015865
+v -1.188806 0.065486 0.015865
+v -1.213374 -0.104656 0.015865
+v -0.801706 -0.184282 0.485181
+v -0.895856 -0.221047 0.485181
+v -0.691581 -0.156036 0.379138
+v -0.626536 -0.139352 0.273096
+v -0.895856 -0.221047 0.183063
+v -0.895856 -0.221047 0.334122
+v -0.604330 -0.117969 0.167054
+v -0.905314 -0.184173 0.060628
+v -0.805481 -0.169564 0.485181
+v -0.905314 -0.184173 0.485181
+v -0.630312 -0.124633 0.273096
+v -0.905314 -0.184173 0.334122
+v -0.695357 -0.141317 0.379138
+v -0.905314 -0.184173 0.183063
+v -0.901562 -0.222255 0.060184
+v -0.901562 -0.222255 0.485181
+v -0.901562 -0.222255 0.183063
+v -0.901562 -0.222255 0.334122
+v -0.927691 -0.194571 0.060184
+v -0.927691 -0.194571 0.485181
+v -0.927691 -0.194571 0.334122
+v -0.927691 -0.194571 0.183063
+v -0.983631 -0.299713 0.334122
+v -0.929214 -0.248354 0.485181
+v -1.017420 -0.331605 0.060184
+v -1.010957 -0.325505 0.183063
+v -0.993541 -0.289213 0.334122
+v -0.939124 -0.237854 0.485181
+v -1.027330 -0.321105 0.060184
+v -1.020867 -0.315005 0.183063
+v 0.560710 0.478158 0.207038
+v 0.041581 0.369786 0.207038
+v 0.560710 0.478158 1.642936
+v 0.041581 0.369786 1.642936
+v 0.481217 0.474041 1.811573
+v 0.119206 0.381187 1.811573
+v 0.375634 0.446960 1.888369
+v 0.224788 0.408268 1.888369
+v 0.036432 0.389862 0.207038
+v 0.543632 0.544739 0.207038
+v 0.036431 0.389862 1.642936
+v 0.543632 0.544739 1.642936
+v 0.109960 0.417232 1.811573
+v 0.471971 0.510086 1.811573
+v 0.215543 0.444313 1.888369
+v 0.366389 0.483004 1.888369
+v 0.572950 0.516778 0.207038
+v 0.572950 0.516778 1.642936
+v 0.039006 0.379824 0.207038
+v 0.220165 0.426291 1.888369
+v 0.114583 0.399209 1.811573
+v 0.371012 0.464982 1.888369
+v 0.497373 0.497393 1.811573
+v 0.039006 0.379824 1.642936
+v 0.683494 -0.000540 0.488844
+v 0.164365 -0.108912 0.488844
+v 0.683494 -0.000540 1.642936
+v 0.164365 -0.108912 1.642936
+v 0.604001 -0.004657 1.811573
+v 0.241990 -0.097511 1.811573
+v 0.498418 -0.031738 1.888369
+v 0.347572 -0.070430 1.888369
+v 0.159216 -0.088836 0.488844
+v 0.666416 0.066041 0.488844
+v 0.159215 -0.088836 1.642936
+v 0.666416 0.066041 1.642936
+v 0.232745 -0.061466 1.811573
+v 0.594755 0.031388 1.811573
+v 0.338327 -0.034385 1.888369
+v 0.489173 0.004306 1.888369
+v 0.695734 0.038080 0.488844
+v 0.695734 0.038080 1.642936
+v 0.161790 -0.098874 0.488844
+v 0.342949 -0.052407 1.888369
+v 0.237367 -0.079489 1.811573
+v 0.493796 -0.013716 1.888369
+v 0.620157 0.018695 1.811573
+v 0.161790 -0.098874 1.642936
+v 0.169411 -0.128585 0.260200
+v 0.700229 -0.065786 0.260200
+v 0.154169 -0.069162 0.260200
+v 0.649681 0.131288 0.260200
+v 0.695734 0.038080 0.260200
+v 0.161790 -0.098874 0.260200
+v 0.052427 0.327499 0.147904
+v 0.569809 0.442682 0.164881
+v 0.048786 0.341695 0.133247
+v 0.557734 0.489762 0.116271
+v 0.584550 0.471551 0.140576
+v 0.050607 0.334597 0.140576
+v 0.456597 0.456424 1.203159
+v 0.145694 0.391520 1.203159
+v 0.456597 0.456424 1.238526
+v 0.145694 0.391520 1.238526
+v 0.190975 0.349151 1.206875
+v 0.436549 0.400416 1.206875
+v 0.190975 0.349151 1.234810
+v 0.436549 0.400416 1.234810
+v 0.232635 0.309816 1.210284
+v 0.418277 0.348570 1.210284
+v 0.232635 0.309816 1.231401
+v 0.418277 0.348570 1.231401
+v 0.310046 0.008015 1.210284
+v 0.495687 0.046769 1.210284
+v 0.310046 0.008015 1.231401
+v 0.495687 0.046769 1.231401
+v 0.301088 -0.048430 1.207753
+v 0.531219 -0.000389 1.207753
+v 0.301088 -0.048430 1.233932
+v 0.531219 -0.000389 1.233932
+v -0.361917 -0.060740 0.216145
+v -0.393719 0.063246 0.133585
+v 0.037321 -0.011525 0.244091
+v 0.037321 -0.011525 0.182190
+v -0.295395 -0.096865 0.165446
+v -0.295395 -0.096865 0.133585
+v -0.112082 0.362755 0.106748
+v -0.709622 -0.402704 0.036062
+v -0.661571 -0.390379 0.036062
+v -0.679269 -0.521040 0.016092
+v -0.631218 -0.508715 0.016092
+v -0.007398 0.207375 0.182190
+v -0.101360 0.365505 0.106748
+v -0.012411 0.226918 0.197565
+v -0.106373 0.385048 0.122122
+v -0.032844 0.221677 0.197565
+v -0.126805 0.379807 0.122122
+v 0.796302 0.123426 0.384930
+v 0.779472 0.189042 0.384930
+v 0.823953 0.097930 0.314791
+v 0.794043 0.214538 0.314791
+v 0.673151 0.129858 0.353539
+v 0.681289 0.101381 0.353539
+v 0.669989 0.140924 0.321880
+v 0.684451 0.090316 0.321880
+v 0.739131 0.352338 0.327501
+v 0.724956 0.405865 0.288475
+v 0.764280 0.289042 0.286335
+v 0.739090 0.384167 0.216981
+v 0.615716 0.344409 0.297519
+v 0.622701 0.321142 0.314462
+v 0.612348 0.334694 0.265441
+v 0.624761 0.293344 0.295551
+v 0.376825 -0.032416 1.219295
+v 0.366160 -0.035508 1.217731
+v 0.363217 -0.026764 1.222155
+v 0.373446 -0.023798 1.223656
+v 0.381905 0.479107 0.203021
+v 0.370322 0.475749 0.201322
+v 0.367838 0.484627 0.205814
+v 0.378068 0.487592 0.207315
+v 0.233763 0.428698 1.223826
+v 0.222685 0.424432 1.222424
+v 0.220388 0.433470 1.218175
+v 0.230356 0.437309 1.219438
+v 0.449913 0.004704 0.315832
+v 0.437657 -0.000016 0.314280
+v 0.435949 0.009249 0.310106
+v 0.445917 0.013088 0.311368
+v 1.083683 0.150041 0.024883
+v 1.017611 0.204212 0.065181
+v 1.118428 0.182430 0.038317
+v 1.052356 0.236600 0.078615
+v 1.055575 -0.026825 0.550587
+v 1.188766 -0.136025 0.469353
+v 1.080182 -0.003888 0.560101
+v 1.213374 -0.113088 0.478867
+v 1.086800 -0.069554 0.593881
+v 1.180993 -0.146780 0.536431
+v 1.103351 -0.054126 0.600280
+v 1.197544 -0.131352 0.542830
+v 1.125187 -0.105809 0.600377
+v 1.156613 -0.131575 0.581210
+v 1.134700 -0.096941 0.604055
+v 1.166126 -0.122707 0.584888
+v -0.068426 0.121118 0.078173
+v -0.078022 0.153731 0.078173
+v -0.082638 0.169418 0.039637
+v -0.063810 0.105431 0.039637
+v -0.086797 0.183555 0.099874
+v -0.091413 0.199242 0.061338
+v -0.109873 0.261981 0.069042
+v -0.102034 0.235338 0.037722
+v -0.033633 0.131355 0.078173
+v -0.043229 0.163968 0.078173
+v -0.047845 0.179655 0.039637
+v -0.029018 0.115668 0.039637
+v -0.052005 0.193792 0.099874
+v -0.056620 0.209479 0.061338
+v -0.075080 0.272218 0.069042
+v -0.067241 0.245575 0.037722
+v -0.032826 0.261959 -0.000000
+v 0.020381 0.054522 -0.294790
+v -0.018120 0.204625 -0.182190
+v -0.600555 -0.132688 -0.167054
+v -0.895856 -0.221047 -0.032004
+v -0.918563 -0.132516 -0.031252
+v -0.918563 -0.132516 -0.000000
+v -1.020322 0.264209 -0.031252
+v -1.026338 0.287663 -0.000000
+v -0.799248 0.085274 -0.000000
+v -0.944429 -0.031673 -0.000000
+v -0.942985 -0.037302 -0.031252
+v -0.814187 0.063500 -0.032215
+v -0.936589 0.236456 -0.000000
+v -0.951527 0.214682 -0.032215
+v -0.981654 0.113453 -0.031252
+v -0.985383 0.127995 -0.000000
+v -0.858562 -0.422464 -0.031252
+v -0.881269 -0.333934 -0.032004
+v -0.909338 -0.224505 -0.032004
+v -0.932045 -0.135974 -0.031252
+v -0.932045 -0.135974 -0.000000
+v -0.909338 -0.224505 -0.000000
+v -0.881269 -0.333934 -0.000000
+v -0.858562 -0.422464 -0.000000
+v -0.849888 -0.456280 -0.000000
+v -1.033804 0.260751 -0.031252
+v -1.039819 0.284205 -0.000000
+v -0.957911 -0.035131 -0.000000
+v -0.956467 -0.040760 -0.031252
+v -0.995135 0.109995 -0.031252
+v -0.998865 0.124537 -0.000000
+v -0.949006 -0.481703 -0.000000
+v -1.061334 -0.474475 -0.015866
+v -1.151131 -0.403153 -0.016247
+v -1.186759 -0.295662 -0.016247
+v -1.209467 -0.207132 -0.015866
+v -1.186759 -0.295662 -0.000000
+v -1.209467 -0.207132 -0.000000
+v -1.151131 -0.403153 -0.000000
+v -1.061334 -0.474475 -0.000000
+v -1.135139 0.189626 -0.000000
+v -1.135139 0.189626 -0.015866
+v -1.213374 -0.104656 -0.000000
+v -1.188806 0.065486 -0.015866
+v -1.188806 0.065486 -0.000000
+v -1.213374 -0.104656 -0.015866
+v -0.801706 -0.184282 -0.485181
+v -0.895856 -0.221047 -0.485181
+v -0.691581 -0.156036 -0.379138
+v -0.626536 -0.139352 -0.273096
+v -0.895856 -0.221047 -0.183063
+v -0.895856 -0.221047 -0.334122
+v -0.604330 -0.117969 -0.167054
+v -0.905314 -0.184173 -0.060628
+v -0.805481 -0.169564 -0.485181
+v -0.905314 -0.184173 -0.485181
+v -0.630312 -0.124633 -0.273096
+v -0.905314 -0.184173 -0.334122
+v -0.695357 -0.141317 -0.379138
+v -0.905314 -0.184173 -0.183063
+v -0.901562 -0.222255 -0.060184
+v -0.901562 -0.222255 -0.485181
+v -0.901562 -0.222255 -0.183063
+v -0.901562 -0.222255 -0.334122
+v -0.927691 -0.194571 -0.060184
+v -0.927691 -0.194571 -0.485181
+v -0.927691 -0.194571 -0.334122
+v -0.927691 -0.194571 -0.183063
+v -0.983631 -0.299713 -0.334122
+v -0.929214 -0.248354 -0.485181
+v -1.017420 -0.331605 -0.060184
+v -1.010957 -0.325505 -0.183063
+v -0.993541 -0.289213 -0.334122
+v -0.939124 -0.237854 -0.485181
+v -1.027330 -0.321105 -0.060184
+v -1.020867 -0.315005 -0.183063
+v 0.560710 0.478158 -0.207038
+v 0.041581 0.369786 -0.207038
+v 0.560710 0.478158 -1.642936
+v 0.041581 0.369786 -1.642936
+v 0.481217 0.474041 -1.811573
+v 0.119206 0.381187 -1.811573
+v 0.375634 0.446960 -1.888369
+v 0.224788 0.408268 -1.888369
+v 0.036432 0.389862 -0.207038
+v 0.543632 0.544739 -0.207038
+v 0.036432 0.389862 -1.642936
+v 0.543632 0.544739 -1.642936
+v 0.109961 0.417232 -1.811573
+v 0.471971 0.510086 -1.811573
+v 0.215543 0.444313 -1.888369
+v 0.366389 0.483004 -1.888369
+v 0.572950 0.516778 -0.207038
+v 0.572950 0.516778 -1.642936
+v 0.039006 0.379824 -0.207038
+v 0.220166 0.426291 -1.888369
+v 0.114583 0.399209 -1.811573
+v 0.371012 0.464982 -1.888369
+v 0.497373 0.497393 -1.811573
+v 0.039006 0.379824 -1.642936
+v 0.683494 -0.000540 -0.488844
+v 0.164365 -0.108912 -0.488844
+v 0.683494 -0.000540 -1.642936
+v 0.164365 -0.108912 -1.642936
+v 0.604001 -0.004657 -1.811573
+v 0.241990 -0.097511 -1.811573
+v 0.498419 -0.031738 -1.888369
+v 0.347572 -0.070430 -1.888369
+v 0.159216 -0.088836 -0.488844
+v 0.666416 0.066041 -0.488844
+v 0.159216 -0.088836 -1.642936
+v 0.666416 0.066041 -1.642936
+v 0.232745 -0.061466 -1.811573
+v 0.594756 0.031388 -1.811573
+v 0.338327 -0.034385 -1.888369
+v 0.489173 0.004306 -1.888369
+v 0.695734 0.038080 -0.488844
+v 0.695734 0.038080 -1.642936
+v 0.161790 -0.098874 -0.488844
+v 0.342950 -0.052407 -1.888369
+v 0.237367 -0.079489 -1.811573
+v 0.493796 -0.013716 -1.888369
+v 0.620157 0.018695 -1.811573
+v 0.161790 -0.098874 -1.642936
+v 0.169411 -0.128585 -0.260200
+v 0.700229 -0.065786 -0.260200
+v 0.154169 -0.069162 -0.260200
+v 0.649681 0.131288 -0.260200
+v 0.695734 0.038080 -0.260200
+v 0.161790 -0.098874 -0.260200
+v 0.052427 0.327499 -0.147904
+v 0.569809 0.442682 -0.164881
+v 0.048786 0.341695 -0.133247
+v 0.557734 0.489762 -0.116271
+v 0.584550 0.471551 -0.140576
+v 0.050607 0.334597 -0.140576
+v 0.456597 0.456424 -1.203159
+v 0.145694 0.391520 -1.203159
+v 0.456597 0.456424 -1.238526
+v 0.145694 0.391520 -1.238526
+v 0.190975 0.349151 -1.206875
+v 0.436550 0.400416 -1.206875
+v 0.190975 0.349151 -1.234810
+v 0.436550 0.400416 -1.234810
+v 0.232635 0.309816 -1.210284
+v 0.418277 0.348570 -1.210284
+v 0.232635 0.309816 -1.231401
+v 0.418277 0.348570 -1.231401
+v 0.310046 0.008015 -1.210284
+v 0.495688 0.046769 -1.210284
+v 0.310046 0.008015 -1.231401
+v 0.495688 0.046769 -1.231401
+v 0.301088 -0.048430 -1.207753
+v 0.531219 -0.000389 -1.207753
+v 0.301088 -0.048430 -1.233932
+v 0.531219 -0.000389 -1.233932
+v -0.405866 0.110604 -0.000000
+v -0.361917 -0.060740 -0.216145
+v -0.393719 0.063246 -0.133585
+v 0.037321 -0.011525 -0.000000
+v 0.037321 -0.011525 -0.244091
+v 0.037321 -0.011525 -0.182190
+v -0.295395 -0.096865 -0.000000
+v -0.295395 -0.096865 -0.165446
+v -0.295395 -0.096865 -0.133585
+v 0.039778 0.292533 -0.000000
+v -0.112082 0.362755 -0.106748
+v -0.073943 0.401560 -0.000000
+v -0.359435 0.114112 -0.000000
+v -0.315628 0.125348 -0.000000
+v -0.287870 -0.116624 -0.000000
+v -0.244063 -0.105388 -0.000000
+v -0.310619 -0.064857 -0.000000
+v -0.275705 -0.018950 -0.000000
+v -0.035649 -0.051651 -0.000000
+v -0.067291 0.034787 -0.000000
+v -0.705317 -0.403170 -0.000000
+v -0.709622 -0.402704 -0.036062
+v -0.657266 -0.390845 -0.000000
+v -0.661571 -0.390379 -0.036062
+v -0.674964 -0.521507 -0.000000
+v -0.679269 -0.521040 -0.016092
+v -0.626913 -0.509182 -0.000000
+v -0.631218 -0.508715 -0.016092
+v -0.007398 0.207375 -0.182190
+v -0.101360 0.365505 -0.106748
+v -0.063222 0.404310 -0.000000
+v -0.012411 0.226918 -0.197565
+v -0.106373 0.385048 -0.122122
+v -0.068234 0.423853 -0.000000
+v -0.032844 0.221677 -0.197565
+v -0.126805 0.379807 -0.122122
+v -0.088667 0.418612 -0.000000
+v 0.796302 0.123426 -0.384930
+v 0.779472 0.189042 -0.384930
+v 0.823953 0.097930 -0.314791
+v 0.794043 0.214538 -0.314791
+v 0.673151 0.129858 -0.353539
+v 0.681289 0.101381 -0.353539
+v 0.669989 0.140924 -0.321880
+v 0.684451 0.090316 -0.321880
+v 0.739131 0.352338 -0.327501
+v 0.724956 0.405865 -0.288475
+v 0.764280 0.289042 -0.286335
+v 0.739090 0.384167 -0.216981
+v 0.615716 0.344409 -0.297519
+v 0.622701 0.321142 -0.314462
+v 0.612348 0.334694 -0.265441
+v 0.624761 0.293344 -0.295551
+v 0.378312 -0.031985 -1.219513
+v 0.364674 -0.035939 -1.217513
+v 0.363217 -0.026764 -1.222155
+v 0.373447 -0.023798 -1.223656
+v 0.381774 0.479069 -0.203002
+v 0.370454 0.475787 -0.201341
+v 0.367838 0.484627 -0.205814
+v 0.378068 0.487592 -0.207315
+v 0.233513 0.428602 -1.223795
+v 0.222935 0.424528 -1.222455
+v 0.220388 0.433470 -1.218175
+v 0.230356 0.437309 -1.219438
+v 0.449009 0.004356 -0.315717
+v 0.438561 0.000332 -0.314394
+v 0.435949 0.009249 -0.310106
+v 0.445917 0.013088 -0.311368
+v 1.012174 0.220313 -0.063779
+v 1.049197 0.298034 -0.024887
+v 1.057175 0.206141 -0.078300
+v 1.094198 0.283863 -0.039409
+v 1.008112 0.598720 -0.470230
+v 0.933480 0.442046 -0.548630
+v 1.039982 0.588684 -0.480514
+v 0.965350 0.432009 -0.558914
+v 0.994706 0.604637 -0.537020
+v 0.941926 0.493836 -0.592465
+v 1.016141 0.597887 -0.543937
+v 0.963361 0.487086 -0.599382
+v 0.978193 0.580090 -0.581240
+v 0.960584 0.543123 -0.599739
+v 0.990514 0.576210 -0.585216
+v 0.972905 0.539243 -0.603715
+v -0.059195 0.089744 -0.000000
+v -0.087254 0.185105 -0.000000
+v -0.068426 0.121118 -0.078173
+v -0.078022 0.153731 -0.078173
+v -0.082638 0.169418 -0.039637
+v -0.063810 0.105431 -0.039637
+v -0.086797 0.183555 -0.099874
+v -0.091413 0.199242 -0.061338
+v -0.109873 0.261981 -0.069042
+v -0.102034 0.235338 -0.037722
+v -0.024402 0.099981 -0.000000
+v -0.052461 0.195342 -0.000000
+v -0.033633 0.131355 -0.078173
+v -0.043229 0.163968 -0.078173
+v -0.047845 0.179655 -0.039637
+v -0.029018 0.115668 -0.039637
+v -0.052005 0.193792 -0.099874
+v -0.056620 0.209479 -0.061338
+v -0.075080 0.272218 -0.069042
+v -0.067241 0.245575 -0.037722
+v 0.069203 0.262096 0.088377
+v 0.071695 0.252381 0.102419
+v 0.066711 0.271811 0.074335
+v 0.574104 0.425939 0.041610
+v 0.590632 0.361502 0.134946
+v 0.603147 0.399050 0.088377
+v 0.068484 0.264899 -0.090395
+v 0.070950 0.255286 -0.104178
+v 0.066018 0.274513 -0.076613
+v 0.573471 0.428407 -0.044497
+v 0.589826 0.364642 -0.136104
+v 0.602427 0.401854 -0.090395
+vt 0.7448 0.7820
+vt 0.7051 0.7691
+vt 0.7236 0.7438
+vt 0.7448 0.7507
+vt 0.6806 0.6942
+vt 0.7051 0.6606
+vt 0.7236 0.6860
+vt 0.7105 0.7038
+vt 0.6806 0.7356
+vt 0.7105 0.7259
+vt 0.7448 0.6479
+vt 0.7448 0.6792
+vt 0.2568 0.3652
+vt 0.2964 0.3648
+vt 0.2964 0.4510
+vt 0.2537 0.4514
+vt 0.0108 0.0069
+vt 0.0448 0.0066
+vt 0.0433 0.0929
+vt 0.0066 0.0932
+vt 0.0865 0.0066
+vt 0.0882 0.0929
+vt 0.1200 0.0069
+vt 0.1242 0.0932
+vt 0.6320 0.7072
+vt 0.5923 0.7077
+vt 0.5892 0.6212
+vt 0.6321 0.6207
+vt 0.0465 0.1667
+vt 0.0154 0.1670
+vt 0.0847 0.1667
+vt 0.1153 0.1670
+vt 0.5956 0.5479
+vt 0.6320 0.5474
+vt 0.2964 0.5094
+vt 0.2602 0.5254
+vt 0.0497 0.2463
+vt 0.0812 0.2463
+vt 0.0790 0.2959
+vt 0.0516 0.2959
+vt 0.1065 0.2465
+vt 0.1010 0.2961
+vt 0.6319 0.4686
+vt 0.6052 0.4690
+vt 0.6112 0.4198
+vt 0.6318 0.4194
+vt 0.7498 0.7882
+vt 0.7763 0.7878
+vt 0.7764 0.8376
+vt 0.7558 0.8378
+vt 0.0240 0.2465
+vt 0.0293 0.2961
+vt 0.0538 0.3575
+vt 0.0355 0.3575
+vt 0.0763 0.3575
+vt 0.9241 0.5520
+vt 0.9414 0.5333
+vt 0.9270 0.5511
+vt 0.6255 0.3589
+vt 0.6318 0.3586
+vt 0.7236 0.3945
+vt 0.7367 0.4072
+vt 0.7323 0.4117
+vt 0.7193 0.3990
+vt 0.7528 0.4229
+vt 0.7483 0.4274
+vt 0.7656 0.4357
+vt 0.7613 0.4401
+vt 0.7763 0.8644
+vt 0.7699 0.8684
+vt 0.3657 0.8672
+vt 0.3974 0.8411
+vt 0.4272 0.8737
+vt 0.7143 0.3941
+vt 0.7622 0.4241
+vt 0.4271 0.8842
+vt 0.3655 0.8861
+vt 0.2964 0.5250
+vt 0.7659 0.7439
+vt 0.7844 0.7692
+vt 0.8090 0.6942
+vt 0.7790 0.7039
+vt 0.7659 0.6860
+vt 0.7844 0.6607
+vt 0.8090 0.7357
+vt 0.7790 0.7260
+vt 0.3361 0.3652
+vt 0.3392 0.4514
+vt 0.2435 0.0067
+vt 0.2478 0.0930
+vt 0.2116 0.0929
+vt 0.2099 0.0066
+vt 0.1667 0.0929
+vt 0.1682 0.0066
+vt 0.1301 0.0930
+vt 0.1343 0.0067
+vt 0.6747 0.6212
+vt 0.6716 0.7077
+vt 0.2389 0.1668
+vt 0.2081 0.1667
+vt 0.1700 0.1667
+vt 0.1390 0.1668
+vt 0.6682 0.5479
+vt 0.3327 0.5254
+vt 0.2047 0.2461
+vt 0.2026 0.2957
+vt 0.1752 0.2957
+vt 0.1732 0.2461
+vt 0.1530 0.2957
+vt 0.1476 0.2462
+vt 0.6523 0.4198
+vt 0.6584 0.4690
+vt 0.8030 0.7882
+vt 0.7970 0.8378
+vt 0.2301 0.2462
+vt 0.2247 0.2957
+vt 0.2182 0.3570
+vt 0.2000 0.3570
+vt 0.1775 0.3570
+vt 0.7178 0.9252
+vt 0.7207 0.9261
+vt 0.7350 0.9439
+vt 0.6380 0.3589
+vt 0.7149 0.4034
+vt 0.7278 0.4162
+vt 0.7438 0.4320
+vt 0.7569 0.4446
+vt 0.7827 0.8684
+vt 0.8251 0.0257
+vt 0.8865 0.0195
+vt 0.8567 0.0521
+vt 0.7452 0.4413
+vt 0.8250 0.0066
+vt 0.8865 0.0088
+vt 0.9215 0.6311
+vt 0.9215 0.6728
+vt 0.8958 0.6711
+vt 0.8958 0.6329
+vt 0.9218 0.5977
+vt 0.8961 0.6022
+vt 0.4352 0.9042
+vt 0.4347 0.8645
+vt 0.4606 0.8645
+vt 0.4611 0.9010
+vt 0.8404 0.9057
+vt 0.8408 0.9452
+vt 0.8152 0.9420
+vt 0.8148 0.9057
+vt 0.9218 0.7067
+vt 0.8961 0.7023
+vt 0.1728 0.6926
+vt 0.2110 0.6926
+vt 0.2091 0.7008
+vt 0.1762 0.7008
+vt 0.1419 0.7151
+vt 0.1496 0.7202
+vt 0.4685 0.8646
+vt 0.4689 0.8960
+vt 0.1301 0.7515
+vt 0.1394 0.7515
+vt 0.2418 0.7151
+vt 0.2356 0.7202
+vt 0.1878 0.7366
+vt 0.1799 0.7423
+vt 0.2458 0.7515
+vt 0.2082 0.7515
+vt 0.2052 0.7423
+vt 0.1769 0.7515
+vt 0.1974 0.7366
+vt 0.8090 0.5539
+vt 0.8090 0.5955
+vt 0.7974 0.5918
+vt 0.7974 0.5576
+vt 0.7844 0.6291
+vt 0.7773 0.6194
+vt 0.7448 0.5074
+vt 0.7844 0.5203
+vt 0.7773 0.5300
+vt 0.7448 0.5195
+vt 0.7448 0.6420
+vt 0.7448 0.6299
+vt 0.0271 0.9440
+vt 0.0361 0.9440
+vt 0.0361 0.9627
+vt 0.0271 0.9627
+vt 0.9540 0.7637
+vt 0.9447 0.7570
+vt 0.9460 0.7459
+vt 0.9550 0.7454
+vt 0.8924 0.1279
+vt 0.9111 0.1279
+vt 0.9111 0.1391
+vt 0.8924 0.1391
+vt 0.6806 0.9335
+vt 0.6989 0.9335
+vt 0.6989 0.9441
+vt 0.6806 0.9441
+vt 0.8712 0.4827
+vt 0.8712 0.4641
+vt 0.9010 0.4641
+vt 0.9010 0.4827
+vt 0.8924 0.0910
+vt 0.9111 0.0910
+vt 0.9550 0.7934
+vt 0.9460 0.7939
+vt 0.6989 0.9149
+vt 0.6806 0.9149
+vt 0.4960 0.9319
+vt 0.4773 0.9319
+vt 0.4773 0.8891
+vt 0.4960 0.8891
+vt 0.9878 0.4631
+vt 0.9878 0.4021
+vt 0.9916 0.4020
+vt 0.9916 0.4630
+vt 0.9897 0.7238
+vt 0.9897 0.6654
+vt 0.9934 0.6668
+vt 0.9934 0.7252
+vt 0.6605 0.9845
+vt 0.6605 0.9232
+vt 0.6643 0.9233
+vt 0.6643 0.9847
+vt 0.6565 0.9847
+vt 0.6565 0.9234
+vt 0.9858 0.7913
+vt 0.9858 0.7329
+vt 0.9898 0.7326
+vt 0.9898 0.7909
+vt 0.9838 0.4630
+vt 0.9838 0.4020
+vt 0.9858 0.7235
+vt 0.9858 0.6650
+vt 0.9934 0.7311
+vt 0.9934 0.7895
+vt 0.6071 0.9449
+vt 0.5961 0.9413
+vt 0.6005 0.9352
+vt 0.6071 0.9374
+vt 0.6250 0.9204
+vt 0.6250 0.9320
+vt 0.6178 0.9296
+vt 0.6178 0.9226
+vt 0.5961 0.9110
+vt 0.6071 0.9074
+vt 0.6071 0.9148
+vt 0.6005 0.9170
+vt 0.5892 0.9320
+vt 0.5964 0.9296
+vt 0.6181 0.9413
+vt 0.6137 0.9352
+vt 0.6181 0.9110
+vt 0.6137 0.9170
+vt 0.5892 0.9204
+vt 0.5964 0.9226
+vt 0.9598 0.2821
+vt 0.9598 0.2712
+vt 0.9638 0.2693
+vt 0.9637 0.2819
+vt 0.6348 0.9195
+vt 0.6348 0.9311
+vt 0.6309 0.9322
+vt 0.6309 0.9187
+vt 0.9469 0.0556
+vt 0.9469 0.0669
+vt 0.9430 0.0672
+vt 0.9430 0.0540
+vt 0.9637 0.2305
+vt 0.9638 0.2401
+vt 0.9598 0.2392
+vt 0.9598 0.2280
+vt 0.6348 0.9402
+vt 0.6309 0.9428
+vt 0.9469 0.0775
+vt 0.9430 0.0796
+vt 0.9638 0.2516
+vt 0.9598 0.2527
+vt 0.9598 0.2932
+vt 0.9638 0.2950
+vt 0.6348 0.9099
+vt 0.6309 0.9074
+vt 0.9637 0.2608
+vt 0.9598 0.2634
+vt 0.6071 0.9261
+vt 0.8674 0.7543
+vt 0.8674 0.7678
+vt 0.8618 0.7696
+vt 0.8618 0.7525
+vt 0.9091 0.7543
+vt 0.9011 0.7433
+vt 0.9046 0.7386
+vt 0.9147 0.7525
+vt 0.8753 0.7787
+vt 0.8719 0.7835
+vt 0.8882 0.7392
+vt 0.8882 0.7333
+vt 0.9011 0.7787
+vt 0.9091 0.7678
+vt 0.9147 0.7696
+vt 0.9046 0.7835
+vt 0.8882 0.7829
+vt 0.8882 0.7888
+vt 0.8753 0.7433
+vt 0.8719 0.7386
+vt 0.9075 0.7346
+vt 0.9194 0.7510
+vt 0.9075 0.7876
+vt 0.8882 0.7939
+vt 0.8570 0.7712
+vt 0.8570 0.7510
+vt 0.8882 0.7284
+vt 0.9194 0.7712
+vt 0.8689 0.7876
+vt 0.8689 0.7346
+vt 0.8760 0.9757
+vt 0.8759 0.9561
+vt 0.8838 0.9561
+vt 0.8838 0.9757
+vt 0.9539 0.2280
+vt 0.9539 0.2449
+vt 0.9460 0.2449
+vt 0.9461 0.2280
+vt 0.3045 0.9249
+vt 0.3046 0.9089
+vt 0.3124 0.9089
+vt 0.3124 0.9249
+vt 0.8760 0.9372
+vt 0.8838 0.9372
+vt 0.9539 0.2651
+vt 0.9460 0.2651
+vt 0.8071 0.9425
+vt 0.8071 0.9623
+vt 0.7992 0.9623
+vt 0.7993 0.9425
+vt 0.3046 0.9619
+vt 0.3045 0.9451
+vt 0.3124 0.9451
+vt 0.3124 0.9619
+vt 0.9539 0.2810
+vt 0.9461 0.2810
+vt 0.8071 0.9809
+vt 0.7993 0.9809
+vt 0.8050 0.9247
+vt 0.7886 0.9367
+vt 0.7873 0.9327
+vt 0.8017 0.9223
+vt 0.7886 0.8743
+vt 0.8050 0.8862
+vt 0.8017 0.8887
+vt 0.7873 0.8783
+vt 0.8882 0.9398
+vt 0.8881 0.9563
+vt 0.9417 0.2637
+vt 0.9417 0.2460
+vt 0.7949 0.9619
+vt 0.7950 0.9446
+vt 0.7519 0.8862
+vt 0.7683 0.8743
+vt 0.7696 0.8783
+vt 0.7552 0.8887
+vt 0.7683 0.9367
+vt 0.7519 0.9247
+vt 0.7552 0.9223
+vt 0.7696 0.9327
+vt 0.7950 0.9781
+vt 0.3167 0.9263
+vt 0.3167 0.9440
+vt 0.8882 0.9734
+vt 0.7498 0.9055
+vt 0.7614 0.8931
+vt 0.7573 0.9055
+vt 0.7719 0.9256
+vt 0.7850 0.9256
+vt 0.8071 0.9055
+vt 0.7996 0.9055
+vt 0.7956 0.8931
+vt 0.7719 0.8854
+vt 0.7614 0.9179
+vt 0.7956 0.9179
+vt 0.7850 0.8854
+vt 0.9858 0.9401
+vt 0.9858 0.9271
+vt 0.9898 0.9283
+vt 0.9898 0.9392
+vt 0.7828 0.9673
+vt 0.7827 0.9547
+vt 0.7867 0.9549
+vt 0.7868 0.9654
+vt 0.9898 0.8974
+vt 0.9898 0.9082
+vt 0.9858 0.9091
+vt 0.9858 0.9000
+vt 0.7641 0.9159
+vt 0.7730 0.9223
+vt 0.7828 0.9425
+vt 0.7868 0.9447
+vt 0.9898 0.9212
+vt 0.9858 0.9201
+vt 0.7421 0.9498
+vt 0.7421 0.9625
+vt 0.7381 0.9622
+vt 0.7382 0.9515
+vt 0.9858 0.9509
+vt 0.9898 0.9482
+vt 0.7928 0.8951
+vt 0.7839 0.8887
+vt 0.7421 0.9745
+vt 0.7382 0.9722
+vt 0.7962 0.9055
+vt 0.7784 0.9055
+vt 0.7641 0.8951
+vt 0.7730 0.8887
+vt 0.7928 0.9159
+vt 0.7607 0.9055
+vt 0.7839 0.9223
+vt 0.7367 0.3211
+vt 0.7454 0.3212
+vt 0.7454 0.3369
+vt 0.7367 0.3368
+vt 0.7570 0.3212
+vt 0.7672 0.3211
+vt 0.7672 0.3368
+vt 0.7570 0.3369
+vt 0.4454 0.9100
+vt 0.4568 0.9102
+vt 0.4568 0.9259
+vt 0.4454 0.9257
+vt 0.4347 0.9103
+vt 0.4347 0.9260
+vt 0.7466 0.3443
+vt 0.7559 0.3443
+vt 0.7547 0.3517
+vt 0.7502 0.3517
+vt 0.4375 0.9336
+vt 0.4459 0.9334
+vt 0.4465 0.9413
+vt 0.4423 0.9413
+vt 0.9541 0.9394
+vt 0.9459 0.9394
+vt 0.9465 0.9320
+vt 0.9510 0.9318
+vt 0.7641 0.3442
+vt 0.7586 0.3516
+vt 0.4547 0.9335
+vt 0.4509 0.9413
+vt 0.7403 0.3442
+vt 0.9427 0.9295
+vt 0.9460 0.9182
+vt 0.9412 0.9253
+vt 0.9425 0.9210
+vt 0.3514 0.7516
+vt 0.3514 0.7540
+vt 0.3497 0.7540
+vt 0.3497 0.7517
+vt 0.3514 0.7983
+vt 0.3514 0.8008
+vt 0.3497 0.8007
+vt 0.3497 0.7983
+vt 0.3497 0.8096
+vt 0.3497 0.8067
+vt 0.3514 0.8067
+vt 0.3514 0.8096
+vt 0.3514 0.7570
+vt 0.3497 0.7569
+vt 0.6678 0.7987
+vt 0.6677 0.8016
+vt 0.6660 0.8016
+vt 0.6660 0.7988
+vt 0.3514 0.7929
+vt 0.3514 0.7953
+vt 0.3497 0.7953
+vt 0.3497 0.7930
+vt 0.3514 0.7595
+vt 0.3497 0.7594
+vt 0.6678 0.8045
+vt 0.6660 0.8044
+vt 0.3497 0.8124
+vt 0.3514 0.8123
+vt 0.4258 0.9097
+vt 0.4258 0.9069
+vt 0.4284 0.9057
+vt 0.4284 0.9096
+vt 0.2450 0.6691
+vt 0.2450 0.6721
+vt 0.2425 0.6728
+vt 0.2425 0.6686
+vt 0.7435 0.9033
+vt 0.7435 0.9062
+vt 0.7409 0.9064
+vt 0.7409 0.9024
+vt 0.0687 0.8053
+vt 0.0687 0.8028
+vt 0.0712 0.8033
+vt 0.0712 0.8067
+vt 0.2450 0.6745
+vt 0.2425 0.6760
+vt 0.7435 0.9090
+vt 0.7409 0.9102
+vt 0.0687 0.7998
+vt 0.0712 0.7992
+vt 0.4258 0.9126
+vt 0.4284 0.9136
+vt 0.2450 0.6666
+vt 0.2425 0.6652
+vt 0.0687 0.7974
+vt 0.0712 0.7959
+vt 0.4392 0.9788
+vt 0.4420 0.9797
+vt 0.4392 0.9835
+vt 0.4347 0.9850
+vt 0.4347 0.9821
+vt 0.4420 0.9874
+vt 0.4392 0.9883
+vt 0.4437 0.9821
+vt 0.4364 0.9797
+vt 0.4364 0.9874
+vt 0.4437 0.9850
+vt 0.8702 0.1138
+vt 0.8662 0.1151
+vt 0.8643 0.1124
+vt 0.8702 0.1105
+vt 0.8741 0.1259
+vt 0.8766 0.1226
+vt 0.8797 0.1236
+vt 0.8761 0.1286
+vt 0.8638 0.1184
+vt 0.8607 0.1174
+vt 0.8766 0.1184
+vt 0.8797 0.1174
+vt 0.8662 0.1259
+vt 0.8702 0.1272
+vt 0.8702 0.1305
+vt 0.8643 0.1286
+vt 0.8638 0.1226
+vt 0.8607 0.1236
+vt 0.8741 0.1151
+vt 0.8761 0.1124
+vt 0.8832 0.1248
+vt 0.8782 0.1316
+vt 0.8621 0.1316
+vt 0.8571 0.1248
+vt 0.8621 0.1094
+vt 0.8701 0.1068
+vt 0.8832 0.1163
+vt 0.8701 0.1342
+vt 0.8571 0.1163
+vt 0.8782 0.1094
+vt 0.0066 0.9925
+vt 0.0066 0.9844
+vt 0.0102 0.9844
+vt 0.0102 0.9926
+vt 0.3655 0.9888
+vt 0.3655 0.9818
+vt 0.3691 0.9818
+vt 0.3691 0.9889
+vt 0.3805 0.9734
+vt 0.3805 0.9667
+vt 0.3841 0.9666
+vt 0.3841 0.9734
+vt 0.0066 0.9764
+vt 0.0102 0.9764
+vt 0.3655 0.9733
+vt 0.3691 0.9733
+vt 0.3499 0.9773
+vt 0.3499 0.9856
+vt 0.3463 0.9856
+vt 0.3463 0.9773
+vt 0.3805 0.9889
+vt 0.3805 0.9818
+vt 0.3841 0.9819
+vt 0.3841 0.9889
+vt 0.3655 0.9667
+vt 0.3691 0.9666
+vt 0.3499 0.9934
+vt 0.3463 0.9934
+vt 0.9117 0.8916
+vt 0.9299 0.8934
+vt 0.9299 0.9362
+vt 0.9117 0.9344
+vt 0.8986 0.9344
+vt 0.8986 0.8916
+vt 0.7119 0.9630
+vt 0.7119 0.9813
+vt 0.7013 0.9813
+vt 0.7013 0.9630
+vt 0.7604 0.9425
+vt 0.7604 0.9853
+vt 0.7498 0.9853
+vt 0.7498 0.9425
+vt 0.5061 0.9378
+vt 0.5061 0.9806
+vt 0.4955 0.9806
+vt 0.4955 0.9378
+vt 0.5647 0.9336
+vt 0.5647 0.9764
+vt 0.5516 0.9764
+vt 0.5516 0.9336
+vt 0.7013 0.9500
+vt 0.7119 0.9500
+vt 0.7119 0.9335
+vt 0.7119 0.9441
+vt 0.5773 0.8939
+vt 0.5516 0.8922
+vt 0.5516 0.8540
+vt 0.5773 0.8523
+vt 0.5774 0.9277
+vt 0.5517 0.9233
+vt 0.4352 0.8250
+vt 0.4611 0.8282
+vt 0.8152 0.8692
+vt 0.8408 0.8660
+vt 0.5517 0.8232
+vt 0.5774 0.8187
+vt 0.1727 0.8103
+vt 0.1761 0.8023
+vt 0.2090 0.8023
+vt 0.2109 0.8103
+vt 0.1419 0.7878
+vt 0.1495 0.7829
+vt 0.4689 0.8333
+vt 0.2356 0.7829
+vt 0.2418 0.7878
+vt 0.1799 0.7607
+vt 0.1877 0.7664
+vt 0.2052 0.7607
+vt 0.1974 0.7664
+vt 0.6806 0.5539
+vt 0.6921 0.5577
+vt 0.6921 0.5918
+vt 0.6806 0.5955
+vt 0.7122 0.6194
+vt 0.7051 0.6292
+vt 0.7122 0.5301
+vt 0.7051 0.5203
+vt 0.0271 0.9813
+vt 0.0361 0.9813
+vt 0.9450 0.1212
+vt 0.9459 0.1395
+vt 0.9369 0.1391
+vt 0.9356 0.1279
+vt 0.9297 0.1279
+vt 0.9297 0.1391
+vt 0.6806 0.8963
+vt 0.6806 0.8857
+vt 0.6989 0.8857
+vt 0.6989 0.8963
+vt 0.9010 0.5014
+vt 0.8712 0.5014
+vt 0.9297 0.0910
+vt 0.9459 0.0915
+vt 0.9369 0.0910
+vt 0.5147 0.8891
+vt 0.5147 0.9319
+vt 0.4666 0.9721
+vt 0.4627 0.9720
+vt 0.4627 0.9101
+vt 0.4666 0.9101
+vt 0.9876 0.2662
+vt 0.9839 0.2676
+vt 0.9839 0.2079
+vt 0.9876 0.2065
+vt 0.3460 0.9712
+vt 0.3421 0.9714
+vt 0.3421 0.9090
+vt 0.3460 0.9089
+vt 0.3499 0.9714
+vt 0.3499 0.9090
+vt 0.9934 0.6592
+vt 0.9895 0.6588
+vt 0.9895 0.5991
+vt 0.9934 0.5995
+vt 0.4705 0.9720
+vt 0.4705 0.9100
+vt 0.9915 0.2658
+vt 0.9915 0.2061
+vt 0.9858 0.6574
+vt 0.9858 0.5977
+vt 0.1852 0.9065
+vt 0.1851 0.9140
+vt 0.1785 0.9161
+vt 0.1741 0.9101
+vt 0.2031 0.9311
+vt 0.1959 0.9287
+vt 0.1959 0.9218
+vt 0.2031 0.9195
+vt 0.1741 0.9404
+vt 0.1785 0.9344
+vt 0.1851 0.9365
+vt 0.1852 0.9440
+vt 0.1744 0.9218
+vt 0.1673 0.9195
+vt 0.1918 0.9161
+vt 0.1962 0.9101
+vt 0.1918 0.9344
+vt 0.1962 0.9404
+vt 0.1744 0.9287
+vt 0.1673 0.9311
+vt 0.8510 0.9264
+vt 0.8471 0.9263
+vt 0.8471 0.9136
+vt 0.8510 0.9156
+vt 0.6742 0.9465
+vt 0.6703 0.9473
+vt 0.6703 0.9338
+vt 0.6742 0.9349
+vt 0.9654 0.5349
+vt 0.9693 0.5333
+vt 0.9693 0.5465
+vt 0.9654 0.5462
+vt 0.1225 0.9407
+vt 0.1186 0.9431
+vt 0.1186 0.9319
+vt 0.1225 0.9311
+vt 0.6702 0.9232
+vt 0.6741 0.9258
+vt 0.9693 0.5589
+vt 0.9654 0.5568
+vt 0.1186 0.9184
+vt 0.1225 0.9195
+vt 0.8510 0.9376
+vt 0.8471 0.9393
+vt 0.6741 0.9561
+vt 0.6702 0.9586
+vt 0.1186 0.9077
+vt 0.1225 0.9103
+vt 0.1851 0.9253
+vt 0.3305 0.8880
+vt 0.3340 0.8927
+vt 0.3177 0.8980
+vt 0.3177 0.8921
+vt 0.3177 0.8484
+vt 0.3177 0.8425
+vt 0.3340 0.8478
+vt 0.3305 0.8525
+vt 0.3013 0.8927
+vt 0.3048 0.8880
+vt 0.3441 0.8617
+vt 0.3385 0.8635
+vt 0.2968 0.8635
+vt 0.2912 0.8617
+vt 0.3013 0.8478
+vt 0.3048 0.8526
+vt 0.2912 0.8788
+vt 0.2968 0.8770
+vt 0.3441 0.8788
+vt 0.3385 0.8770
+vt 0.3177 0.8374
+vt 0.3370 0.8437
+vt 0.2864 0.8803
+vt 0.2864 0.8601
+vt 0.3370 0.8967
+vt 0.3177 0.9030
+vt 0.3489 0.8601
+vt 0.2984 0.8437
+vt 0.2984 0.8967
+vt 0.3489 0.8803
+vt 0.9273 0.9806
+vt 0.9195 0.9806
+vt 0.9194 0.9610
+vt 0.9273 0.9610
+vt 0.2986 0.9619
+vt 0.2907 0.9619
+vt 0.2908 0.9450
+vt 0.2986 0.9450
+vt 0.9594 0.5493
+vt 0.9516 0.5493
+vt 0.9516 0.5333
+vt 0.9595 0.5333
+vt 0.9195 0.9421
+vt 0.9273 0.9421
+vt 0.2908 0.9247
+vt 0.2986 0.9247
+vt 0.4774 0.9378
+vt 0.4853 0.9378
+vt 0.4852 0.9576
+vt 0.4773 0.9575
+vt 0.9595 0.5863
+vt 0.9516 0.5863
+vt 0.9516 0.5696
+vt 0.9594 0.5696
+vt 0.2907 0.9089
+vt 0.2986 0.9089
+vt 0.4853 0.9761
+vt 0.4774 0.9761
+vt 0.2253 0.9338
+vt 0.2286 0.9313
+vt 0.2430 0.9417
+vt 0.2418 0.9457
+vt 0.2418 0.8834
+vt 0.2430 0.8873
+vt 0.2286 0.8977
+vt 0.2253 0.8953
+vt 0.9151 0.9612
+vt 0.9152 0.9447
+vt 0.2864 0.9439
+vt 0.2864 0.9262
+vt 0.4896 0.9399
+vt 0.4896 0.9572
+vt 0.2785 0.8953
+vt 0.2751 0.8977
+vt 0.2607 0.8873
+vt 0.2621 0.8834
+vt 0.2620 0.9457
+vt 0.2607 0.9417
+vt 0.2751 0.9313
+vt 0.2785 0.9338
+vt 0.4896 0.9734
+vt 0.9473 0.5684
+vt 0.9473 0.5507
+vt 0.9152 0.9783
+vt 0.2805 0.9145
+vt 0.2730 0.9145
+vt 0.2689 0.9021
+vt 0.2453 0.9346
+vt 0.2584 0.9346
+vt 0.2348 0.9021
+vt 0.2307 0.9145
+vt 0.2232 0.9145
+vt 0.2584 0.8944
+vt 0.2689 0.9269
+vt 0.2348 0.9269
+vt 0.2453 0.8944
+vt 0.9898 0.9698
+vt 0.9858 0.9689
+vt 0.9858 0.9580
+vt 0.9898 0.9568
+vt 0.1226 0.9738
+vt 0.1186 0.9719
+vt 0.1186 0.9614
+vt 0.1225 0.9612
+vt 0.9858 0.8676
+vt 0.9899 0.8702
+vt 0.9898 0.8794
+vt 0.9858 0.8784
+vt 0.2574 0.9314
+vt 0.2662 0.9249
+vt 0.1186 0.9511
+vt 0.1226 0.9490
+vt 0.9899 0.8903
+vt 0.9858 0.8915
+vt 0.9654 0.5648
+vt 0.9694 0.5665
+vt 0.9694 0.5772
+vt 0.9654 0.5775
+vt 0.9898 0.9806
+vt 0.9858 0.9779
+vt 0.2464 0.8977
+vt 0.2376 0.9041
+vt 0.9694 0.5872
+vt 0.9654 0.5895
+vt 0.2520 0.9145
+vt 0.2342 0.9145
+vt 0.2662 0.9041
+vt 0.2574 0.8977
+vt 0.2376 0.9249
+vt 0.2696 0.9145
+vt 0.2464 0.9314
+vt 0.3655 0.9607
+vt 0.3655 0.9450
+vt 0.3769 0.9449
+vt 0.3769 0.9606
+vt 0.7308 0.3211
+vt 0.7308 0.3368
+vt 0.7207 0.3368
+vt 0.7207 0.3211
+vt 0.7090 0.3211
+vt 0.7091 0.3368
+vt 0.7003 0.3368
+vt 0.7003 0.3211
+vt 0.3877 0.9451
+vt 0.3877 0.9608
+vt 0.9603 0.9341
+vt 0.9545 0.9290
+vt 0.9558 0.9247
+vt 0.9631 0.9252
+vt 0.7198 0.3443
+vt 0.7183 0.3517
+vt 0.7138 0.3517
+vt 0.7107 0.3443
+vt 0.9542 0.9205
+vt 0.9606 0.9165
+vt 0.9531 0.9114
+vt 0.9505 0.9180
+vt 0.9440 0.9117
+vt 0.7039 0.3443
+vt 0.7274 0.3443
+vt 0.3676 0.9376
+vt 0.3757 0.9376
+vt 0.3843 0.9377
+vt 0.3497 0.7654
+vt 0.3514 0.7655
+vt 0.3514 0.7678
+vt 0.3497 0.7677
+vt 0.3497 0.7845
+vt 0.3514 0.7845
+vt 0.3514 0.7869
+vt 0.3497 0.7870
+vt 0.3514 0.8212
+vt 0.3497 0.8212
+vt 0.3497 0.8183
+vt 0.3514 0.8183
+vt 0.3514 0.7707
+vt 0.3497 0.7707
+vt 0.6584 0.7987
+vt 0.6601 0.7988
+vt 0.6601 0.8016
+vt 0.6584 0.8016
+vt 0.3497 0.7791
+vt 0.3514 0.7793
+vt 0.3514 0.7816
+vt 0.3497 0.7815
+vt 0.3514 0.7731
+vt 0.3497 0.7733
+vt 0.6601 0.8044
+vt 0.6584 0.8045
+vt 0.3514 0.8240
+vt 0.3497 0.8239
+vt 0.4284 0.8959
+vt 0.4258 0.8959
+vt 0.4258 0.8920
+vt 0.4284 0.8931
+vt 0.7409 0.8896
+vt 0.7434 0.8891
+vt 0.7434 0.8933
+vt 0.7409 0.8926
+vt 0.7409 0.9170
+vt 0.7434 0.9161
+vt 0.7434 0.9201
+vt 0.7409 0.9199
+vt 0.0687 0.7806
+vt 0.0712 0.7792
+vt 0.0712 0.7826
+vt 0.0687 0.7831
+vt 0.7435 0.8965
+vt 0.7409 0.8950
+vt 0.7434 0.9239
+vt 0.7409 0.9227
+vt 0.0712 0.7867
+vt 0.0687 0.7861
+vt 0.4284 0.8989
+vt 0.4258 0.8998
+vt 0.7409 0.8871
+vt 0.7435 0.8857
+vt 0.0712 0.7900
+vt 0.0687 0.7885
+vt 0.9003 0.7126
+vt 0.9003 0.7173
+vt 0.8975 0.7135
+vt 0.9048 0.7188
+vt 0.9048 0.7159
+vt 0.8975 0.7212
+vt 0.9003 0.7221
+vt 0.8958 0.7159
+vt 0.9031 0.7135
+vt 0.9031 0.7212
+vt 0.8958 0.7188
+vt 0.8445 0.1185
+vt 0.8476 0.1174
+vt 0.8476 0.1236
+vt 0.8445 0.1226
+vt 0.8317 0.1185
+vt 0.8286 0.1174
+vt 0.8323 0.1125
+vt 0.8342 0.1151
+vt 0.8440 0.1286
+vt 0.8421 0.1259
+vt 0.8381 0.1105
+vt 0.8381 0.1138
+vt 0.8342 0.1259
+vt 0.8323 0.1286
+vt 0.8286 0.1236
+vt 0.8317 0.1226
+vt 0.8381 0.1305
+vt 0.8381 0.1272
+vt 0.8440 0.1125
+vt 0.8421 0.1151
+vt 0.8250 0.1163
+vt 0.8300 0.1094
+vt 0.8381 0.1342
+vt 0.8300 0.1316
+vt 0.8512 0.1163
+vt 0.8512 0.1248
+vt 0.8381 0.1068
+vt 0.8250 0.1248
+vt 0.8462 0.1316
+vt 0.8462 0.1094
+vt 0.0138 0.9925
+vt 0.0138 0.9844
+vt 0.3727 0.9888
+vt 0.3727 0.9818
+vt 0.3877 0.9734
+vt 0.3877 0.9667
+vt 0.0138 0.9764
+vt 0.3727 0.9733
+vt 0.3428 0.9773
+vt 0.3427 0.9856
+vt 0.3877 0.9889
+vt 0.3877 0.9818
+vt 0.3727 0.9667
+vt 0.3428 0.9934
+vt 0.1432 0.9511
+vt 0.1432 0.9083
+vt 0.1614 0.9065
+vt 0.1614 0.9493
+vt 0.1301 0.9511
+vt 0.1301 0.9083
+vt 0.6806 0.9630
+vt 0.6912 0.9630
+vt 0.6912 0.9813
+vt 0.6806 0.9813
+vt 0.7768 0.9853
+vt 0.7663 0.9853
+vt 0.7663 0.9425
+vt 0.7768 0.9425
+vt 0.8986 0.9421
+vt 0.9092 0.9421
+vt 0.9092 0.9849
+vt 0.8986 0.9849
+vt 0.8700 0.9800
+vt 0.8570 0.9800
+vt 0.8570 0.9372
+vt 0.8700 0.9372
+vt 0.6806 0.9500
+vt 0.6912 0.9500
+vt 0.7119 0.8857
+vt 0.7119 0.8963
+vt 0.7995 0.4776
+vt 0.8017 0.4710
+vt 0.8238 0.4927
+vt 0.8229 0.5005
+vt 0.7796 0.4494
+vt 0.7761 0.4546
+vt 0.4198 0.8968
+vt 0.4199 0.9107
+vt 0.4135 0.9076
+vt 0.4135 0.8920
+vt 0.4199 0.9482
+vt 0.4135 0.9452
+vt 0.4272 0.7923
+vt 0.4272 0.8232
+vt 0.4164 0.8052
+vt 0.4272 0.8542
+vt 0.3595 0.5092
+vt 0.3532 0.5092
+vt 0.3532 0.4911
+vt 0.3596 0.4911
+vt 0.3532 0.4688
+vt 0.3596 0.4688
+vt 0.3532 0.4507
+vt 0.3595 0.4507
+vt 0.3532 0.3974
+vt 0.3532 0.3648
+vt 0.3595 0.3696
+vt 0.3595 0.4004
+vt 0.3532 0.4301
+vt 0.3595 0.4312
+vt 0.9497 0.7282
+vt 0.9496 0.7094
+vt 0.9528 0.7094
+vt 0.9529 0.7282
+vt 0.9496 0.6869
+vt 0.9528 0.6869
+vt 0.9496 0.6687
+vt 0.9528 0.6687
+vt 0.9528 0.6182
+vt 0.9497 0.6182
+vt 0.9497 0.5977
+vt 0.9529 0.5977
+vt 0.9496 0.6490
+vt 0.9528 0.6490
+vt 0.3435 0.5971
+vt 0.3435 0.6166
+vt 0.2907 0.6170
+vt 0.2864 0.5971
+vt 0.3435 0.5383
+vt 0.3435 0.5565
+vt 0.2880 0.5564
+vt 0.3018 0.5382
+vt 0.3435 0.5789
+vt 0.2864 0.5789
+vt 0.2681 0.9517
+vt 0.2743 0.9516
+vt 0.2743 0.9750
+vt 0.2712 0.9750
+vt 0.3435 0.6476
+vt 0.3039 0.6485
+vt 0.3435 0.6785
+vt 0.3204 0.6698
+vt 0.3532 0.5161
+vt 0.9870 0.4689
+vt 0.9902 0.4899
+vt 0.9870 0.4899
+vt 0.3434 0.5313
+vt 0.3231 0.5313
+vt 0.0622 0.9863
+vt 0.0646 0.9664
+vt 0.0677 0.9664
+vt 0.0698 0.9863
+vt 0.7949 0.3800
+vt 0.8160 0.3586
+vt 0.8214 0.3639
+vt 0.8003 0.3853
+vt 0.8757 0.5708
+vt 0.9183 0.5618
+vt 0.9183 0.5919
+vt 0.8983 0.5919
+vt 0.8570 0.5286
+vt 0.9183 0.5074
+vt 0.9183 0.5318
+vt 0.8623 0.5497
+vt 0.7738 0.4014
+vt 0.7793 0.4067
+vt 0.0644 0.9440
+vt 0.0674 0.9440
+vt 0.9310 0.5720
+vt 0.9338 0.5711
+vt 0.9385 0.5919
+vt 0.9414 0.5910
+vt 0.9857 0.0484
+vt 0.9787 0.0484
+vt 0.9787 0.0183
+vt 0.9857 0.0183
+vt 0.9857 0.1029
+vt 0.9787 0.1028
+vt 0.9787 0.0784
+vt 0.9857 0.0784
+vt 0.9838 0.3416
+vt 0.9866 0.3111
+vt 0.9892 0.3108
+vt 0.9864 0.3413
+vt 0.9866 0.3961
+vt 0.9843 0.3718
+vt 0.9869 0.3715
+vt 0.9892 0.3958
+vt 0.9358 0.2825
+vt 0.9358 0.3126
+vt 0.9290 0.3126
+vt 0.9157 0.2825
+vt 0.9335 0.4389
+vt 0.9335 0.4689
+vt 0.9090 0.4690
+vt 0.9157 0.4389
+vt 0.9335 0.4088
+vt 0.9289 0.4089
+vt 0.9358 0.2281
+vt 0.9358 0.2525
+vt 0.9090 0.2524
+vt 0.9074 0.2280
+vt 0.9617 0.9453
+vt 0.9693 0.9453
+vt 0.9693 0.9770
+vt 0.9664 0.9770
+vt 0.9335 0.4933
+vt 0.9074 0.4934
+vt 0.1696 0.9804
+vt 0.1696 0.9879
+vt 0.1620 0.9832
+vt 0.1620 0.9804
+vt 0.5830 0.0184
+vt 0.5829 0.3039
+vt 0.4776 0.3039
+vt 0.4777 0.0184
+vt 0.5675 0.3374
+vt 0.4931 0.3374
+vt 0.5458 0.3527
+vt 0.5148 0.3527
+vt 0.2537 0.0247
+vt 0.3593 0.0245
+vt 0.3596 0.3101
+vt 0.2540 0.3102
+vt 0.3441 0.3436
+vt 0.2697 0.3437
+vt 0.3224 0.3589
+vt 0.2914 0.3589
+vt 0.8447 0.5415
+vt 0.8220 0.8261
+vt 0.8148 0.8255
+vt 0.8375 0.5409
+vt 0.2578 0.5589
+vt 0.2557 0.5590
+vt 0.2642 0.5452
+vt 0.2657 0.5466
+vt 0.6469 0.9074
+vt 0.6468 0.9293
+vt 0.6430 0.9293
+vt 0.6432 0.9074
+vt 0.6468 0.9602
+vt 0.6430 0.9602
+vt 0.8459 0.5078
+vt 0.8418 0.5074
+vt 0.2739 0.8439
+vt 0.2719 0.8441
+vt 0.2537 0.5591
+vt 0.6469 0.9860
+vt 0.6432 0.9817
+vt 0.2768 0.8773
+vt 0.2731 0.8775
+vt 0.2760 0.8438
+vt 0.2805 0.8770
+vt 0.6505 0.9602
+vt 0.6506 0.9817
+vt 0.8492 0.5080
+vt 0.8511 0.5419
+vt 0.6505 0.9293
+vt 0.6506 0.9074
+vt 0.2540 0.0066
+vt 0.3596 0.0098
+vt 0.8284 0.8265
+vt 0.0066 0.6416
+vt 0.0067 0.4122
+vt 0.1120 0.4122
+vt 0.1118 0.6416
+vt 0.0221 0.3786
+vt 0.0965 0.3786
+vt 0.0439 0.3633
+vt 0.0749 0.3634
+vt 0.2358 0.6413
+vt 0.1301 0.6411
+vt 0.1305 0.4116
+vt 0.2361 0.4118
+vt 0.1461 0.3781
+vt 0.2205 0.3783
+vt 0.1679 0.3629
+vt 0.1989 0.3630
+vt 0.7199 0.2815
+vt 0.7201 0.0520
+vt 0.7273 0.0521
+vt 0.7271 0.2816
+vt 0.8934 0.4126
+vt 0.8954 0.4126
+vt 0.8954 0.4581
+vt 0.8893 0.4581
+vt 0.0822 0.9077
+vt 0.0820 0.9296
+vt 0.0783 0.9296
+vt 0.0785 0.9077
+vt 0.0820 0.9606
+vt 0.0783 0.9606
+vt 0.7214 0.3152
+vt 0.7255 0.3152
+vt 0.8954 0.1831
+vt 0.8974 0.1831
+vt 0.8975 0.4126
+vt 0.0822 0.9863
+vt 0.0785 0.9821
+vt 0.8946 0.1497
+vt 0.8983 0.1497
+vt 0.8933 0.1831
+vt 0.8909 0.1497
+vt 0.0857 0.9606
+vt 0.0859 0.9821
+vt 0.7181 0.3152
+vt 0.7135 0.2816
+vt 0.0857 0.9296
+vt 0.0859 0.9077
+vt 0.2361 0.6867
+vt 0.1302 0.6866
+vt 0.7137 0.0521
+vt 0.7003 0.0066
+vt 0.7201 0.0066
+vt 0.1120 0.6871
+vt 0.0071 0.6872
+vt 0.9015 0.4581
+vt 0.7407 0.0066
+vt 0.8350 0.8355
+vt 0.8302 0.8400
+vt 0.4773 0.0066
+vt 0.5827 0.0099
+vt 0.2626 0.5439
+vt 0.8246 0.8443
+vt 0.5954 0.7237
+vt 0.6454 0.7237
+vt 0.6389 0.7330
+vt 0.6011 0.7330
+vt 0.2102 0.8834
+vt 0.2173 0.8834
+vt 0.2166 0.8941
+vt 0.2110 0.8941
+vt 0.5892 0.7136
+vt 0.6525 0.7136
+vt 0.5276 0.9796
+vt 0.5206 0.9796
+vt 0.5213 0.9705
+vt 0.5268 0.9705
+vt 0.4773 0.7913
+vt 0.5406 0.7913
+vt 0.5344 0.8014
+vt 0.4844 0.8014
+vt 0.5261 0.9620
+vt 0.5219 0.9620
+vt 0.5218 0.9003
+vt 0.5260 0.9003
+vt 0.5287 0.8107
+vt 0.4910 0.8107
+vt 0.2159 0.9040
+vt 0.2117 0.9040
+vt 0.2117 0.9659
+vt 0.2159 0.9659
+vt 0.2164 0.9761
+vt 0.2112 0.9761
+vt 0.5316 0.8726
+vt 0.4938 0.8726
+vt 0.6361 0.7949
+vt 0.5983 0.7949
+vt 0.6401 0.8055
+vt 0.5933 0.8055
+vt 0.5214 0.8891
+vt 0.5266 0.8891
+vt 0.5366 0.8832
+vt 0.4897 0.8832
+vt 0.8667 0.8162
+vt 0.8921 0.7998
+vt 0.9033 0.8099
+vt 0.9033 0.8163
+vt 0.0492 0.8103
+vt 0.0184 0.7879
+vt 0.0628 0.7879
+vt 0.0628 0.8002
+vt 0.8570 0.8428
+vt 0.9033 0.8428
+vt 0.0066 0.7517
+vt 0.0628 0.7517
+vt 0.6689 0.7136
+vt 0.6720 0.7929
+vt 0.6584 0.7929
+vt 0.6585 0.7246
+vt 0.4773 0.7731
+vt 0.4773 0.7369
+vt 0.5457 0.7369
+vt 0.5457 0.7634
+vt 0.4773 0.7854
+vt 0.5457 0.7698
+vt 0.9355 0.1860
+vt 0.9074 0.1859
+vt 0.9121 0.1646
+vt 0.9481 0.1497
+vt 0.3144 0.9749
+vt 0.3152 0.9821
+vt 0.3053 0.9821
+vt 0.3045 0.9749
+vt 0.4528 0.9796
+vt 0.4528 0.9894
+vt 0.4496 0.9886
+vt 0.4496 0.9788
+vt 0.1876 0.9745
+vt 0.1915 0.9503
+vt 0.1947 0.9499
+vt 0.1948 0.9741
+vt 0.1400 0.9570
+vt 0.1400 0.9813
+vt 0.1301 0.9813
+vt 0.1301 0.9570
+vt 0.1745 0.9742
+vt 0.1745 0.9499
+vt 0.1777 0.9502
+vt 0.1817 0.9745
+vt 0.0708 0.7733
+vt 0.0687 0.7727
+vt 0.0687 0.7362
+vt 0.0708 0.7368
+vt 0.5728 0.9548
+vt 0.5706 0.9548
+vt 0.5801 0.9336
+vt 0.5823 0.9336
+vt 0.9011 0.0246
+vt 0.8971 0.0215
+vt 0.9331 0.0066
+vt 0.9371 0.0096
+vt 0.8964 0.0459
+vt 0.8924 0.0459
+vt 0.9430 0.0078
+vt 0.9470 0.0066
+vt 0.9470 0.0430
+vt 0.9430 0.0442
+vt 0.3226 0.9332
+vt 0.3268 0.9332
+vt 0.3363 0.9574
+vt 0.3321 0.9574
+vt 0.9463 0.0475
+vt 0.9442 0.0481
+vt 0.1191 0.9019
+vt 0.0834 0.8869
+vt 0.0875 0.8838
+vt 0.1232 0.8988
+vt 0.0783 0.8626
+vt 0.0824 0.8626
+vt 0.6506 0.8715
+vt 0.6506 0.9015
+vt 0.5892 0.8747
+vt 0.5946 0.8536
+vt 0.6079 0.8325
+vt 0.6306 0.8114
+vt 0.6506 0.8415
+vt 0.6506 0.8114
+vt 0.8148 0.9577
+vt 0.8288 0.9511
+vt 0.8288 0.9749
+vt 0.8148 0.9711
+vt 0.0434 0.9877
+vt 0.0434 0.9818
+vt 0.0497 0.9796
+vt 0.0497 0.9900
+vt 0.2367 0.9751
+vt 0.2232 0.9751
+vt 0.2294 0.9516
+vt 0.2353 0.9518
+vt 0.9558 0.9713
+vt 0.9412 0.9718
+vt 0.9497 0.9453
+vt 0.9558 0.9469
+vt 0.4347 0.9472
+vt 0.4492 0.9473
+vt 0.4492 0.9712
+vt 0.4431 0.9729
+vt 0.6038 0.9684
+vt 0.5892 0.9747
+vt 0.5892 0.9508
+vt 0.6038 0.9549
+vt 0.1755 0.9886
+vt 0.1755 0.9827
+vt 0.1817 0.9804
+vt 0.1817 0.9908
+vt 0.0544 0.9375
+vt 0.0434 0.9381
+vt 0.0481 0.9137
+vt 0.0529 0.9136
+vt 0.7731 0.3273
+vt 0.7860 0.3211
+vt 0.7860 0.3497
+vt 0.7796 0.3507
+vt 0.0680 0.9381
+vt 0.0583 0.9121
+vt 0.9619 0.5147
+vt 0.9597 0.5147
+vt 0.9597 0.3108
+vt 0.9619 0.3108
+vt 0.3497 0.5220
+vt 0.3517 0.5220
+vt 0.3517 0.7457
+vt 0.3497 0.7457
+vt 0.3576 0.5220
+vt 0.3596 0.5220
+vt 0.3596 0.7463
+vt 0.3576 0.7463
+vt 0.9542 0.0183
+vt 0.9564 0.0183
+vt 0.9564 0.2221
+vt 0.9540 0.2221
+vt 0.9799 0.7794
+vt 0.9778 0.7795
+vt 0.9778 0.5978
+vt 0.9799 0.5977
+vt 0.9729 0.0184
+vt 0.9729 0.2003
+vt 0.9702 0.2002
+vt 0.9620 0.5977
+vt 0.9640 0.5978
+vt 0.9640 0.8014
+vt 0.9619 0.8013
+vt 0.9698 0.5141
+vt 0.9678 0.5142
+vt 0.9678 0.3109
+vt 0.9698 0.3108
+vt 0.9705 0.0183
+vt 0.3226 0.9633
+vt 0.3320 0.9645
+vt 0.3320 0.9816
+vt 0.3226 0.9804
+vt 0.8899 0.6157
+vt 0.8570 0.6157
+vt 0.8622 0.6034
+vt 0.8855 0.6034
+vt 0.9532 0.4030
+vt 0.9446 0.4004
+vt 0.9417 0.3133
+vt 0.9478 0.3150
+vt 0.8823 0.7225
+vt 0.8660 0.7225
+vt 0.9619 0.8095
+vt 0.9705 0.8073
+vt 0.9705 0.9038
+vt 0.9645 0.9054
+vt 0.1004 0.8174
+vt 0.0837 0.8174
+vt 0.0783 0.7109
+vt 0.1120 0.7109
+vt 0.6347 0.9695
+vt 0.6302 0.9689
+vt 0.6322 0.9586
+vt 0.6348 0.9590
+vt 0.6367 0.9802
+vt 0.6301 0.9794
+vt 0.0840 0.6986
+vt 0.1078 0.6986
+vt 0.9455 0.3005
+vt 0.9496 0.3017
+vt 0.6342 0.9508
+vt 0.6367 0.9511
+vt 0.0925 0.6931
+vt 0.1005 0.6931
+vt 0.9508 0.2920
+vt 0.9532 0.2927
+vt 0.8704 0.5977
+vt 0.8782 0.5977
+vt 0.8735 0.9036
+vt 0.8602 0.9036
+vt 0.8635 0.8959
+vt 0.8703 0.8959
+vt 0.8768 0.9115
+vt 0.8570 0.9115
+vt 0.8765 0.8916
+vt 0.8797 0.8993
+vt 0.8928 0.8977
+vt 0.8872 0.9040
+vt 0.3820 0.9197
+vt 0.3788 0.9274
+vt 0.3720 0.9274
+vt 0.3688 0.9197
+vt 0.3853 0.9118
+vt 0.3655 0.9119
+vt 0.3882 0.9240
+vt 0.3850 0.9317
+vt 0.3957 0.9193
+vt 0.4012 0.9256
+vt 0.9912 0.2959
+vt 0.9840 0.2959
+vt 0.9839 0.2896
+vt 0.9912 0.2896
+vt 0.4206 0.9775
+vt 0.4278 0.9775
+vt 0.4278 0.9852
+vt 0.4206 0.9852
+vt 0.6159 0.9805
+vt 0.6159 0.9877
+vt 0.6096 0.9878
+vt 0.6096 0.9806
+vt 0.4633 0.9780
+vt 0.4705 0.9780
+vt 0.4705 0.9854
+vt 0.4633 0.9854
+vt 0.9912 0.3026
+vt 0.9840 0.3026
+vt 0.4705 0.9916
+vt 0.4633 0.9916
+vt 0.4206 0.9696
+vt 0.4278 0.9696
+vt 0.9840 0.2734
+vt 0.9912 0.2734
+vt 0.9486 0.9777
+vt 0.9558 0.9777
+vt 0.9558 0.9855
+vt 0.9485 0.9855
+vt 0.8151 0.5016
+vt 0.7930 0.4799
+vt 0.7709 0.4583
+vt 0.4073 0.8968
+vt 0.4071 0.9107
+vt 0.4071 0.9483
+vt 0.8865 0.1009
+vt 0.8757 0.0880
+vt 0.8865 0.0700
+vt 0.8865 0.0390
+vt 0.3470 0.5092
+vt 0.3469 0.4911
+vt 0.3469 0.4688
+vt 0.3470 0.4507
+vt 0.3470 0.4004
+vt 0.3470 0.3696
+vt 0.3470 0.4312
+vt 0.9561 0.7282
+vt 0.9561 0.7094
+vt 0.9560 0.6869
+vt 0.9559 0.6687
+vt 0.9561 0.5977
+vt 0.9560 0.6182
+vt 0.9560 0.6490
+vt 0.3435 0.7659
+vt 0.2864 0.7658
+vt 0.2907 0.7459
+vt 0.3435 0.7463
+vt 0.3435 0.8247
+vt 0.3018 0.8247
+vt 0.2880 0.8065
+vt 0.3435 0.8065
+vt 0.2864 0.7840
+vt 0.3435 0.7840
+vt 0.2805 0.9517
+vt 0.2775 0.9750
+vt 0.3039 0.7143
+vt 0.3435 0.7154
+vt 0.3204 0.6931
+vt 0.3435 0.6844
+vt 0.9838 0.4899
+vt 0.3435 0.8316
+vt 0.3231 0.8316
+vt 0.4012 0.9801
+vt 0.3936 0.9801
+vt 0.3958 0.9601
+vt 0.3988 0.9601
+vt 0.7017 0.4748
+vt 0.7071 0.4801
+vt 0.6860 0.5016
+vt 0.6806 0.4963
+vt 0.1488 0.8373
+vt 0.1715 0.8162
+vt 0.1914 0.8162
+vt 0.1914 0.8462
+vt 0.1301 0.8795
+vt 0.1355 0.8584
+vt 0.1914 0.8763
+vt 0.1914 0.9006
+vt 0.7282 0.4587
+vt 0.7228 0.4534
+vt 0.3956 0.9376
+vt 0.3986 0.9376
+vt 0.7246 0.9053
+vt 0.7275 0.9062
+vt 0.7322 0.8857
+vt 0.7350 0.8866
+vt 0.9787 0.1388
+vt 0.9787 0.1087
+vt 0.9857 0.1088
+vt 0.9857 0.1388
+vt 0.9787 0.1932
+vt 0.9787 0.1688
+vt 0.9857 0.1688
+vt 0.9857 0.1933
+vt 0.2425 0.6283
+vt 0.2450 0.6286
+vt 0.2478 0.6593
+vt 0.2452 0.6590
+vt 0.2452 0.5737
+vt 0.2478 0.5740
+vt 0.2455 0.5984
+vt 0.2430 0.5981
+vt 0.9357 0.3485
+vt 0.9155 0.3485
+vt 0.9288 0.3185
+vt 0.9356 0.3185
+vt 0.9093 0.8298
+vt 0.9272 0.8298
+vt 0.9338 0.8599
+vt 0.9093 0.8599
+vt 0.9094 0.7998
+vt 0.9140 0.7998
+vt 0.9358 0.4030
+vt 0.9074 0.4030
+vt 0.9089 0.3785
+vt 0.9357 0.3785
+vt 0.4147 0.9541
+vt 0.4100 0.9858
+vt 0.4071 0.9858
+vt 0.4071 0.9541
+vt 0.9353 0.8843
+vt 0.9092 0.8843
+vt 0.9107 0.7126
+vt 0.9183 0.7126
+vt 0.9183 0.7155
+vt 0.9107 0.7202
+vt 0.6945 0.3409
+vt 0.5892 0.3409
+vt 0.5892 0.0554
+vt 0.6945 0.0554
+vt 0.6047 0.0219
+vt 0.6791 0.0218
+vt 0.6264 0.0066
+vt 0.6574 0.0066
+vt 0.3655 0.3409
+vt 0.3658 0.0553
+vt 0.4715 0.0555
+vt 0.4712 0.3410
+vt 0.3815 0.0218
+vt 0.4559 0.0219
+vt 0.4032 0.0066
+vt 0.4342 0.0066
+vt 0.8590 0.4679
+vt 0.8518 0.4684
+vt 0.8297 0.1837
+vt 0.8369 0.1832
+vt 0.7969 0.3254
+vt 0.8049 0.3377
+vt 0.8033 0.3390
+vt 0.7949 0.3253
+vt 0.0956 0.9077
+vt 0.0993 0.9077
+vt 0.0992 0.9295
+vt 0.0955 0.9295
+vt 0.0992 0.9604
+vt 0.0955 0.9604
+vt 0.8601 0.5013
+vt 0.8560 0.5016
+vt 0.7928 0.3251
+vt 0.8105 0.0402
+vt 0.8126 0.0403
+vt 0.0993 0.9821
+vt 0.0956 0.9863
+vt 0.8118 0.0066
+vt 0.8154 0.0068
+vt 0.8146 0.0404
+vt 0.8191 0.0070
+vt 0.0918 0.9604
+vt 0.0919 0.9821
+vt 0.8633 0.5010
+vt 0.8653 0.4673
+vt 0.0918 0.9295
+vt 0.0919 0.9077
+vt 0.4715 0.3556
+vt 0.3659 0.3589
+vt 0.8433 0.1827
+vt 0.3655 0.4103
+vt 0.4708 0.4102
+vt 0.4709 0.6397
+vt 0.3657 0.6398
+vt 0.4555 0.6732
+vt 0.3811 0.6733
+vt 0.4338 0.6885
+vt 0.4028 0.6885
+vt 0.5830 0.4041
+vt 0.5833 0.6336
+vt 0.4777 0.6337
+vt 0.4773 0.4042
+vt 0.5678 0.6671
+vt 0.4933 0.6672
+vt 0.5461 0.6824
+vt 0.5151 0.6825
+vt 0.7666 0.0400
+vt 0.7738 0.0400
+vt 0.7736 0.2695
+vt 0.7664 0.2695
+vt 0.8753 0.1951
+vt 0.8712 0.1497
+vt 0.8773 0.1497
+vt 0.8773 0.1951
+vt 0.1090 0.9077
+vt 0.1127 0.9077
+vt 0.1126 0.9295
+vt 0.1089 0.9295
+vt 0.1126 0.9604
+vt 0.1089 0.9604
+vt 0.7681 0.0066
+vt 0.7722 0.0066
+vt 0.8794 0.1951
+vt 0.8795 0.4246
+vt 0.8774 0.4246
+vt 0.1127 0.9821
+vt 0.1090 0.9863
+vt 0.8803 0.4582
+vt 0.8766 0.4582
+vt 0.8753 0.4246
+vt 0.8729 0.4582
+vt 0.1052 0.9604
+vt 0.1053 0.9821
+vt 0.7648 0.0066
+vt 0.7602 0.0400
+vt 0.1052 0.9295
+vt 0.1053 0.9077
+vt 0.4774 0.3588
+vt 0.5833 0.3586
+vt 0.7600 0.2695
+vt 0.7664 0.3149
+vt 0.7466 0.3150
+vt 0.3660 0.3649
+vt 0.4709 0.3648
+vt 0.8834 0.1497
+vt 0.7869 0.3150
+vt 0.8451 0.1693
+vt 0.8499 0.1738
+vt 0.6942 0.3492
+vt 0.5888 0.3526
+vt 0.8018 0.3404
+vt 0.8395 0.1649
+vt 0.6868 0.8697
+vt 0.6925 0.8604
+vt 0.7303 0.8604
+vt 0.7368 0.8697
+vt 0.9827 0.4975
+vt 0.9820 0.5082
+vt 0.9765 0.5082
+vt 0.9757 0.4975
+vt 0.6806 0.8798
+vt 0.7439 0.8798
+vt 0.5335 0.9796
+vt 0.5342 0.9705
+vt 0.5397 0.9705
+vt 0.5406 0.9796
+vt 0.3655 0.7864
+vt 0.3726 0.7763
+vt 0.4226 0.7763
+vt 0.4288 0.7864
+vt 0.5348 0.9620
+vt 0.5348 0.9003
+vt 0.5390 0.9003
+vt 0.5390 0.9620
+vt 0.3791 0.7669
+vt 0.4169 0.7669
+vt 0.9814 0.5181
+vt 0.9772 0.5181
+vt 0.9814 0.5800
+vt 0.9818 0.5902
+vt 0.9766 0.5902
+vt 0.9771 0.5800
+vt 0.3819 0.7050
+vt 0.4197 0.7050
+vt 0.6897 0.7985
+vt 0.7274 0.7985
+vt 0.6847 0.7878
+vt 0.7315 0.7878
+vt 0.5343 0.8891
+vt 0.5395 0.8891
+vt 0.3779 0.6944
+vt 0.4247 0.6944
+vt 0.8667 0.8693
+vt 0.9033 0.8694
+vt 0.9033 0.8757
+vt 0.8921 0.8857
+vt 0.0492 0.6931
+vt 0.0628 0.7031
+vt 0.0628 0.7154
+vt 0.0184 0.7154
+vt 0.9277 0.7147
+vt 0.9380 0.7258
+vt 0.9388 0.7939
+vt 0.9253 0.7939
+vt 0.4773 0.7006
+vt 0.5457 0.7103
+vt 0.4773 0.6883
+vt 0.5457 0.7040
+vt 0.9481 0.2221
+vt 0.9121 0.2071
+vt 0.2342 0.8167
+vt 0.2453 0.8185
+vt 0.2454 0.8275
+vt 0.2271 0.8257
+vt 0.1973 0.8162
+vt 0.1973 0.8252
+vt 0.2454 0.8703
+vt 0.2271 0.8685
+vt 0.3053 0.9677
+vt 0.3152 0.9678
+vt 0.4560 0.9788
+vt 0.4560 0.9887
+vt 0.2019 0.9745
+vt 0.1979 0.9503
+vt 0.1558 0.9813
+vt 0.1459 0.9813
+vt 0.1459 0.9570
+vt 0.1558 0.9570
+vt 0.1673 0.9745
+vt 0.1713 0.9502
+vt 0.0687 0.7304
+vt 0.0687 0.6937
+vt 0.0708 0.6931
+vt 0.0708 0.7297
+vt 0.5823 0.9761
+vt 0.5801 0.9761
+vt 0.9011 0.0670
+vt 0.9371 0.0821
+vt 0.9331 0.0851
+vt 0.8971 0.0701
+vt 0.8511 0.8672
+vt 0.8511 0.9039
+vt 0.8471 0.9027
+vt 0.8471 0.8660
+vt 0.3321 0.9089
+vt 0.3363 0.9089
+vt 0.8478 0.9071
+vt 0.8500 0.9077
+vt 0.1191 0.8233
+vt 0.1232 0.8263
+vt 0.0875 0.8414
+vt 0.0834 0.8383
+vt 0.0680 0.8462
+vt 0.0066 0.8430
+vt 0.0680 0.8162
+vt 0.0119 0.8641
+vt 0.0253 0.8852
+vt 0.0479 0.9063
+vt 0.0680 0.8762
+vt 0.0680 0.9063
+vt 0.8347 0.9683
+vt 0.8347 0.9549
+vt 0.8487 0.9511
+vt 0.8487 0.9749
+vt 0.7827 0.9755
+vt 0.7890 0.9732
+vt 0.7890 0.9836
+vt 0.7827 0.9813
+vt 0.2425 0.9750
+vt 0.2440 0.9518
+vt 0.2499 0.9516
+vt 0.2560 0.9750
+vt 0.0212 0.9445
+vt 0.0212 0.9689
+vt 0.0151 0.9705
+vt 0.0066 0.9440
+vt 0.7178 0.9755
+vt 0.7262 0.9498
+vt 0.7323 0.9515
+vt 0.7323 0.9755
+vt 0.6096 0.9683
+vt 0.6096 0.9549
+vt 0.6242 0.9508
+vt 0.6242 0.9747
+vt 0.1876 0.9826
+vt 0.1938 0.9803
+vt 0.1938 0.9908
+vt 0.1876 0.9885
+vt 0.0201 0.9375
+vt 0.0216 0.9135
+vt 0.0265 0.9137
+vt 0.0311 0.9382
+vt 0.0434 0.9675
+vt 0.0499 0.9440
+vt 0.0563 0.9451
+vt 0.0563 0.9737
+vt 0.0066 0.9382
+vt 0.0163 0.9121
+vt 0.3575 0.9566
+vt 0.3575 0.7522
+vt 0.3596 0.7521
+vt 0.3596 0.9566
+vt 0.2478 0.3629
+vt 0.2478 0.5678
+vt 0.2454 0.5678
+vt 0.2449 0.3629
+vt 0.1199 0.5933
+vt 0.1199 0.8164
+vt 0.1179 0.8164
+vt 0.1179 0.5932
+vt 0.1199 0.3634
+vt 0.1199 0.5873
+vt 0.1179 0.5873
+vt 0.1179 0.3633
+vt 0.9799 0.7854
+vt 0.9799 0.9665
+vt 0.9778 0.9665
+vt 0.9778 0.7854
+vt 0.9623 0.2218
+vt 0.9623 0.0184
+vt 0.9643 0.0183
+vt 0.9643 0.2218
+vt 0.9699 0.8009
+vt 0.9699 0.5977
+vt 0.9719 0.5978
+vt 0.9719 0.8010
+vt 0.9780 0.3108
+vt 0.9780 0.4915
+vt 0.9757 0.4916
+vt 0.9757 0.3108
+vt 0.9241 0.5247
+vt 0.9241 0.5074
+vt 0.9334 0.5087
+vt 0.9334 0.5260
+vt 0.5516 0.7062
+vt 0.5569 0.6939
+vt 0.5780 0.6939
+vt 0.5814 0.7062
+vt 0.9781 0.2061
+vt 0.9781 0.3023
+vt 0.9725 0.3042
+vt 0.9702 0.2089
+vt 0.5563 0.8129
+vt 0.5711 0.8129
+vt 0.4592 0.8191
+vt 0.4635 0.7125
+vt 0.4680 0.7121
+vt 0.4656 0.8185
+vt 0.4449 0.8191
+vt 0.4347 0.7125
+vt 0.2032 0.8897
+vt 0.1999 0.8994
+vt 0.1976 0.8993
+vt 0.1992 0.8895
+vt 0.2032 0.8765
+vt 0.1973 0.8762
+vt 0.4392 0.7001
+vt 0.4595 0.7001
+vt 0.9859 0.8608
+vt 0.9858 0.8504
+vt 0.9903 0.8510
+vt 0.9925 0.8617
+vt 0.9899 0.8323
+vt 0.9925 0.8326
+vt 0.9903 0.8405
+vt 0.9878 0.8401
+vt 0.4464 0.6944
+vt 0.4532 0.6944
+vt 0.5646 0.6883
+vt 0.5717 0.6883
+vt 0.8735 0.9193
+vt 0.8703 0.9270
+vt 0.8635 0.9270
+vt 0.8602 0.9193
+vt 0.8797 0.9237
+vt 0.8765 0.9313
+vt 0.8872 0.9190
+vt 0.8928 0.9252
+vt 0.3820 0.9040
+vt 0.3688 0.9040
+vt 0.3720 0.8963
+vt 0.3788 0.8963
+vt 0.3850 0.8920
+vt 0.3882 0.8997
+vt 0.4012 0.8981
+vt 0.3957 0.9043
+vt 0.9931 0.8040
+vt 0.9930 0.8101
+vt 0.9858 0.8101
+vt 0.9858 0.8040
+vt 0.4206 0.9618
+vt 0.4206 0.9541
+vt 0.4278 0.9541
+vt 0.4278 0.9618
+vt 0.5955 0.9878
+vt 0.5892 0.9878
+vt 0.5892 0.9806
+vt 0.5955 0.9806
+vt 0.2986 0.9677
+vt 0.2986 0.9875
+vt 0.2914 0.9875
+vt 0.2914 0.9677
+vt 0.9354 0.9913
+vt 0.9354 0.9838
+vt 0.9426 0.9838
+vt 0.9427 0.9913
+vt 0.9931 0.7972
+vt 0.9858 0.7972
+vt 0.9354 0.9777
+vt 0.9427 0.9777
+vt 0.9931 0.8264
+vt 0.9858 0.8264
+vt 0.9486 0.9934
+vt 0.9558 0.9934
+vt 0.8442 0.8515
+vt 0.8365 0.8602
+vt 0.8511 0.8427
+vt 0.6733 0.8119
+vt 0.6733 0.9173
+vt 0.6577 0.9169
+vt 0.6565 0.8114
+vt 0.9538 0.4092
+vt 0.9538 0.5147
+vt 0.9406 0.5143
+vt 0.9394 0.4088
+vt 0.2784 0.5339
+vt 0.2805 0.5366
+vt 0.2762 0.5313
+vt 0.8509 0.1497
+vt 0.8585 0.1583
+vt 0.8653 0.1668
+vt 0.9437 0.7032
+vt 0.9277 0.7037
+vt 0.9289 0.5982
+vt 0.9437 0.5977
+vt 0.9549 0.9052
+vt 0.9412 0.9055
+vt 0.9424 0.8000
+vt 0.9549 0.7998
+vt 0.8191 0.3473
+vt 0.8170 0.3499
+vt 0.8149 0.3525
+vn 0.9686 0.2485 -0.0000
+vn -0.1802 0.9340 0.3085
+vn 0.2015 -0.5542 0.8076
+vn 0.0557 0.0143 0.9983
+vn -0.0901 0.5827 0.8076
+vn 0.2916 -0.9054 0.3085
+vn 0.0091 -0.5986 0.8010
+vn -0.1355 -0.0348 0.9902
+vn -0.2801 0.5290 0.8010
+vn 0.0985 -0.9469 0.3059
+vn -0.3695 0.8774 0.3059
+vn -0.1873 -0.0480 0.9811
+vn -0.2952 0.4551 0.8401
+vn 0.0992 -0.9290 0.3566
+vn -0.3602 0.8620 0.3566
+vn -0.0397 -0.5410 0.8401
+vn -0.2421 -0.3587 0.9015
+vn -0.3890 -0.0998 0.9158
+vn 0.9686 0.2484 -0.0000
+vn 0.0606 -0.8682 0.4925
+vn -0.9686 -0.2485 0.0000
+vn 0.3071 0.8593 0.4090
+vn -0.2898 0.1497 0.9453
+vn -0.3745 0.4324 0.8202
+vn -0.0273 -0.5844 0.8110
+vn 0.1030 -0.9415 0.3209
+vn -0.1802 0.9340 -0.3085
+vn 0.2015 -0.5542 -0.8076
+vn 0.0557 0.0143 -0.9983
+vn -0.0901 0.5827 -0.8076
+vn 0.2916 -0.9054 -0.3085
+vn 0.0091 -0.5986 -0.8010
+vn -0.1355 -0.0348 -0.9902
+vn -0.2801 0.5290 -0.8010
+vn 0.0985 -0.9469 -0.3059
+vn -0.3695 0.8774 -0.3059
+vn -0.1873 -0.0481 -0.9811
+vn -0.2952 0.4551 -0.8401
+vn 0.0992 -0.9290 -0.3566
+vn -0.3602 0.8620 -0.3566
+vn -0.0397 -0.5410 -0.8401
+vn -0.2421 -0.3587 -0.9015
+vn -0.3890 -0.0998 -0.9158
+vn 0.0606 -0.8682 -0.4925
+vn 0.3071 0.8593 -0.4090
+vn -0.2898 0.1497 -0.9453
+vn -0.3745 0.4324 -0.8202
+vn -0.0273 -0.5844 -0.8110
+vn 0.1030 -0.9415 -0.3209
+vn 0.1918 0.0492 0.9802
+vn 0.0487 0.6073 0.7930
+vn 0.4234 -0.8538 0.3029
+vn -0.0398 0.9522 0.3029
+vn 0.3350 -0.5089 0.7930
+vn 0.7129 0.1829 0.6770
+vn 0.6140 0.5684 0.5477
+vn 0.8728 -0.4409 0.2092
+vn 0.5529 0.8066 0.2092
+vn 0.8118 -0.2026 0.5477
+vn -0.2485 0.9686 -0.0000
+vn 0.0000 0.0000 1.0000
+vn -0.9155 -0.4023 0.0000
+vn 0.9391 0.3438 -0.0000
+vn 0.9638 0.2667 -0.0000
+vn -0.9648 -0.2631 0.0000
+vn -0.2497 0.9683 -0.0000
+vn -0.8601 -0.2888 0.4205
+vn -0.2735 -0.4686 -0.8400
+vn 0.8601 0.2888 -0.4205
+vn 0.7982 0.5299 0.2865
+vn -0.4254 0.1236 0.8965
+vn -0.7982 -0.5299 -0.2865
+vn 0.4254 -0.1236 -0.8965
+vn 0.2735 0.4686 0.8400
+vn -0.1847 -0.4661 0.8652
+vn -0.3862 0.3197 0.8652
+vn 0.4234 0.2685 0.8652
+vn 0.1246 -0.4856 0.8652
+vn -0.5003 0.0316 0.8652
+vn 0.1847 0.4661 0.8652
+vn 0.3862 -0.3197 0.8652
+vn -0.4234 -0.2685 0.8652
+vn -0.1246 0.4856 0.8652
+vn 0.5003 -0.0316 0.8652
+vn -0.7949 0.0502 0.6047
+vn 0.1979 -0.7715 0.6047
+vn 0.6726 0.4266 0.6047
+vn -0.6136 0.5078 0.6047
+vn -0.2934 -0.7405 0.6047
+vn 0.7949 -0.0502 0.6047
+vn -0.1979 0.7715 0.6047
+vn -0.6726 -0.4266 0.6047
+vn 0.6136 -0.5078 0.6047
+vn 0.2934 0.7405 0.6047
+vn -0.0542 -0.1367 0.9891
+vn -0.1133 0.0937 0.9891
+vn 0.1242 0.0787 0.9891
+vn 0.0365 -0.1424 0.9891
+vn -0.1467 0.0093 0.9891
+vn 0.0542 0.1367 0.9891
+vn 0.1133 -0.0937 0.9891
+vn -0.1242 -0.0787 0.9891
+vn -0.0365 0.1424 0.9891
+vn 0.1467 -0.0093 0.9891
+vn -0.1733 0.6755 0.7168
+vn -0.5889 -0.3735 0.7168
+vn 0.5372 -0.4446 0.7168
+vn 0.2569 0.6483 0.7168
+vn -0.6959 0.0440 0.7168
+vn 0.1733 -0.6755 0.7168
+vn 0.5889 0.3735 0.7168
+vn -0.5372 0.4446 0.7168
+vn -0.2569 -0.6483 0.7168
+vn 0.6959 -0.0440 0.7168
+vn -0.8445 -0.5356 0.0000
+vn 0.7704 -0.6376 0.0000
+vn 0.3684 0.9297 0.0000
+vn -0.9980 0.0630 0.0000
+vn 0.2485 -0.9686 0.0000
+vn 0.8445 0.5356 0.0000
+vn -0.7704 0.6376 0.0000
+vn -0.3684 -0.9297 0.0000
+vn 0.9980 -0.0630 0.0000
+vn 0.5688 -0.4708 -0.6744
+vn 0.2720 0.6865 -0.6744
+vn -0.7369 0.0465 -0.6744
+vn 0.1835 -0.7153 -0.6744
+vn 0.6236 0.3955 -0.6744
+vn -0.5688 0.4708 -0.6744
+vn -0.2720 -0.6865 -0.6744
+vn 0.7369 -0.0465 -0.6744
+vn -0.1835 0.7153 -0.6744
+vn -0.6236 -0.3955 -0.6744
+vn 0.0000 0.0000 -1.0000
+vn -0.1929 0.7519 -0.6304
+vn -0.6555 -0.4158 -0.6304
+vn 0.5980 -0.4949 -0.6304
+vn 0.2859 0.7217 -0.6304
+vn -0.7747 0.0489 -0.6304
+vn 0.1929 -0.7519 -0.6304
+vn 0.6555 0.4158 -0.6304
+vn -0.5980 0.4949 -0.6304
+vn -0.2859 -0.7217 -0.6304
+vn 0.7747 -0.0489 -0.6304
+vn 0.0221 0.0140 -0.9997
+vn -0.0202 0.0168 -0.9997
+vn -0.0097 -0.0244 -0.9997
+vn 0.0261 -0.0016 -0.9997
+vn -0.0065 0.0254 -0.9997
+vn -0.0222 -0.0141 -0.9997
+vn 0.0202 -0.0167 -0.9997
+vn 0.0096 0.0243 -0.9997
+vn -0.0263 0.0017 -0.9997
+vn 0.0065 -0.0254 -0.9997
+vn -0.1572 0.6452 0.7477
+vn 0.1238 -0.4906 0.8625
+vn 0.2337 -0.9483 -0.2146
+vn -0.0206 0.0955 0.9952
+vn 0.2210 -0.8893 0.4004
+vn 0.6159 0.2405 0.7502
+vn 0.8264 -0.4818 0.2915
+vn 0.5508 0.6225 0.5560
+vn 0.7338 -0.1782 0.6556
+vn 0.8393 -0.5208 -0.1560
+vn 0.6573 -0.7292 -0.1902
+vn 0.5886 -0.2840 0.7569
+vn 0.3275 0.6715 0.6647
+vn 0.6522 -0.6707 0.3532
+vn 0.4880 0.2157 0.8458
+vn 0.9708 0.2399 -0.0029
+vn -0.3672 -0.9268 0.0787
+vn -0.7680 0.6356 0.0787
+vn 0.8418 0.5340 0.0787
+vn 0.2477 -0.9656 0.0787
+vn -0.9949 0.0628 0.0787
+vn 0.3672 0.9268 0.0787
+vn 0.7680 -0.6356 0.0787
+vn -0.8418 -0.5340 0.0787
+vn -0.2477 0.9656 0.0787
+vn 0.9949 -0.0628 0.0787
+vn -0.8248 0.0521 0.5631
+vn 0.2053 -0.8005 0.5631
+vn 0.6979 0.4426 0.5631
+vn -0.6366 0.5269 0.5631
+vn -0.3044 -0.7683 0.5631
+vn 0.8248 -0.0521 0.5631
+vn -0.2053 0.8005 0.5631
+vn -0.6979 -0.4426 0.5631
+vn 0.6366 -0.5269 0.5631
+vn 0.3044 0.7683 0.5631
+vn -0.1075 0.0890 0.9902
+vn 0.0347 -0.1352 0.9902
+vn -0.0347 0.1352 0.9902
+vn -0.0514 -0.1298 0.9902
+vn 0.1393 -0.0088 0.9902
+vn 0.0514 0.1298 0.9902
+vn -0.1179 -0.0748 0.9902
+vn -0.1393 0.0088 0.9902
+vn 0.1075 -0.0890 0.9902
+vn 0.1179 0.0748 0.9902
+vn -0.1681 0.6553 0.7365
+vn -0.5713 -0.3623 0.7365
+vn 0.5211 -0.4313 0.7365
+vn 0.2492 0.6289 0.7365
+vn -0.6751 0.0426 0.7365
+vn 0.1681 -0.6553 0.7365
+vn 0.5713 0.3623 0.7365
+vn -0.5211 0.4313 0.7365
+vn -0.2492 -0.6289 0.7365
+vn 0.6751 -0.0426 0.7365
+vn -0.8444 -0.5356 0.0124
+vn 0.7703 -0.6376 0.0124
+vn 0.3683 0.9296 0.0124
+vn -0.9979 0.0630 0.0124
+vn 0.2484 -0.9686 0.0124
+vn 0.8444 0.5356 0.0124
+vn -0.7703 0.6376 0.0124
+vn -0.3683 -0.9296 0.0124
+vn 0.9979 -0.0630 0.0124
+vn -0.2484 0.9686 0.0124
+vn -0.9391 -0.3438 0.0000
+vn 0.2497 -0.9683 0.0000
+vn -0.9683 -0.2497 0.0000
+vn 0.9683 0.2497 -0.0000
+vn 0.1918 0.0492 -0.9802
+vn 0.0487 0.6073 -0.7930
+vn 0.4234 -0.8538 -0.3029
+vn -0.0398 0.9522 -0.3029
+vn 0.3350 -0.5089 -0.7930
+vn 0.7129 0.1829 -0.6770
+vn 0.6140 0.5684 -0.5477
+vn 0.8728 -0.4409 -0.2092
+vn 0.5529 0.8066 -0.2092
+vn 0.8118 -0.2026 -0.5477
+vn -0.8601 -0.2888 -0.4205
+vn -0.2735 -0.4686 0.8400
+vn 0.8601 0.2888 0.4205
+vn 0.7982 0.5299 -0.2865
+vn -0.4254 0.1236 -0.8965
+vn -0.7982 -0.5299 0.2865
+vn 0.4254 -0.1236 0.8965
+vn 0.2735 0.4686 -0.8400
+vn -0.1847 -0.4661 -0.8652
+vn -0.3862 0.3197 -0.8652
+vn 0.4234 0.2685 -0.8652
+vn 0.1246 -0.4856 -0.8652
+vn -0.5003 0.0316 -0.8652
+vn 0.1847 0.4661 -0.8652
+vn 0.3862 -0.3197 -0.8652
+vn -0.4234 -0.2685 -0.8652
+vn -0.1246 0.4856 -0.8652
+vn 0.5003 -0.0316 -0.8652
+vn -0.7949 0.0502 -0.6047
+vn 0.1979 -0.7715 -0.6047
+vn 0.6726 0.4266 -0.6047
+vn -0.6136 0.5078 -0.6047
+vn -0.2934 -0.7405 -0.6047
+vn 0.7949 -0.0502 -0.6047
+vn -0.1979 0.7715 -0.6047
+vn -0.6726 -0.4266 -0.6047
+vn 0.6136 -0.5078 -0.6047
+vn 0.2934 0.7405 -0.6047
+vn -0.0542 -0.1367 -0.9891
+vn -0.1133 0.0937 -0.9891
+vn 0.1242 0.0787 -0.9891
+vn 0.0365 -0.1424 -0.9891
+vn -0.1467 0.0093 -0.9891
+vn 0.0542 0.1367 -0.9891
+vn 0.1133 -0.0937 -0.9891
+vn -0.1242 -0.0787 -0.9891
+vn -0.0365 0.1424 -0.9891
+vn 0.1467 -0.0093 -0.9891
+vn -0.1733 0.6755 -0.7168
+vn -0.5889 -0.3735 -0.7168
+vn 0.5372 -0.4446 -0.7168
+vn 0.2569 0.6483 -0.7168
+vn -0.6959 0.0440 -0.7168
+vn 0.1733 -0.6755 -0.7168
+vn 0.5889 0.3735 -0.7168
+vn -0.5372 0.4446 -0.7168
+vn -0.2569 -0.6483 -0.7168
+vn 0.6959 -0.0440 -0.7168
+vn 0.5688 -0.4708 0.6744
+vn 0.2720 0.6865 0.6744
+vn -0.7369 0.0465 0.6744
+vn 0.1835 -0.7153 0.6744
+vn 0.6236 0.3955 0.6744
+vn -0.5688 0.4708 0.6744
+vn -0.2720 -0.6865 0.6744
+vn 0.7369 -0.0465 0.6744
+vn -0.1835 0.7153 0.6744
+vn -0.6236 -0.3955 0.6744
+vn -0.1929 0.7519 0.6304
+vn -0.6555 -0.4158 0.6304
+vn 0.5980 -0.4949 0.6304
+vn 0.2859 0.7217 0.6304
+vn -0.7747 0.0489 0.6304
+vn 0.1929 -0.7519 0.6304
+vn 0.6555 0.4158 0.6304
+vn -0.5980 0.4949 0.6304
+vn -0.2859 -0.7217 0.6304
+vn 0.7747 -0.0489 0.6304
+vn 0.0221 0.0140 0.9997
+vn -0.0202 0.0168 0.9997
+vn -0.0097 -0.0244 0.9997
+vn 0.0261 -0.0016 0.9997
+vn -0.0065 0.0254 0.9997
+vn -0.0222 -0.0141 0.9997
+vn 0.0202 -0.0167 0.9997
+vn 0.0096 0.0243 0.9997
+vn -0.0263 0.0017 0.9997
+vn 0.0065 -0.0254 0.9997
+vn -0.2337 0.9483 0.2146
+vn -0.1238 0.4906 -0.8625
+vn 0.1572 -0.6452 -0.7477
+vn -0.2210 0.8893 -0.4004
+vn 0.0206 -0.0955 -0.9952
+vn 0.4659 0.8345 -0.2941
+vn 0.6802 0.0972 -0.7265
+vn 0.4907 0.8604 0.1378
+vn 0.5496 0.5516 -0.6274
+vn 0.7836 -0.3002 -0.5439
+vn 0.5892 -0.4594 -0.6646
+vn 0.3743 0.5649 -0.7354
+vn 0.2570 0.9506 0.1739
+vn 0.4753 0.0303 -0.8793
+vn 0.3186 0.8869 -0.3345
+vn -0.3672 -0.9268 -0.0787
+vn -0.7680 0.6356 -0.0787
+vn 0.8418 0.5340 -0.0787
+vn 0.2477 -0.9656 -0.0787
+vn -0.9949 0.0628 -0.0787
+vn 0.3672 0.9268 -0.0787
+vn 0.7680 -0.6356 -0.0787
+vn -0.8418 -0.5340 -0.0787
+vn -0.2477 0.9656 -0.0787
+vn 0.9949 -0.0628 -0.0787
+vn -0.8248 0.0521 -0.5631
+vn 0.2053 -0.8005 -0.5631
+vn 0.6979 0.4426 -0.5631
+vn -0.6366 0.5269 -0.5631
+vn -0.3044 -0.7683 -0.5631
+vn 0.8248 -0.0521 -0.5631
+vn -0.2053 0.8005 -0.5631
+vn -0.6979 -0.4426 -0.5631
+vn 0.6366 -0.5269 -0.5631
+vn 0.3044 0.7683 -0.5631
+vn -0.1075 0.0890 -0.9902
+vn 0.0347 -0.1352 -0.9902
+vn -0.0347 0.1352 -0.9902
+vn -0.0514 -0.1298 -0.9902
+vn 0.1393 -0.0088 -0.9902
+vn 0.0514 0.1298 -0.9902
+vn -0.1179 -0.0748 -0.9902
+vn -0.1393 0.0088 -0.9902
+vn 0.1075 -0.0890 -0.9902
+vn 0.1179 0.0748 -0.9902
+vn -0.1681 0.6553 -0.7365
+vn -0.5713 -0.3623 -0.7365
+vn 0.5211 -0.4313 -0.7365
+vn 0.2492 0.6289 -0.7365
+vn -0.6751 0.0426 -0.7365
+vn 0.1681 -0.6553 -0.7365
+vn 0.5713 0.3623 -0.7365
+vn -0.5211 0.4313 -0.7365
+vn -0.2492 -0.6289 -0.7365
+vn 0.6751 -0.0426 -0.7365
+vn -0.8444 -0.5356 -0.0124
+vn 0.7703 -0.6376 -0.0124
+vn 0.3683 0.9296 -0.0124
+vn -0.9979 0.0630 -0.0124
+vn 0.2484 -0.9686 -0.0124
+vn 0.8444 0.5356 -0.0124
+vn -0.7703 0.6376 -0.0124
+vn -0.3683 -0.9296 -0.0124
+vn 0.9979 -0.0630 -0.0124
+vn -0.2484 0.9686 -0.0124
+vn 0.4415 0.7002 0.5611
+vn 0.5786 0.5257 0.6236
+vn -0.0172 -0.0044 0.9998
+vn -0.0070 -0.0041 1.0000
+vn -0.0062 -0.0016 1.0000
+vn -0.6220 -0.7831 0.0000
+vn -0.9492 -0.3146 0.0000
+vn -0.9179 0.3968 0.0000
+vn -0.9993 -0.0381 0.0000
+vn -0.9897 0.1429 0.0000
+vn -0.0555 -0.0080 0.9984
+vn -0.0541 -0.0452 0.9975
+vn -0.0535 -0.0157 0.9984
+vn -0.0541 -0.0075 0.9985
+vn -0.6049 0.6793 0.4154
+vn -0.0674 -0.0030 0.9977
+vn -0.1060 0.0058 0.9944
+vn -0.0642 -0.9979 0.0000
+vn 0.1108 -0.5908 0.7992
+vn -0.1863 0.9821 0.0281
+vn -0.2146 0.9767 0.0041
+vn -0.2094 0.9778 0.0105
+vn 0.6607 0.1695 0.7313
+vn 0.9391 0.2409 0.2452
+vn 0.8184 0.2099 0.5350
+vn 0.7272 0.6864 0.0000
+vn -0.6517 -0.6151 0.4439
+vn -0.7253 -0.6846 0.0721
+vn -0.7057 -0.6661 0.2414
+vn 0.6864 -0.7272 -0.0000
+vn -0.8039 0.5944 0.0220
+vn -0.8700 0.4875 0.0738
+vn -0.7882 0.6154 0.0055
+vn 0.2044 -0.9789 0.0000
+vn 0.2226 -0.9747 0.0223
+vn -0.2920 0.9564 0.0000
+vn -0.2740 0.9615 0.0223
+vn 0.6902 0.7237 0.0000
+vn -0.5579 -0.1431 0.8175
+vn 0.5558 0.7481 0.3625
+vn 0.4255 0.4159 0.8037
+vn -0.8791 -0.2255 0.4199
+vn 0.5732 -0.1598 0.8037
+vn 0.8473 -0.3882 0.3625
+vn -0.2483 0.8023 -0.5428
+vn 0.9533 -0.3021 0.0000
+vn -0.3281 0.9258 0.1876
+vn 0.9975 -0.0542 0.0442
+vn 0.1580 -0.9695 0.1876
+vn 0.8483 0.5277 0.0442
+vn 0.9068 -0.2369 0.3488
+vn 0.1556 -0.7685 0.6206
+vn 0.6804 0.6633 -0.3115
+vn 0.0148 -0.0708 0.9974
+vn -0.6832 -0.7302 0.0000
+vn 0.0149 -0.0715 0.9973
+vn 0.9415 -0.3370 0.0000
+vn 0.0149 -0.0715 -0.9973
+vn 0.9431 -0.3324 0.0000
+vn 0.0148 -0.0708 -0.9974
+vn -0.6865 -0.7271 0.0000
+vn -0.9876 0.1567 0.0000
+vn -0.0097 0.0463 0.9989
+vn 0.7987 0.6018 0.0000
+vn -0.0097 0.0463 -0.9989
+vn 0.7556 0.5383 0.3733
+vn 0.8685 0.4933 0.0480
+vn -0.0344 0.6897 -0.7233
+vn 0.6227 0.6192 0.4785
+vn -0.2482 0.9678 -0.0422
+vn 0.2474 -0.9643 0.0941
+vn 0.9601 0.2332 0.1543
+vn 0.0401 -0.1563 0.9869
+vn -0.9601 -0.2332 -0.1543
+vn 0.0976 -0.3807 -0.9195
+vn 0.2403 -0.9367 -0.2547
+vn 0.8807 0.4110 -0.2353
+vn 0.9326 0.1032 0.3458
+vn -0.0976 0.3807 0.9195
+vn -0.2421 0.9440 0.2243
+vn 0.1506 -0.5871 0.7954
+vn -0.8136 -0.5593 -0.1589
+vn -0.7721 -0.4776 -0.4193
+vn 0.2867 -0.9580 0.0000
+vn 0.2871 -0.9578 0.0102
+vn 0.2832 -0.9588 0.0229
+vn 0.2929 -0.9548 0.0498
+vn 0.3033 -0.9529 0.0000
+vn 0.3637 -0.9315 0.0000
+vn 0.9299 0.2385 0.2799
+vn -0.9615 -0.2748 -0.0000
+vn -0.2497 -0.0663 0.9661
+vn -0.5036 0.8295 0.2415
+vn 0.0744 -0.9312 0.3570
+vn 0.9443 0.3162 0.0907
+vn -0.9716 -0.1781 0.1559
+vn -0.2322 0.5303 0.8154
+vn -0.4648 0.8327 -0.3009
+vn 0.0443 -0.5408 0.8400
+vn -0.3090 0.8501 0.4264
+vn 0.9359 0.3129 0.1620
+vn -0.9642 -0.2351 -0.1224
+vn 0.3090 -0.8501 -0.4264
+vn -0.2680 0.8469 -0.4593
+vn 0.2680 -0.8469 0.4593
+vn 0.9117 0.4102 0.0251
+vn -0.9600 -0.2591 -0.1065
+vn -0.6896 -0.6983 -0.1919
+vn 0.6857 -0.5275 -0.5016
+vn -0.7022 -0.6627 -0.2605
+vn -0.7077 0.6149 0.3480
+vn 0.7055 0.6495 0.2836
+vn -0.2668 -0.1109 0.9574
+vn -0.5876 0.3225 0.7421
+vn 0.7123 0.6084 0.3500
+vn 0.6889 -0.7240 -0.0362
+vn 0.7147 0.5602 0.4187
+vn 0.4765 -0.7203 0.5041
+vn -0.6714 -0.7318 -0.1171
+vn -0.9593 -0.2823 -0.0000
+vn 0.9593 0.2823 0.0000
+vn 0.1616 -0.5491 0.8200
+vn 0.2598 -0.8831 0.3906
+vn -0.2112 0.7178 -0.6635
+vn 0.1501 -0.5100 -0.8470
+vn -0.1616 0.5491 -0.8200
+vn 0.2609 -0.8868 0.3814
+vn -0.0996 0.3385 0.9357
+vn -0.2609 0.8868 0.3814
+vn 0.4415 0.7002 -0.5611
+vn 0.5786 0.5257 -0.6236
+vn -0.0172 -0.0044 -0.9998
+vn -0.0070 -0.0041 -1.0000
+vn -0.0062 -0.0016 -1.0000
+vn -0.0555 -0.0080 -0.9984
+vn -0.0541 -0.0452 -0.9975
+vn -0.0535 -0.0157 -0.9984
+vn -0.0541 -0.0075 -0.9985
+vn -0.6049 0.6793 -0.4154
+vn -0.0674 -0.0030 -0.9977
+vn -0.1060 0.0058 -0.9944
+vn 0.1108 -0.5908 -0.7992
+vn -0.1863 0.9821 -0.0281
+vn -0.2146 0.9767 -0.0041
+vn -0.2094 0.9778 -0.0105
+vn 0.6607 0.1695 -0.7313
+vn 0.9391 0.2409 -0.2452
+vn 0.8184 0.2099 -0.5350
+vn -0.6517 -0.6151 -0.4439
+vn -0.7253 -0.6846 -0.0721
+vn -0.7057 -0.6661 -0.2414
+vn -0.8039 0.5944 -0.0220
+vn -0.8700 0.4875 -0.0738
+vn -0.7882 0.6154 -0.0055
+vn 0.2226 -0.9747 -0.0223
+vn -0.2740 0.9615 -0.0223
+vn -0.5579 -0.1431 -0.8175
+vn 0.5558 0.7481 -0.3625
+vn 0.4255 0.4159 -0.8037
+vn -0.8791 -0.2255 -0.4199
+vn 0.5732 -0.1598 -0.8037
+vn 0.8473 -0.3882 -0.3625
+vn -0.2483 0.8023 0.5428
+vn -0.3281 0.9258 -0.1876
+vn 0.9975 -0.0542 -0.0442
+vn 0.1580 -0.9695 -0.1876
+vn 0.8483 0.5277 -0.0442
+vn 0.9068 -0.2369 -0.3488
+vn 0.1556 -0.7685 -0.6206
+vn 0.6804 0.6633 0.3115
+vn 0.7556 0.5383 -0.3733
+vn 0.8685 0.4933 -0.0480
+vn -0.0344 0.6897 0.7233
+vn 0.6227 0.6192 -0.4785
+vn -0.2482 0.9678 0.0422
+vn 0.2474 -0.9643 -0.0941
+vn 0.9601 0.2332 -0.1543
+vn 0.0401 -0.1563 -0.9869
+vn -0.9601 -0.2332 0.1543
+vn 0.0976 -0.3807 0.9195
+vn 0.2403 -0.9367 0.2547
+vn 0.8807 0.4110 0.2353
+vn 0.9326 0.1032 -0.3458
+vn -0.0976 0.3807 -0.9195
+vn -0.2421 0.9440 -0.2243
+vn 0.1506 -0.5871 -0.7954
+vn -0.8136 -0.5593 0.1589
+vn -0.7721 -0.4776 0.4193
+vn 0.2871 -0.9578 -0.0102
+vn 0.2832 -0.9588 -0.0229
+vn 0.2929 -0.9548 -0.0498
+vn 0.9299 0.2385 -0.2799
+vn -0.2497 -0.0663 -0.9661
+vn -0.5036 0.8295 -0.2415
+vn 0.0744 -0.9312 -0.3570
+vn 0.9443 0.3162 -0.0907
+vn -0.9716 -0.1781 -0.1559
+vn -0.2322 0.5303 -0.8154
+vn -0.4648 0.8327 0.3009
+vn 0.0443 -0.5408 -0.8400
+vn -0.3090 0.8501 -0.4264
+vn 0.3090 -0.8501 0.4264
+vn 0.9093 0.3703 -0.1899
+vn -0.9802 -0.1746 0.0929
+vn -0.2680 0.8469 0.4593
+vn 0.9314 0.3601 -0.0528
+vn -0.9469 -0.3116 0.0793
+vn 0.2680 -0.8469 -0.4593
+vn -0.9191 0.3308 0.2138
+vn -0.3793 -0.8632 -0.3332
+vn -0.9132 0.2936 0.2825
+vn 0.4014 0.7758 0.4868
+vn 0.9099 -0.2805 -0.3057
+vn 0.1033 0.8528 -0.5120
+vn 0.3069 0.9515 0.0227
+vn 0.8965 -0.2409 -0.3719
+vn -0.4083 -0.5500 -0.7285
+vn 0.8759 -0.1969 -0.4404
+vn -0.3132 -0.0216 -0.9495
+vn -0.9192 0.3685 0.1387
+vn 0.1616 -0.5491 -0.8200
+vn 0.2598 -0.8831 -0.3906
+vn -0.2112 0.7178 0.6635
+vn 0.1501 -0.5100 0.8470
+vn -0.1616 0.5491 0.8200
+vn 0.2609 -0.8868 -0.3814
+vn -0.0996 0.3385 -0.9357
+vn -0.2609 0.8868 -0.3814
+vn 0.8060 0.5006 -0.3158
+vn 0.9227 0.0313 0.3842
+vn 0.0486 -0.4240 0.9044
+vn -0.2306 0.6575 -0.7173
+vn 0.8039 0.5024 0.3183
+vn 0.9214 0.0293 -0.3875
+vn 0.0492 -0.4238 -0.9044
+vn -0.2300 0.6576 0.7174
+usemtl plane
+s off
+f 22/1/1 4/2/1 8/3/1 28/4/1
+f 2/5/1 1/6/1 5/7/1 6/8/1
+f 3/9/1 2/5/1 6/8/1 7/10/1
+f 4/2/1 3/9/1 7/10/1 8/3/1
+f 1/6/1 23/11/1 29/12/1 5/7/1
+f 4/13/2 22/14/2 34/15/2 12/16/2
+f 1/17/3 2/18/3 10/19/3 9/20/3
+f 2/18/4 3/21/4 11/22/4 10/19/4
+f 3/21/5 4/23/5 12/24/5 11/22/5
+f 23/25/6 1/26/6 9/27/6 35/28/6
+f 9/20/7 10/19/7 14/29/7 13/30/7
+f 10/19/8 11/22/8 507/31/8 14/29/8
+f 11/22/9 12/24/9 508/32/9 507/31/9
+f 35/28/10 9/27/10 13/33/10 40/34/10
+f 12/16/11 34/15/11 888/35/11 508/36/11
+f 20/37/12 641/38/12 509/39/12 16/40/12
+f 641/38/13 642/41/13 17/42/13 509/39/13
+f 55/43/14 21/44/14 15/45/14 44/46/14
+f 642/47/15 879/48/15 43/49/15 17/50/15
+f 21/51/16 20/37/16 16/40/16 15/52/16
+f 15/52/17 16/40/17 19/53/17 18/54/17
+f 16/40/18 509/39/18 510/55/18 19/53/18
+f 509/56/19 17/57/19 537/58/19
+f 44/46/20 15/45/20 18/59/20 48/60/20
+f 18/61/21 19/62/21 52/63/21 53/64/21
+f 19/62/21 510/65/21 51/66/21 52/63/21
+f 510/65/21 511/67/21 728/68/21 51/66/21
+f 17/50/22 43/49/22 731/69/22 514/70/22
+f 17/71/23 514/72/23 511/73/23
+f 48/74/21 18/61/21 53/64/21
+f 510/65/21 538/75/21 511/67/21
+f 511/73/24 538/76/24 537/77/24 17/71/24
+f 13/30/25 14/29/25 20/37/25 21/51/25
+f 40/34/26 13/33/26 21/44/26 55/43/26
+f 14/29/12 507/31/12 641/38/12 20/37/12
+f 508/36/11 888/35/11 722/78/11
+f 22/1/1 28/4/1 33/79/1 27/80/1
+f 25/81/1 31/82/1 30/83/1 24/84/1
+f 26/85/1 32/86/1 31/82/1 25/81/1
+f 27/80/1 33/79/1 32/86/1 26/85/1
+f 24/84/1 30/83/1 29/12/1 23/11/1
+f 27/87/27 39/88/27 34/15/27 22/14/27
+f 24/89/28 36/90/28 37/91/28 25/92/28
+f 25/92/29 37/91/29 38/93/29 26/94/29
+f 26/94/30 38/93/30 39/95/30 27/96/30
+f 23/25/31 35/28/31 36/97/31 24/98/31
+f 36/90/32 41/99/32 42/100/32 37/91/32
+f 37/91/33 42/100/33 723/101/33 38/93/33
+f 38/93/34 723/101/34 724/102/34 39/95/34
+f 35/28/35 40/34/35 41/103/35 36/97/35
+f 39/88/36 724/104/36 888/35/36 34/15/36
+f 54/105/37 46/106/37 725/107/37 880/108/37
+f 880/108/38 725/107/38 47/109/38 881/110/38
+f 55/43/39 44/46/39 45/111/39 56/112/39
+f 881/113/40 47/114/40 43/49/40 879/48/40
+f 56/115/41 45/116/41 46/106/41 54/105/41
+f 45/116/42 49/117/42 50/118/42 46/106/42
+f 46/106/43 50/118/43 726/119/43 725/107/43
+f 725/120/19 775/121/19 47/122/19
+f 44/46/44 48/60/44 49/123/44 45/111/44
+f 49/124/21 53/64/21 52/63/21 50/125/21
+f 50/125/21 52/63/21 51/66/21 726/126/21
+f 726/126/21 51/66/21 728/68/21 727/127/21
+f 47/114/45 734/128/45 731/69/45 43/49/45
+f 47/129/46 727/130/46 734/131/46
+f 48/74/21 53/64/21 49/124/21
+f 726/126/21 727/127/21 776/132/21
+f 727/130/47 47/129/47 775/133/47 776/134/47
+f 41/99/48 56/115/48 54/105/48 42/100/48
+f 40/34/49 55/43/49 56/112/49 41/103/49
+f 42/100/37 54/105/37 880/108/37 723/101/37
+f 724/104/36 722/78/36 888/35/36
+f 59/135/50 58/136/50 62/137/50 63/138/50
+f 60/139/51 59/135/51 63/138/51 64/140/51
+f 57/141/52 269/142/52 275/143/52 61/144/52
+f 268/145/53 60/146/53 64/147/53 274/148/53
+f 58/136/54 57/149/54 61/150/54 62/137/54
+f 63/151/55 62/152/55 66/153/55 67/154/55
+f 64/155/56 63/151/56 67/154/56 68/156/56
+f 61/144/57 275/143/57 281/157/57 65/158/57
+f 274/159/58 64/155/58 68/156/58 280/160/58
+f 62/152/59 61/161/59 65/162/59 66/153/59
+f 68/156/1 67/154/1 71/163/1 72/164/1
+f 65/162/1 281/165/1 287/166/1 69/167/1
+f 280/160/1 68/156/1 72/164/1 286/168/1
+f 66/153/1 65/162/1 69/167/1 70/169/1
+f 67/154/1 66/153/1 70/169/1 71/163/1
+f 58/170/21 59/171/21 75/172/21 74/173/21
+f 59/171/21 60/174/21 76/175/21 75/172/21
+f 269/176/21 57/177/21 73/178/21 293/179/21
+f 60/174/21 268/180/21 292/181/21 76/175/21
+f 57/177/21 58/170/21 74/173/21 73/178/21
+f 77/182/60 78/183/60 892/184/60 891/185/60
+f 82/186/61 81/187/61 79/188/61 80/189/61
+f 81/190/62 895/191/62 893/192/62 79/193/62
+f 83/194/63 84/195/63 263/196/63 262/197/63
+f 892/198/64 78/199/64 82/200/64 896/201/64
+f 77/202/65 891/203/65 895/191/65 81/190/65
+f 78/204/61 77/205/61 81/187/61 82/186/61
+f 898/206/63 84/195/63 83/194/63 897/207/63
+f 896/208/66 82/209/66 84/210/66 898/211/66
+f 89/212/67 97/213/67 98/214/67 90/215/67
+f 87/216/68 95/217/68 96/218/68 88/219/68
+f 85/220/69 93/221/69 94/222/69 86/223/69
+f 92/224/70 100/225/70 93/221/70 85/220/70
+f 90/226/71 98/227/71 99/228/71 91/229/71
+f 88/230/72 96/231/72 97/213/72 89/212/72
+f 86/232/73 94/233/73 95/217/73 87/216/73
+f 91/229/74 99/228/74 100/234/74 92/235/74
+f 107/236/75 108/237/75 118/238/75 117/239/75
+f 104/240/76 105/241/76 115/242/76 114/243/76
+f 101/244/77 102/245/77 112/246/77 111/247/77
+f 108/237/78 109/248/78 119/249/78 118/238/78
+f 105/241/79 106/250/79 116/251/79 115/242/79
+f 102/245/80 103/252/80 113/253/80 112/246/80
+f 109/248/81 110/254/81 120/255/81 119/249/81
+f 106/250/82 107/236/82 117/239/82 116/251/82
+f 103/252/83 104/240/83 114/243/83 113/253/83
+f 110/254/84 101/244/84 111/247/84 120/255/84
+f 101/256/85 110/257/85 131/258/85 130/259/85
+f 104/260/86 103/261/86 128/262/86 127/263/86
+f 107/264/87 106/265/87 125/266/87 124/267/87
+f 110/268/88 109/269/88 122/270/88 131/271/88
+f 103/261/89 102/272/89 129/273/89 128/262/89
+f 106/265/90 105/274/90 126/275/90 125/266/90
+f 109/269/91 108/276/91 123/277/91 122/270/91
+f 102/278/92 101/256/92 130/259/92 129/279/92
+f 105/280/93 104/260/93 127/263/93 126/281/93
+f 108/276/94 107/282/94 124/283/94 123/277/94
+f 117/239/95 118/238/95 121/284/95
+f 114/243/96 115/242/96 121/284/96
+f 111/247/97 112/246/97 121/284/97
+f 118/238/98 119/249/98 121/284/98
+f 115/242/99 116/251/99 121/284/99
+f 112/246/100 113/253/100 121/284/100
+f 119/249/101 120/255/101 121/284/101
+f 116/251/102 117/239/102 121/284/102
+f 113/253/103 114/243/103 121/284/103
+f 120/255/104 111/247/104 121/284/104
+f 131/285/61 122/286/61 132/287/61 141/288/61
+f 127/289/61 128/290/61 138/291/61 137/292/61
+f 122/286/61 123/293/61 133/294/61 132/287/61
+f 128/290/61 129/295/61 139/296/61 138/291/61
+f 125/297/61 126/298/61 136/299/61 135/300/61
+f 123/293/61 124/301/61 134/302/61 133/294/61
+f 129/295/61 130/303/61 140/304/61 139/296/61
+f 130/303/61 131/285/61 141/288/61 140/304/61
+f 126/298/61 127/289/61 137/292/61 136/299/61
+f 124/301/61 125/297/61 135/300/61 134/302/61
+f 137/292/105 138/291/105 148/305/105 147/306/105
+f 134/302/106 135/300/106 145/307/106 144/308/106
+f 141/288/107 132/287/107 142/309/107 151/310/107
+f 138/291/108 139/296/108 149/311/108 148/305/108
+f 135/300/109 136/299/109 146/312/109 145/307/109
+f 132/287/110 133/294/110 143/313/110 142/309/110
+f 139/296/111 140/304/111 150/314/111 149/311/111
+f 136/299/112 137/292/112 147/306/112 146/312/112
+f 133/294/113 134/302/113 144/308/113 143/313/113
+f 140/304/114 141/288/114 151/310/114 150/314/114
+f 144/315/115 145/316/115 155/317/115 154/318/115
+f 151/319/116 142/320/116 152/321/116 161/322/116
+f 148/323/117 149/324/117 159/325/117 158/326/117
+f 145/316/118 146/327/118 156/328/118 155/317/118
+f 142/320/119 143/329/119 153/330/119 152/321/119
+f 149/331/120 150/332/120 160/333/120 159/334/120
+f 146/335/121 147/336/121 157/337/121 156/338/121
+f 143/329/122 144/339/122 154/340/122 153/330/122
+f 150/332/123 151/341/123 161/342/123 160/333/123
+f 147/336/60 148/323/60 158/326/60 157/337/60
+f 161/343/124 152/344/124 162/345/124 171/346/124
+f 158/347/125 159/348/125 169/349/125 168/350/125
+f 155/317/126 156/328/126 166/351/126 165/352/126
+f 152/321/127 153/330/127 163/353/127 162/354/127
+f 159/334/128 160/333/128 170/355/128 169/356/128
+f 156/357/129 157/358/129 167/359/129 166/360/129
+f 153/361/130 154/362/130 164/363/130 163/364/130
+f 160/333/131 161/342/131 171/365/131 170/355/131
+f 157/337/132 158/326/132 168/366/132 167/367/132
+f 154/318/133 155/317/133 165/352/133 164/368/133
+f 165/369/134 166/360/134 176/370/134 175/371/134
+f 162/345/134 163/364/134 173/372/134 172/373/134
+f 169/349/134 170/374/134 180/375/134 179/376/134
+f 166/360/134 167/359/134 177/377/134 176/370/134
+f 163/364/134 164/363/134 174/378/134 173/372/134
+f 170/374/134 171/346/134 181/379/134 180/375/134
+f 167/359/134 168/350/134 178/380/134 177/377/134
+f 164/363/134 165/369/134 175/371/134 174/378/134
+f 171/346/134 162/345/134 172/373/134 181/379/134
+f 168/350/134 169/349/134 179/376/134 178/380/134
+f 172/381/135 173/382/135 183/383/135 182/384/135
+f 179/385/136 180/386/136 190/387/136 189/388/136
+f 176/389/137 177/390/137 187/391/137 186/392/137
+f 173/372/138 174/378/138 184/393/138 183/394/138
+f 180/386/139 181/395/139 191/396/139 190/387/139
+f 177/390/140 178/397/140 188/398/140 187/391/140
+f 174/399/141 175/400/141 185/401/141 184/402/141
+f 181/403/142 172/381/142 182/384/142 191/404/142
+f 178/380/143 179/376/143 189/405/143 188/406/143
+f 175/400/144 176/407/144 186/408/144 185/401/144
+f 189/405/145 190/409/145 192/410/145
+f 186/411/146 187/412/146 192/410/146
+f 183/394/147 184/393/147 192/410/147
+f 190/409/148 191/413/148 192/410/148
+f 187/412/149 188/406/149 192/410/149
+f 184/393/150 185/414/150 192/410/150
+f 191/413/151 182/415/151 192/410/151
+f 188/406/152 189/405/152 192/410/152
+f 185/414/153 186/411/153 192/410/153
+f 182/415/154 183/394/154 192/410/154
+f 414/416/155 196/417/155 200/418/155 420/419/155
+f 195/420/156 194/421/156 198/422/156 199/423/156
+f 193/424/157 415/425/157 421/426/157 197/427/157
+f 196/417/158 195/420/158 199/423/158 200/418/158
+f 194/428/159 193/424/159 197/427/159 198/429/159
+f 206/430/160 205/431/160 203/432/160 204/433/160
+f 208/434/161 207/435/161 201/436/161 202/437/161
+f 436/438/162 206/439/162 204/440/162 426/441/162
+f 205/431/163 208/442/163 202/443/163 203/432/163
+f 207/435/164 437/444/164 427/445/164 201/436/164
+f 197/427/165 421/426/165 437/444/165 207/435/165
+f 199/423/166 198/422/166 208/442/166 205/431/166
+f 420/419/167 200/418/167 206/430/167 436/446/167
+f 198/429/168 197/427/168 207/435/168 208/434/168
+f 200/418/169 199/423/169 205/431/169 206/430/169
+f 204/440/170 203/447/170 426/441/170
+f 427/448/170 203/447/170 202/449/170
+f 427/448/170 202/449/170 201/450/170
+f 203/447/170 427/448/170 426/441/170
+f 215/451/171 216/452/171 226/453/171 225/454/171
+f 212/455/172 213/456/172 223/457/172 222/458/172
+f 209/459/173 210/460/173 220/461/173 219/462/173
+f 216/452/174 217/463/174 227/464/174 226/453/174
+f 213/465/175 214/466/175 224/467/175 223/468/175
+f 210/469/176 211/470/176 221/471/176 220/472/176
+f 217/463/177 218/473/177 228/474/177 227/464/177
+f 214/466/178 215/475/178 225/476/178 224/467/178
+f 211/470/179 212/455/179 222/458/179 221/471/179
+f 218/477/180 209/459/180 219/462/180 228/478/180
+f 209/479/181 218/480/181 239/481/181 238/482/181
+f 212/483/182 211/484/182 236/485/182 235/486/182
+f 215/487/183 214/488/183 233/489/183 232/490/183
+f 218/491/184 217/492/184 230/493/184 239/494/184
+f 211/484/185 210/495/185 237/496/185 236/485/185
+f 214/488/186 213/497/186 234/498/186 233/489/186
+f 217/492/187 216/499/187 231/500/187 230/493/187
+f 210/501/188 209/479/188 238/482/188 237/502/188
+f 213/503/189 212/483/189 235/486/189 234/504/189
+f 216/499/190 215/505/190 232/506/190 231/500/190
+f 225/507/95 226/508/95 229/509/95
+f 222/510/96 223/511/96 229/509/96
+f 219/512/97 220/513/97 229/509/97
+f 226/508/98 227/514/98 229/509/98
+f 223/511/99 224/515/99 229/509/99
+f 220/513/100 221/516/100 229/509/100
+f 227/514/101 228/517/101 229/509/101
+f 224/515/102 225/507/102 229/509/102
+f 221/516/103 222/510/103 229/509/103
+f 228/517/104 219/512/104 229/509/104
+f 239/518/191 230/519/191 240/520/191 249/521/191
+f 235/522/192 236/523/192 246/524/192 245/525/192
+f 230/519/193 231/526/193 241/527/193 240/520/193
+f 236/523/194 237/528/194 247/529/194 246/524/194
+f 233/530/195 234/531/195 244/532/195 243/533/195
+f 231/526/196 232/534/196 242/535/196 241/527/196
+f 237/528/197 238/536/197 248/537/197 247/529/197
+f 238/536/198 239/518/198 249/521/198 248/537/198
+f 234/531/199 235/522/199 245/525/199 244/532/199
+f 232/534/200 233/530/200 243/533/200 242/535/200
+f 245/525/201 246/524/201 256/538/201 255/539/201
+f 242/535/202 243/533/202 253/540/202 252/541/202
+f 249/521/203 240/520/203 250/542/203 259/543/203
+f 246/524/204 247/529/204 257/544/204 256/538/204
+f 243/533/205 244/532/205 254/545/205 253/540/205
+f 240/520/206 241/527/206 251/546/206 250/542/206
+f 247/529/207 248/537/207 258/547/207 257/544/207
+f 244/532/208 245/525/208 255/539/208 254/545/208
+f 241/527/209 242/535/209 252/541/209 251/546/209
+f 248/537/210 249/521/210 259/543/210 258/547/210
+f 252/548/211 253/549/211 492/550/211 491/551/211
+f 259/552/212 250/553/212 489/554/212 498/555/212
+f 256/556/213 257/557/213 496/558/213 495/559/213
+f 253/549/214 254/560/214 493/561/214 492/550/214
+f 250/553/215 251/562/215 490/563/215 489/554/215
+f 257/564/216 258/565/216 497/566/216 496/567/216
+f 254/568/217 255/569/217 494/570/217 493/571/217
+f 251/562/218 252/572/218 491/573/218 490/563/218
+f 258/565/219 259/574/219 498/575/219 497/566/219
+f 255/569/220 256/556/220 495/559/220 494/570/220
+f 261/576/61 260/577/61 262/578/61 263/579/61
+f 261/576/61 263/579/61 266/580/61 267/581/61
+f 82/582/221 80/583/221 260/584/221 261/585/221
+f 80/586/222 83/587/222 262/588/222 260/589/222
+f 264/590/66 265/591/66 267/592/66 266/593/66
+f 84/594/134 82/595/134 265/596/134 264/597/134
+f 82/582/223 261/585/223 267/598/223 265/599/223
+f 263/196/224 84/195/224 264/600/224 266/601/224
+f 272/602/225 278/603/225 277/604/225 271/605/225
+f 273/606/226 279/607/226 278/603/226 272/602/226
+f 270/608/227 276/609/227 275/143/227 269/142/227
+f 268/145/228 274/148/228 279/610/228 273/611/228
+f 271/605/229 277/604/229 276/612/229 270/613/229
+f 278/614/230 284/615/230 283/616/230 277/617/230
+f 279/618/231 285/619/231 284/615/231 278/614/231
+f 276/609/232 282/620/232 281/157/232 275/143/232
+f 274/159/233 280/160/233 285/619/233 279/618/233
+f 277/617/234 283/616/234 282/621/234 276/622/234
+f 285/619/1 291/623/1 290/624/1 284/615/1
+f 282/621/1 288/625/1 287/166/1 281/165/1
+f 280/160/1 286/168/1 291/623/1 285/619/1
+f 283/616/1 289/626/1 288/625/1 282/621/1
+f 284/615/1 290/624/1 289/626/1 283/616/1
+f 271/627/21 295/628/21 296/629/21 272/630/21
+f 272/630/21 296/629/21 297/631/21 273/632/21
+f 269/176/21 293/179/21 294/633/21 270/634/21
+f 273/632/21 297/631/21 292/181/21 268/180/21
+f 270/634/21 294/633/21 295/628/21 271/627/21
+f 298/635/60 891/185/60 892/184/60 299/636/60
+f 303/637/134 301/638/134 300/639/134 302/640/134
+f 302/641/62 300/642/62 893/192/62 895/191/62
+f 304/643/63 501/644/63 502/645/63 305/646/63
+f 892/198/64 896/201/64 303/647/64 299/648/64
+f 298/649/65 302/641/65 895/191/65 891/203/65
+f 299/650/134 303/637/134 302/640/134 298/651/134
+f 898/206/63 897/207/63 304/643/63 305/646/63
+f 896/208/66 898/211/66 305/652/66 303/653/66
+f 310/654/235 311/655/235 319/656/235 318/657/235
+f 308/658/236 309/659/236 317/660/236 316/661/236
+f 306/662/237 307/663/237 315/664/237 314/665/237
+f 313/666/238 306/662/238 314/665/238 321/667/238
+f 311/668/239 312/669/239 320/670/239 319/671/239
+f 309/672/240 310/654/240 318/657/240 317/673/240
+f 307/674/241 308/658/241 316/661/241 315/675/241
+f 312/669/242 313/676/242 321/677/242 320/670/242
+f 328/678/243 338/679/243 339/680/243 329/681/243
+f 325/682/244 335/683/244 336/684/244 326/685/244
+f 322/686/245 332/687/245 333/688/245 323/689/245
+f 329/681/246 339/680/246 340/690/246 330/691/246
+f 326/685/247 336/684/247 337/692/247 327/693/247
+f 323/689/248 333/688/248 334/694/248 324/695/248
+f 330/691/249 340/690/249 341/696/249 331/697/249
+f 327/693/250 337/692/250 338/679/250 328/678/250
+f 324/695/251 334/694/251 335/683/251 325/682/251
+f 331/697/252 341/696/252 332/687/252 322/686/252
+f 322/698/253 351/699/253 352/700/253 331/701/253
+f 325/702/254 348/703/254 349/704/254 324/705/254
+f 328/706/255 345/707/255 346/708/255 327/709/255
+f 331/710/256 352/711/256 343/712/256 330/713/256
+f 324/705/257 349/704/257 350/714/257 323/715/257
+f 327/709/258 346/708/258 347/716/258 326/717/258
+f 330/713/259 343/712/259 344/718/259 329/719/259
+f 323/720/260 350/721/260 351/699/260 322/698/260
+f 326/722/261 347/723/261 348/703/261 325/702/261
+f 329/719/262 344/718/262 345/724/262 328/725/262
+f 338/679/263 342/726/263 339/680/263
+f 335/683/264 342/726/264 336/684/264
+f 332/687/265 342/726/265 333/688/265
+f 339/680/266 342/726/266 340/690/266
+f 336/684/267 342/726/267 337/692/267
+f 333/688/268 342/726/268 334/694/268
+f 340/690/269 342/726/269 341/696/269
+f 337/692/270 342/726/270 338/679/270
+f 334/694/271 342/726/271 335/683/271
+f 341/696/272 342/726/272 332/687/272
+f 352/727/134 362/728/134 353/729/134 343/730/134
+f 348/731/134 358/732/134 359/733/134 349/734/134
+f 343/730/134 353/729/134 354/735/134 344/736/134
+f 349/734/134 359/733/134 360/737/134 350/738/134
+f 346/739/134 356/740/134 357/741/134 347/742/134
+f 344/736/134 354/735/134 355/743/134 345/744/134
+f 350/738/134 360/737/134 361/745/134 351/746/134
+f 351/746/134 361/745/134 362/728/134 352/727/134
+f 347/742/134 357/741/134 358/732/134 348/731/134
+f 345/744/134 355/743/134 356/740/134 346/739/134
+f 358/732/273 368/747/273 369/748/273 359/733/273
+f 355/743/274 365/749/274 366/750/274 356/740/274
+f 362/728/275 372/751/275 363/752/275 353/729/275
+f 359/733/276 369/748/276 370/753/276 360/737/276
+f 356/740/277 366/750/277 367/754/277 357/741/277
+f 353/729/278 363/752/278 364/755/278 354/735/278
+f 360/737/279 370/753/279 371/756/279 361/745/279
+f 357/741/280 367/754/280 368/747/280 358/732/280
+f 354/735/281 364/755/281 365/749/281 355/743/281
+f 361/745/282 371/756/282 372/751/282 362/728/282
+f 365/757/115 375/758/115 376/759/115 366/760/115
+f 372/761/116 382/762/116 373/763/116 363/764/116
+f 369/765/117 379/766/117 380/767/117 370/768/117
+f 366/760/118 376/759/118 377/769/118 367/770/118
+f 363/764/119 373/763/119 374/771/119 364/772/119
+f 370/773/120 380/774/120 381/775/120 371/776/120
+f 367/777/121 377/778/121 378/779/121 368/780/121
+f 364/772/122 374/771/122 375/781/122 365/782/122
+f 371/776/123 381/775/123 382/783/123 372/784/123
+f 368/780/60 378/779/60 379/766/60 369/765/60
+f 382/785/283 392/786/283 383/787/283 373/788/283
+f 379/789/284 389/790/284 390/791/284 380/792/284
+f 376/759/285 386/793/285 387/794/285 377/769/285
+f 373/763/286 383/795/286 384/796/286 374/771/286
+f 380/774/287 390/797/287 391/798/287 381/775/287
+f 377/799/288 387/800/288 388/801/288 378/802/288
+f 374/803/289 384/804/289 385/805/289 375/806/289
+f 381/775/290 391/798/290 392/807/290 382/783/290
+f 378/779/291 388/808/291 389/809/291 379/766/291
+f 375/758/292 385/810/292 386/793/292 376/759/292
+f 386/811/61 396/812/61 397/813/61 387/800/61
+f 383/787/61 393/814/61 394/815/61 384/804/61
+f 390/791/61 400/816/61 401/817/61 391/818/61
+f 387/800/61 397/813/61 398/819/61 388/801/61
+f 384/804/61 394/815/61 395/820/61 385/805/61
+f 391/818/61 401/817/61 402/821/61 392/786/61
+f 388/801/61 398/819/61 399/822/61 389/790/61
+f 385/805/61 395/820/61 396/812/61 386/811/61
+f 392/786/61 402/821/61 393/814/61 383/787/61
+f 389/790/61 399/822/61 400/816/61 390/791/61
+f 393/823/293 403/824/293 404/825/293 394/826/293
+f 400/827/294 410/828/294 411/829/294 401/830/294
+f 397/831/295 407/832/295 408/833/295 398/834/295
+f 394/815/296 404/835/296 405/836/296 395/820/296
+f 401/830/297 411/829/297 412/837/297 402/838/297
+f 398/834/298 408/833/298 409/839/298 399/840/298
+f 395/841/299 405/842/299 406/843/299 396/844/299
+f 402/845/300 412/846/300 403/824/300 393/823/300
+f 399/822/301 409/847/301 410/848/301 400/816/301
+f 396/844/302 406/843/302 407/849/302 397/850/302
+f 410/848/303 413/851/303 411/852/303
+f 407/853/304 413/851/304 408/854/304
+f 404/835/305 413/851/305 405/836/305
+f 411/852/306 413/851/306 412/855/306
+f 408/854/307 413/851/307 409/847/307
+f 405/836/308 413/851/308 406/856/308
+f 412/855/309 413/851/309 403/857/309
+f 409/847/310 413/851/310 410/848/310
+f 406/856/311 413/851/311 407/853/311
+f 403/857/312 413/851/312 404/835/312
+f 414/858/313 420/859/313 425/860/313 419/861/313
+f 418/862/314 424/863/314 423/864/314 417/865/314
+f 416/866/315 422/867/315 421/868/315 415/869/315
+f 419/861/316 425/860/316 424/870/316 418/871/316
+f 417/865/317 423/864/317 422/867/317 416/866/317
+f 433/872/318 431/873/318 430/874/318 432/875/318
+f 435/876/319 429/877/319 428/878/319 434/879/319
+f 436/438/320 426/441/320 431/873/320 433/872/320
+f 432/875/321 430/874/321 429/880/321 435/881/321
+f 434/882/322 428/883/322 427/448/322 437/884/322
+f 422/867/323 434/879/323 437/885/323 421/868/323
+f 424/863/324 432/886/324 435/876/324 423/864/324
+f 420/859/325 436/887/325 433/888/325 425/860/325
+f 423/864/326 435/876/326 434/879/326 422/867/326
+f 425/860/327 433/888/327 432/889/327 424/870/327
+f 431/873/170 426/441/170 430/874/170
+f 427/448/170 429/880/170 430/874/170
+f 427/448/170 428/883/170 429/880/170
+f 430/874/170 426/441/170 427/448/170
+f 444/890/328 454/891/328 455/892/328 445/893/328
+f 441/894/329 451/895/329 452/896/329 442/897/329
+f 438/898/330 448/899/330 449/900/330 439/901/330
+f 445/893/331 455/892/331 456/902/331 446/903/331
+f 442/904/332 452/905/332 453/906/332 443/907/332
+f 439/908/333 449/909/333 450/910/333 440/911/333
+f 446/903/334 456/902/334 457/912/334 447/913/334
+f 443/907/335 453/906/335 454/914/335 444/915/335
+f 440/911/336 450/910/336 451/895/336 441/894/336
+f 447/916/337 457/917/337 448/899/337 438/898/337
+f 438/918/338 467/919/338 468/920/338 447/921/338
+f 441/922/339 464/923/339 465/924/339 440/925/339
+f 444/926/340 461/927/340 462/928/340 443/929/340
+f 447/930/341 468/931/341 459/932/341 446/933/341
+f 440/925/342 465/924/342 466/934/342 439/935/342
+f 443/929/343 462/928/343 463/936/343 442/937/343
+f 446/933/344 459/932/344 460/938/344 445/939/344
+f 439/940/345 466/941/345 467/919/345 438/918/345
+f 442/942/346 463/943/346 464/923/346 441/922/346
+f 445/939/347 460/938/347 461/944/347 444/945/347
+f 454/946/263 458/947/263 455/948/263
+f 451/949/264 458/947/264 452/950/264
+f 448/951/265 458/947/265 449/952/265
+f 455/948/266 458/947/266 456/953/266
+f 452/950/267 458/947/267 453/954/267
+f 449/952/268 458/947/268 450/955/268
+f 456/953/269 458/947/269 457/956/269
+f 453/954/270 458/947/270 454/946/270
+f 450/955/271 458/947/271 451/949/271
+f 457/956/272 458/947/272 448/951/272
+f 468/957/348 478/958/348 469/959/348 459/960/348
+f 464/961/349 474/962/349 475/963/349 465/964/349
+f 459/960/350 469/959/350 470/965/350 460/966/350
+f 465/964/351 475/963/351 476/967/351 466/968/351
+f 462/969/352 472/970/352 473/971/352 463/972/352
+f 460/966/353 470/965/353 471/973/353 461/974/353
+f 466/968/354 476/967/354 477/975/354 467/976/354
+f 467/976/355 477/975/355 478/958/355 468/957/355
+f 463/972/356 473/971/356 474/962/356 464/961/356
+f 461/974/357 471/973/357 472/970/357 462/969/357
+f 474/962/358 484/977/358 485/978/358 475/963/358
+f 471/973/359 481/979/359 482/980/359 472/970/359
+f 478/958/360 488/981/360 479/982/360 469/959/360
+f 475/963/361 485/978/361 486/983/361 476/967/361
+f 472/970/362 482/980/362 483/984/362 473/971/362
+f 469/959/363 479/982/363 480/985/363 470/965/363
+f 476/967/364 486/983/364 487/986/364 477/975/364
+f 473/971/365 483/984/365 484/977/365 474/962/365
+f 470/965/366 480/985/366 481/979/366 471/973/366
+f 477/975/367 487/986/367 488/981/367 478/958/367
+f 481/987/368 491/551/368 492/550/368 482/988/368
+f 488/989/369 498/555/369 489/554/369 479/990/369
+f 485/991/370 495/559/370 496/558/370 486/992/370
+f 482/988/371 492/550/371 493/561/371 483/993/371
+f 479/990/372 489/554/372 490/563/372 480/994/372
+f 486/995/373 496/567/373 497/566/373 487/996/373
+f 483/997/374 493/571/374 494/570/374 484/998/374
+f 480/994/375 490/563/375 491/573/375 481/999/375
+f 487/996/376 497/566/376 498/575/376 488/1000/376
+f 484/998/377 494/570/377 495/559/377 485/991/377
+f 500/1001/134 502/1002/134 501/1003/134 499/1004/134
+f 500/1001/134 506/1005/134 505/1006/134 502/1002/134
+f 303/1007/221 500/1008/221 499/1009/221 301/1010/221
+f 301/1011/222 499/1012/222 501/1013/222 304/1014/222
+f 503/1015/66 505/1016/66 506/1017/66 504/1018/66
+f 305/1019/61 503/1020/61 504/1021/61 303/1022/61
+f 303/1007/223 504/1023/223 506/1024/223 500/1008/223
+f 502/645/224 505/1025/224 503/1026/224 305/646/224
+f 738/1027/21 516/1028/21 512/1029/21 730/1030/21
+f 728/68/21 511/67/21 513/1031/21 732/1032/21
+f 512/1033/378 515/1034/378 735/1035/378 730/1036/378
+f 514/1037/379 731/1038/379 735/1035/379 515/1034/379
+f 732/1032/21 513/1031/21 516/1028/21 738/1027/21
+f 512/1039/380 516/1040/380 515/1041/380
+f 515/1041/381 516/1040/381 513/1042/381 514/72/381
+f 513/1042/382 511/73/382 514/72/382
+f 517/1043/1 746/1044/1 745/1045/1 518/1046/1
+f 518/1046/1 745/1045/1 744/1047/1 519/1048/1
+f 519/1048/1 744/1047/1 743/1049/1 520/1050/1
+f 753/1051/1 749/1052/1 521/1053/1 523/1054/1
+f 743/1049/1 750/1055/1 522/1056/1 520/1050/1
+f 750/1055/1 753/1051/1 523/1054/1 522/1056/1
+f 524/1057/383 525/1058/383 761/1059/383 762/1060/383
+f 525/1058/384 526/1061/384 759/1062/384 761/1059/384
+f 526/1061/21 527/1063/21 760/1064/21 759/1062/21
+f 767/1065/385 529/1066/385 528/1067/385 763/1068/385
+f 760/1064/386 527/1063/386 530/1069/386 765/1070/386
+f 765/1070/387 530/1069/387 529/1066/387 767/1065/387
+f 520/1071/388 522/1072/388 530/1073/388 527/1074/388
+f 517/1075/389 518/1076/389 525/1077/389 524/1078/389
+f 518/1076/390 519/1079/390 526/1080/390 525/1077/390
+f 519/1079/391 520/1071/391 527/1074/391 526/1080/391
+f 521/1081/392 749/1082/392 763/1083/392 528/1084/392
+f 522/1072/393 523/1085/393 529/1086/393 530/1073/393
+f 523/1085/394 521/1087/394 528/1088/394 529/1086/394
+f 747/1089/19 746/1044/19 517/1043/19
+f 754/1090/395 524/1091/395 762/1092/395
+f 747/1093/396 517/1075/396 524/1078/396 754/1094/396
+f 532/1095/61 531/1096/61 539/1097/61 540/1098/61
+f 536/1099/21 532/1100/21 540/1101/21 542/1102/21
+f 543/1103/397 542/1104/397 540/1105/397 539/1106/397
+f 537/1107/398 538/1108/398 544/1109/398 541/1110/398
+f 541/1110/399 544/1109/399 542/1104/399 543/1103/399
+f 510/65/21 535/1111/21 544/1112/21 538/75/21
+f 531/1096/400 533/1113/400 543/1114/400 539/1097/400
+f 534/1115/401 509/56/401 537/58/401 541/1116/401
+f 535/1111/21 536/1099/21 542/1102/21 544/1112/21
+f 533/1117/402 534/1115/402 541/1116/402 543/1118/402
+f 548/1119/403 551/1120/403 550/1121/403 546/1122/403
+f 545/1123/403 549/1124/403 552/1125/403 547/1126/403
+f 547/1126/403 552/1125/403 551/1120/403 548/1119/403
+f 553/1127/404 554/1128/404 558/1129/404 557/1130/404
+f 555/1131/405 556/1132/405 560/1133/405 559/1134/405
+f 556/1132/406 553/1127/406 557/1130/406 560/1133/406
+f 548/1135/407 546/1136/407 554/1137/407 553/1138/407
+f 551/1139/408 552/1140/408 560/1141/408 557/1142/408
+f 550/1143/409 551/1139/409 557/1142/409 558/1144/409
+f 545/1145/407 547/1146/407 556/1147/407 555/1148/407
+f 549/1149/134 545/1150/134 555/1151/134 559/1152/134
+f 552/1140/410 549/1153/410 559/1154/410 560/1141/410
+f 546/1155/61 550/1156/61 558/1157/61 554/1158/61
+f 547/1146/407 548/1135/407 553/1138/407 556/1147/407
+f 561/1159/411 563/1160/411 564/1161/411 562/1162/411
+f 563/1160/412 565/1163/412 566/1164/412 564/1161/412
+f 565/1163/119 567/1165/119 568/1166/119 566/1164/119
+f 570/1167/413 569/1168/413 571/1169/413 572/1170/413
+f 572/1170/414 571/1169/414 573/1171/414 574/1172/414
+f 574/1172/60 573/1171/60 575/1173/60 576/1174/60
+f 578/1175/415 577/1176/415 570/1177/415 572/1178/415
+f 562/1179/21 579/1180/21 620/1181/21 615/1182/21
+f 581/1183/416 580/1184/416 575/1185/416 573/1186/416
+f 580/1184/61 582/1187/61 576/1188/61 575/1185/61
+f 583/1189/417 578/1175/417 572/1178/417 574/1190/417
+f 579/1180/21 584/1191/21 571/1192/21 569/1193/21
+f 582/1187/418 583/1194/418 574/1195/418 576/1188/418
+f 584/1191/419 581/1196/419 573/1197/419 571/1192/419
+f 564/1198/419 566/1199/419 581/1196/419 584/1191/419
+f 567/1200/420 565/1201/420 583/1194/420 582/1187/420
+f 562/1179/21 564/1198/21 584/1191/21 579/1180/21
+f 565/1202/421 563/1203/421 578/1175/421 583/1189/421
+f 568/1204/61 567/1200/61 582/1187/61 580/1184/61
+f 566/1205/416 568/1204/416 580/1184/416 581/1183/416
+f 569/1168/422 570/1167/422 618/1206/422 617/1207/422
+f 563/1203/423 561/1208/423 577/1176/423 578/1175/423
+f 585/1209/411 587/1210/411 588/1211/411 586/1212/411
+f 587/1210/412 589/1213/412 590/1214/412 588/1211/412
+f 589/1213/119 591/1215/119 592/1216/119 590/1214/119
+f 594/1217/413 593/1218/413 595/1219/413 596/1220/413
+f 596/1220/414 595/1219/414 597/1221/414 598/1222/414
+f 598/1222/60 597/1221/60 599/1223/60 600/1224/60
+f 602/1225/415 601/1226/415 594/1227/415 596/1228/415
+f 586/1229/21 603/1230/21 614/1231/21 609/1232/21
+f 605/1233/416 604/1234/416 599/1235/416 597/1236/416
+f 604/1234/61 606/1237/61 600/1238/61 599/1235/61
+f 607/1239/417 602/1225/417 596/1228/417 598/1240/417
+f 603/1230/21 608/1241/21 595/1242/21 593/1243/21
+f 606/1237/418 607/1244/418 598/1245/418 600/1238/418
+f 608/1241/419 605/1246/419 597/1247/419 595/1242/419
+f 588/1248/419 590/1249/419 605/1246/419 608/1241/419
+f 591/1250/420 589/1251/420 607/1244/420 606/1237/420
+f 586/1229/21 588/1248/21 608/1241/21 603/1230/21
+f 589/1252/421 587/1253/421 602/1225/421 607/1239/421
+f 592/1254/61 591/1250/61 606/1237/61 604/1234/61
+f 590/1255/416 592/1254/416 604/1234/416 605/1233/416
+f 593/1218/424 594/1217/424 612/1256/424 611/1257/424
+f 587/1253/423 585/1258/423 601/1226/423 602/1225/423
+f 601/1226/425 585/1258/425 610/1259/425 613/1260/425
+f 585/1209/426 586/1212/426 609/1261/426 610/1262/426
+f 603/1230/21 593/1243/21 611/1263/21 614/1231/21
+f 594/1227/427 601/1226/427 613/1260/427 612/1264/427
+f 577/1176/428 561/1208/428 616/1265/428 619/1266/428
+f 561/1159/429 562/1162/429 615/1267/429 616/1268/429
+f 579/1180/21 569/1193/21 617/1269/21 620/1181/21
+f 570/1177/430 577/1176/430 619/1266/430 618/1270/430
+f 628/1271/431 627/1272/431 631/1273/431 632/1274/431
+f 624/1275/432 622/1276/432 625/1277/432 627/1278/432
+f 623/1279/433 624/1280/433 627/1272/433 628/1271/433
+f 621/1281/434 623/1282/434 628/1283/434 626/1284/434
+f 622/1285/435 621/1286/435 626/1287/435 625/1288/435
+f 630/1289/1 632/1290/1 636/1291/1 634/1292/1
+f 626/1284/436 628/1283/436 632/1290/436 630/1289/436
+f 625/1288/437 626/1287/437 630/1293/437 629/1294/437
+f 627/1278/438 625/1277/438 629/1295/438 631/1296/438
+f 635/1297/439 633/1298/439 637/1299/439 639/1300/439
+f 629/1294/134 630/1293/134 634/1301/134 633/1302/134
+f 631/1296/21 629/1295/21 633/1298/21 635/1297/21
+f 632/1274/61 631/1273/61 635/1303/61 636/1304/61
+f 636/1304/440 635/1303/440 639/1305/440 640/1306/440
+f 634/1292/441 636/1291/441 640/1307/441 638/1308/441
+f 633/1302/442 634/1301/442 638/1309/442 637/1310/442
+f 642/1311/443 641/1312/443 645/1313/443 646/1314/443
+f 507/1315/21 508/1316/21 644/1317/21 643/1318/21
+f 879/1319/444 642/1311/444 646/1314/444 885/1320/444
+f 508/1316/21 722/1321/21 882/1322/21 644/1317/21
+f 641/1323/445 507/1324/445 643/1325/445 645/1326/445
+f 644/1327/60 882/1328/60 885/1329/60 646/1330/60
+f 643/1331/60 644/1327/60 646/1330/60 645/1332/60
+f 888/1333/446 890/1334/446 647/1335/446 508/1336/446
+f 899/1337/447 648/1338/447 649/1339/447 901/1340/447
+f 903/1341/448 905/1342/448 651/1343/448 650/1344/448
+f 649/1345/449 651/1346/449 905/1347/449 901/1348/449
+f 648/1349/450 650/1350/450 651/1351/450 649/1352/450
+f 899/1353/451 903/1354/451 650/1355/451 648/1356/451
+f 647/1357/452 653/1358/452 652/1359/452 508/1360/452
+f 890/1361/453 909/1362/453 653/1363/453 647/1364/453
+f 653/1365/454 655/1366/454 654/1367/454 652/1368/454
+f 909/1369/455 912/1370/455 655/1366/455 653/1365/455
+f 655/1371/456 657/1372/456 656/1373/456 654/1374/456
+f 912/1375/457 915/1376/457 657/1377/457 655/1378/457
+f 508/1379/458 652/1380/458 654/1374/458 656/1373/458
+f 656/1381/459 657/1382/459 647/1383/459 508/1384/459
+f 657/1382/460 915/1385/460 890/1386/460 647/1383/460
+f 535/1387/461 510/1388/461 509/1389/461
+f 509/1389/462 534/1390/462 535/1387/462
+f 535/1387/463 534/1390/463 533/1391/463
+f 531/1392/464 536/1393/464 533/1391/464
+f 536/1393/465 535/1387/465 533/1391/465
+f 531/1392/466 532/1394/466 536/1393/466
+f 658/1395/467 660/1396/467 661/1397/467 659/1398/467
+f 663/1399/468 662/1400/468 664/1401/468 665/1402/468
+f 658/1403/469 659/1404/469 662/1405/469 663/1406/469
+f 659/1407/470 661/1408/470 664/1409/470 662/1410/470
+f 660/1411/471 658/1412/471 663/1413/471 665/1414/471
+f 666/1415/472 668/1416/472 669/1417/472 667/1418/472
+f 671/1419/473 670/1420/473 672/1421/473 673/1422/473
+f 666/1423/474 667/1424/474 670/1425/474 671/1426/474
+f 667/1427/475 669/1428/475 672/1429/475 670/1430/475
+f 668/1431/476 666/1423/476 671/1426/476 673/1432/476
+f 676/1433/477 677/1434/477 681/1435/477 680/1436/477
+f 677/1437/478 674/1438/478 678/1439/478 681/1440/478
+f 675/1441/479 676/1442/479 680/1443/479 679/1444/479
+f 674/1445/480 675/1446/480 679/1447/480 678/1448/480
+f 684/1449/481 685/1450/481 689/1451/481 688/1452/481
+f 683/1453/482 687/1454/482 686/1455/482
+f 685/1456/483 682/1457/483 686/1458/483 689/1459/483
+f 683/1460/484 684/1461/484 688/1462/484 687/1463/484
+f 682/1464/482 683/1453/482 686/1455/482
+f 691/1465/314 693/1466/314 692/1467/314 690/1468/314
+f 694/1469/485 695/1470/485 699/1471/485 698/1472/485
+f 690/1473/486 692/1474/486 697/1475/486 695/1476/486
+f 691/1477/487 690/1478/487 695/1470/487 694/1469/487
+f 693/1479/488 691/1480/488 694/1481/488 696/1482/488
+f 692/1483/489 693/1484/489 696/1485/489 697/1486/489
+f 700/1487/490 698/1488/490 702/1489/490 704/1490/490
+f 696/1491/491 694/1492/491 698/1488/491 700/1487/491
+f 697/1486/492 696/1485/492 700/1493/492 701/1494/492
+f 695/1476/493 697/1475/493 701/1495/493 699/1496/493
+f 702/1489/156 703/1497/156 705/1498/156 704/1490/156
+f 701/1494/494 700/1493/494 704/1499/494 705/1500/494
+f 699/1496/495 701/1495/495 705/1501/495 703/1502/495
+f 698/1472/496 699/1471/496 703/1503/496 702/1504/496
+f 708/1505/497 709/1506/497 706/1507/497 707/1508/497
+f 965/1509/497 964/1510/497 709/1506/497 708/1505/497
+f 708/1505/497 707/1508/497 710/1511/497 711/1512/497
+f 711/1512/497 710/1511/497 712/1513/497 713/1514/497
+f 716/1515/498 715/1516/498 714/1517/498 717/1518/498
+f 975/1519/498 716/1515/498 717/1518/498 974/1520/498
+f 716/1515/498 719/1521/498 718/1522/498 715/1516/498
+f 719/1521/498 721/1523/498 720/1524/498 718/1522/498
+f 707/1525/499 715/1526/499 718/1527/499 710/1528/499
+f 709/1529/500 717/1530/500 714/1531/500 706/1532/500
+f 712/1533/501 720/1534/501 721/1535/501 713/1536/501
+f 713/1537/502 721/1538/502 719/1539/502 711/1540/502
+f 706/1541/61 714/1542/61 715/1526/61 707/1525/61
+f 711/1540/503 719/1539/503 716/1543/503 708/1544/503
+f 964/1545/504 974/1546/504 717/1530/504 709/1529/504
+f 710/1528/505 718/1527/505 720/1547/505 712/1548/505
+f 708/1549/506 716/1550/506 975/1551/506 965/1552/506
+f 738/1027/21 730/1030/21 729/1553/21 737/1554/21
+f 728/68/21 732/1032/21 733/1555/21 727/127/21
+f 729/1556/507 730/1036/507 735/1035/507 736/1557/507
+f 734/1558/508 736/1557/508 735/1035/508 731/1038/508
+f 732/1032/21 738/1027/21 737/1554/21 733/1555/21
+f 729/1559/509 736/1560/509 737/1561/509
+f 736/1560/510 734/131/510 733/1562/510 737/1561/510
+f 733/1562/511 734/131/511 727/130/511
+f 739/1563/1 740/1564/1 745/1045/1 746/1044/1
+f 740/1564/1 741/1565/1 744/1047/1 745/1045/1
+f 741/1565/1 742/1566/1 743/1049/1 744/1047/1
+f 753/1051/1 752/1567/1 748/1568/1 749/1052/1
+f 743/1049/1 742/1566/1 751/1569/1 750/1055/1
+f 750/1055/1 751/1569/1 752/1567/1 753/1051/1
+f 755/1570/383 762/1060/383 761/1059/383 756/1571/383
+f 756/1571/384 761/1059/384 759/1062/384 757/1572/384
+f 757/1572/21 759/1062/21 760/1064/21 758/1573/21
+f 767/1065/385 763/1068/385 764/1574/385 766/1575/385
+f 760/1064/386 765/1070/386 768/1576/386 758/1573/386
+f 765/1070/387 767/1065/387 766/1575/387 768/1576/387
+f 742/1577/512 758/1578/512 768/1579/512 751/1580/512
+f 739/1581/513 755/1582/513 756/1583/513 740/1584/513
+f 740/1584/514 756/1583/514 757/1585/514 741/1586/514
+f 741/1586/515 757/1585/515 758/1578/515 742/1577/515
+f 748/1587/516 764/1588/516 763/1083/516 749/1082/516
+f 751/1580/517 768/1579/517 766/1589/517 752/1590/517
+f 752/1590/518 766/1589/518 764/1591/518 748/1592/518
+f 747/1089/1 739/1563/1 746/1044/1
+f 754/1090/395 762/1092/395 755/1593/395
+f 747/1594/519 754/1595/519 755/1582/519 739/1581/519
+f 770/1596/134 778/1597/134 777/1598/134 769/1599/134
+f 774/1600/21 780/1601/21 778/1602/21 770/1603/21
+f 781/1604/520 777/1605/520 778/1606/520 780/1607/520
+f 775/1608/521 779/1609/521 782/1610/521 776/1611/521
+f 779/1609/522 781/1604/522 780/1607/522 782/1610/522
+f 726/126/21 776/132/21 782/1612/21 773/1613/21
+f 769/1599/523 777/1598/523 781/1614/523 771/1615/523
+f 772/1616/524 779/1617/524 775/121/524 725/120/524
+f 773/1613/21 782/1612/21 780/1601/21 774/1600/21
+f 771/1618/525 781/1619/525 779/1617/525 772/1616/525
+f 786/1620/403 784/1621/403 788/1622/403 789/1623/403
+f 783/1624/403 785/1625/403 790/1626/403 787/1627/403
+f 785/1625/403 786/1620/403 789/1623/403 790/1626/403
+f 791/1628/526 795/1629/526 796/1630/526 792/1631/526
+f 793/1632/527 797/1633/527 798/1634/527 794/1635/527
+f 794/1635/528 798/1634/528 795/1629/528 791/1628/528
+f 786/1636/407 791/1637/407 792/1638/407 784/1639/407
+f 789/1640/529 795/1641/529 798/1642/529 790/1643/529
+f 788/1644/530 796/1645/530 795/1641/530 789/1640/530
+f 783/1646/407 793/1647/407 794/1648/407 785/1649/407
+f 787/1650/61 797/1651/61 793/1652/61 783/1653/61
+f 790/1643/531 798/1642/531 797/1654/531 787/1655/531
+f 784/1656/134 792/1657/134 796/1658/134 788/1659/134
+f 785/1649/407 794/1648/407 791/1637/407 786/1636/407
+f 799/1660/411 800/1661/411 802/1662/411 801/1663/411
+f 801/1663/532 802/1662/532 804/1664/532 803/1665/532
+f 803/1665/119 804/1664/119 806/1666/119 805/1667/119
+f 808/1668/413 810/1669/413 809/1670/413 807/1671/413
+f 810/1669/533 812/1672/533 811/1673/533 809/1670/533
+f 812/1672/60 814/1674/60 813/1675/60 811/1673/60
+f 816/1676/415 810/1677/415 808/1678/415 815/1679/415
+f 800/1680/21 853/1681/21 858/1682/21 817/1683/21
+f 819/1684/534 811/1685/534 813/1686/534 818/1687/534
+f 818/1687/134 813/1686/134 814/1688/134 820/1689/134
+f 821/1690/535 812/1691/535 810/1677/535 816/1676/535
+f 817/1683/21 807/1692/21 809/1693/21 822/1694/21
+f 820/1689/536 814/1688/536 812/1695/536 821/1696/536
+f 822/1694/537 809/1693/537 811/1697/537 819/1698/537
+f 802/1699/537 822/1694/537 819/1698/537 804/1700/537
+f 805/1701/538 820/1689/538 821/1696/538 803/1702/538
+f 800/1680/21 817/1683/21 822/1694/21 802/1699/21
+f 803/1703/539 821/1690/539 816/1676/539 801/1704/539
+f 806/1705/134 818/1687/134 820/1689/134 805/1701/134
+f 804/1706/534 819/1684/534 818/1687/534 806/1705/534
+f 807/1671/540 855/1707/540 856/1708/540 808/1668/540
+f 801/1704/423 816/1676/423 815/1679/423 799/1709/423
+f 823/1710/411 824/1711/411 826/1712/411 825/1713/411
+f 825/1713/532 826/1712/532 828/1714/532 827/1715/532
+f 827/1715/119 828/1714/119 830/1716/119 829/1717/119
+f 832/1718/413 834/1719/413 833/1720/413 831/1721/413
+f 834/1719/533 836/1722/533 835/1723/533 833/1720/533
+f 836/1722/60 838/1724/60 837/1725/60 835/1723/60
+f 840/1726/415 834/1727/415 832/1728/415 839/1729/415
+f 824/1730/21 847/1731/21 852/1732/21 841/1733/21
+f 843/1734/534 835/1735/534 837/1736/534 842/1737/534
+f 842/1737/134 837/1736/134 838/1738/134 844/1739/134
+f 845/1740/535 836/1741/535 834/1727/535 840/1726/535
+f 841/1733/21 831/1742/21 833/1743/21 846/1744/21
+f 844/1739/536 838/1738/536 836/1745/536 845/1746/536
+f 846/1744/537 833/1743/537 835/1747/537 843/1748/537
+f 826/1749/537 846/1744/537 843/1748/537 828/1750/537
+f 829/1751/538 844/1739/538 845/1746/538 827/1752/538
+f 824/1730/21 841/1733/21 846/1744/21 826/1749/21
+f 827/1753/539 845/1740/539 840/1726/539 825/1754/539
+f 830/1755/134 842/1737/134 844/1739/134 829/1751/134
+f 828/1756/534 843/1734/534 842/1737/534 830/1755/534
+f 831/1721/541 849/1757/541 850/1758/541 832/1718/541
+f 825/1754/423 840/1726/423 839/1729/423 823/1759/423
+f 839/1729/542 851/1760/542 848/1761/542 823/1759/542
+f 823/1710/543 848/1762/543 847/1763/543 824/1711/543
+f 841/1733/21 852/1732/21 849/1764/21 831/1742/21
+f 832/1728/544 850/1765/544 851/1760/544 839/1729/544
+f 815/1679/545 857/1766/545 854/1767/545 799/1709/545
+f 799/1660/546 854/1768/546 853/1769/546 800/1661/546
+f 817/1683/21 858/1682/21 855/1770/21 807/1692/21
+f 808/1678/547 856/1771/547 857/1766/547 815/1679/547
+f 866/1772/437 870/1773/437 869/1774/437 865/1775/437
+f 862/1776/432 865/1777/432 863/1778/432 860/1779/432
+f 861/1780/435 866/1772/435 865/1775/435 862/1781/435
+f 859/1782/434 864/1783/434 866/1784/434 861/1785/434
+f 860/1786/433 863/1787/433 864/1788/433 859/1789/433
+f 868/1790/1 872/1791/1 874/1792/1 870/1793/1
+f 864/1783/436 868/1790/436 870/1793/436 866/1784/436
+f 863/1787/431 867/1794/431 868/1795/431 864/1788/431
+f 865/1777/438 869/1796/438 867/1797/438 863/1778/438
+f 873/1798/439 877/1799/439 875/1800/439 871/1801/439
+f 867/1794/61 871/1802/61 872/1803/61 868/1795/61
+f 869/1796/21 873/1798/21 871/1801/21 867/1797/21
+f 870/1773/134 874/1804/134 873/1805/134 869/1774/134
+f 874/1804/442 878/1806/442 877/1807/442 873/1805/442
+f 872/1791/441 876/1808/441 878/1809/441 874/1792/441
+f 871/1802/440 875/1810/440 876/1811/440 872/1803/440
+f 881/1812/548 887/1813/548 886/1814/548 880/1815/548
+f 723/1816/21 883/1817/21 884/1818/21 724/1819/21
+f 879/1319/549 885/1320/549 887/1813/549 881/1812/549
+f 724/1819/21 884/1818/21 882/1322/21 722/1321/21
+f 880/1820/550 886/1821/550 883/1822/550 723/1823/550
+f 884/1824/60 887/1825/60 885/1329/60 882/1328/60
+f 883/1826/60 886/1827/60 887/1825/60 884/1824/60
+f 888/1333/551 724/1828/551 889/1829/551 890/1334/551
+f 895/1830/61 893/1831/61 894/1832/61 896/1833/61
+f 891/1834/61 895/1830/61 896/1833/61 892/1835/61
+f 894/1832/61 897/1836/61 898/1837/61 896/1833/61
+f 899/1337/552 901/1340/552 902/1838/552 900/1839/552
+f 903/1341/553 904/1840/553 906/1841/553 905/1342/553
+f 902/1842/554 901/1348/554 905/1347/554 906/1843/554
+f 900/1844/555 902/1845/555 906/1846/555 904/1847/555
+f 899/1353/556 900/1848/556 904/1849/556 903/1354/556
+f 889/1850/557 724/1851/557 907/1852/557 908/1853/557
+f 890/1361/558 889/1854/558 908/1855/558 909/1362/558
+f 908/1856/559 907/1857/559 910/1858/559 911/1859/559
+f 909/1369/560 908/1856/560 911/1859/560 912/1370/560
+f 911/1860/561 910/1861/561 913/1862/561 914/1863/561
+f 912/1375/562 911/1864/562 914/1865/562 915/1376/562
+f 724/1866/563 913/1862/563 910/1861/563 907/1867/563
+f 913/1868/564 724/1869/564 889/1870/564 914/1871/564
+f 914/1871/565 889/1870/565 890/1386/565 915/1385/565
+f 773/1872/461 725/1873/461 726/1874/461
+f 725/1873/566 773/1872/566 772/1875/566
+f 773/1872/567 771/1876/567 772/1875/567
+f 769/1877/568 771/1876/568 774/1878/568
+f 774/1878/465 771/1876/465 773/1872/465
+f 769/1877/466 774/1878/466 770/1879/466
+f 916/1880/569 917/1881/569 919/1882/569 918/1883/569
+f 921/1884/468 923/1885/468 922/1886/468 920/1887/468
+f 916/1888/570 921/1889/570 920/1890/570 917/1891/570
+f 917/1892/571 920/1893/571 922/1894/571 919/1895/571
+f 918/1896/572 923/1897/572 921/1898/572 916/1899/572
+f 924/1900/573 925/1901/573 927/1902/573 926/1903/573
+f 929/1904/574 931/1905/574 930/1906/574 928/1907/574
+f 924/1908/575 929/1909/575 928/1910/575 925/1911/575
+f 925/1912/576 928/1913/576 930/1914/576 927/1915/576
+f 926/1916/577 931/1917/577 929/1909/577 924/1908/577
+f 934/1918/578 938/1919/578 939/1920/578 935/1921/578
+f 932/1922/579 936/1923/579 937/1924/579 933/1925/579
+f 935/1926/580 939/1927/580 936/1928/580 932/1929/580
+f 933/1930/581 937/1931/581 938/1932/581 934/1933/581
+f 942/1934/582 946/1935/582 947/1936/582 943/1937/582
+f 943/1938/583 947/1939/583 944/1940/583 940/1941/583
+f 941/1942/584 945/1943/584 946/1944/584 942/1945/584
+f 940/1946/585 944/1947/585 945/1948/585 941/1949/585
+f 949/1950/156 948/1951/156 950/1952/156 951/1953/156
+f 952/1954/586 956/1955/586 957/1956/586 953/1957/586
+f 948/1958/587 953/1959/587 955/1960/587 950/1961/587
+f 949/1962/588 952/1954/588 953/1957/588 948/1963/588
+f 951/1964/589 954/1965/589 952/1966/589 949/1967/589
+f 950/1968/590 955/1969/590 954/1965/590 951/1964/590
+f 958/1970/591 962/1971/591 960/1972/591 956/1973/591
+f 954/1974/592 958/1970/592 956/1973/592 952/1975/592
+f 955/1969/593 959/1976/593 958/1977/593 954/1965/593
+f 953/1978/594 957/1979/594 959/1980/594 955/1981/594
+f 960/1982/314 962/1983/314 963/1984/314 961/1985/314
+f 959/1976/595 963/1986/595 962/1987/595 958/1977/595
+f 957/1979/596 961/1985/596 963/1984/596 959/1980/596
+f 956/1955/597 960/1988/597 961/1989/597 957/1956/597
+f 968/1990/497 967/1991/497 966/1992/497 969/1993/497
+f 965/1509/497 968/1990/497 969/1993/497 964/1510/497
+f 968/1990/497 971/1994/497 970/1995/497 967/1991/497
+f 971/1994/497 973/1996/497 972/1997/497 970/1995/497
+f 978/1998/498 979/1999/498 976/2000/498 977/2001/498
+f 975/1519/498 974/1520/498 979/1999/498 978/1998/498
+f 978/1998/498 977/2001/498 980/2002/498 981/2003/498
+f 981/2003/498 980/2002/498 982/2004/498 983/2005/498
+f 967/2006/598 970/2007/598 980/2008/598 977/2009/598
+f 969/2010/599 966/2011/599 976/2012/599 979/2013/599
+f 972/2014/600 973/2015/600 983/2016/600 982/2017/600
+f 965/2018/61 964/2019/61 974/2020/61 975/2021/61
+f 973/2022/601 971/2023/601 981/2024/601 983/2025/601
+f 966/2026/134 967/2006/134 977/2009/134 976/2027/134
+f 971/2023/602 968/2028/602 978/2029/602 981/2024/602
+f 964/1545/603 969/2010/603 979/2013/603 974/1546/603
+f 970/2007/604 972/2030/604 982/2031/604 980/2008/604
+f 968/2032/605 965/1552/605 975/1551/605 978/2033/605
+f 618/1270/606 619/1266/606 989/2034/606 987/2035/606
+f 619/1266/607 616/1265/607 988/2036/607 989/2034/607
+f 616/2037/608 615/2038/608 985/2039/608 988/2040/608
+f 617/2041/609 618/2042/609 987/2043/609 986/2044/609
+f 615/1182/21 620/1181/21 984/2045/21 985/2046/21
+f 620/1181/21 617/1269/21 986/2047/21 984/2045/21
+f 856/1771/610 993/2048/610 995/2049/610 857/1766/610
+f 857/1766/611 995/2049/611 994/2050/611 854/1767/611
+f 854/2051/612 994/2052/612 991/2053/612 853/2054/612
+f 855/2055/613 992/2056/613 993/2057/613 856/2058/613
+f 853/1681/21 991/2059/21 990/2060/21 858/1682/21
+f 858/1682/21 990/2060/21 992/2061/21 855/1770/21

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 225
public/assets/plane.dae


+ 198 - 0
public/assets/test.dae

@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="utf-8"?>
+<COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <asset>
+    <contributor>
+      <author>Blender User</author>
+      <authoring_tool>Blender 2.79.0 commit date:2017-09-11, commit time:10:43, hash:5bd8ac9</authoring_tool>
+    </contributor>
+    <created>2017-12-24T02:03:31</created>
+    <modified>2017-12-24T02:03:31</modified>
+    <unit name="meter" meter="1"/>
+    <up_axis>Z_UP</up_axis>
+  </asset>
+  <library_cameras>
+    <camera id="Camera-camera" name="Camera">
+      <optics>
+        <technique_common>
+          <perspective>
+            <xfov sid="xfov">49.13434</xfov>
+            <aspect_ratio>1.777778</aspect_ratio>
+            <znear sid="znear">0.1</znear>
+            <zfar sid="zfar">100</zfar>
+          </perspective>
+        </technique_common>
+      </optics>
+      <extra>
+        <technique profile="blender">
+          <shiftx sid="shiftx" type="float">0</shiftx>
+          <shifty sid="shifty" type="float">0</shifty>
+          <YF_dofdist sid="YF_dofdist" type="float">0</YF_dofdist>
+        </technique>
+      </extra>
+    </camera>
+  </library_cameras>
+  <library_lights>
+    <light id="Lamp-light" name="Lamp">
+      <technique_common>
+        <point>
+          <color sid="color">1 1 1</color>
+          <constant_attenuation>1</constant_attenuation>
+          <linear_attenuation>0</linear_attenuation>
+          <quadratic_attenuation>0.00111109</quadratic_attenuation>
+        </point>
+      </technique_common>
+      <extra>
+        <technique profile="blender">
+          <type sid="type" type="int">0</type>
+          <flag sid="flag" type="int">0</flag>
+          <mode sid="mode" type="int">8192</mode>
+          <gamma sid="blender_gamma" type="float">1</gamma>
+          <red sid="red" type="float">1</red>
+          <green sid="green" type="float">1</green>
+          <blue sid="blue" type="float">1</blue>
+          <shadow_r sid="blender_shadow_r" type="float">0</shadow_r>
+          <shadow_g sid="blender_shadow_g" type="float">0</shadow_g>
+          <shadow_b sid="blender_shadow_b" type="float">0</shadow_b>
+          <energy sid="blender_energy" type="float">1</energy>
+          <dist sid="blender_dist" type="float">29.99998</dist>
+          <spotsize sid="spotsize" type="float">75</spotsize>
+          <spotblend sid="spotblend" type="float">0.15</spotblend>
+          <halo_intensity sid="blnder_halo_intensity" type="float">1</halo_intensity>
+          <att1 sid="att1" type="float">0</att1>
+          <att2 sid="att2" type="float">1</att2>
+          <falloff_type sid="falloff_type" type="int">2</falloff_type>
+          <clipsta sid="clipsta" type="float">1.000799</clipsta>
+          <clipend sid="clipend" type="float">30.002</clipend>
+          <bias sid="bias" type="float">1</bias>
+          <soft sid="soft" type="float">3</soft>
+          <compressthresh sid="compressthresh" type="float">0.04999995</compressthresh>
+          <bufsize sid="bufsize" type="int">2880</bufsize>
+          <samp sid="samp" type="int">3</samp>
+          <buffers sid="buffers" type="int">1</buffers>
+          <filtertype sid="filtertype" type="int">0</filtertype>
+          <bufflag sid="bufflag" type="int">0</bufflag>
+          <buftype sid="buftype" type="int">2</buftype>
+          <ray_samp sid="ray_samp" type="int">1</ray_samp>
+          <ray_sampy sid="ray_sampy" type="int">1</ray_sampy>
+          <ray_sampz sid="ray_sampz" type="int">1</ray_sampz>
+          <ray_samp_type sid="ray_samp_type" type="int">0</ray_samp_type>
+          <area_shape sid="area_shape" type="int">1</area_shape>
+          <area_size sid="area_size" type="float">0.1</area_size>
+          <area_sizey sid="area_sizey" type="float">0.1</area_sizey>
+          <area_sizez sid="area_sizez" type="float">1</area_sizez>
+          <adapt_thresh sid="adapt_thresh" type="float">0.000999987</adapt_thresh>
+          <ray_samp_method sid="ray_samp_method" type="int">1</ray_samp_method>
+          <shadhalostep sid="shadhalostep" type="int">0</shadhalostep>
+          <sun_effect_type sid="sun_effect_type" type="int">0</sun_effect_type>
+          <skyblendtype sid="skyblendtype" type="int">1</skyblendtype>
+          <horizon_brightness sid="horizon_brightness" type="float">1</horizon_brightness>
+          <spread sid="spread" type="float">1</spread>
+          <sun_brightness sid="sun_brightness" type="float">1</sun_brightness>
+          <sun_size sid="sun_size" type="float">1</sun_size>
+          <backscattered_light sid="backscattered_light" type="float">1</backscattered_light>
+          <sun_intensity sid="sun_intensity" type="float">1</sun_intensity>
+          <atm_turbidity sid="atm_turbidity" type="float">2</atm_turbidity>
+          <atm_extinction_factor sid="atm_extinction_factor" type="float">1</atm_extinction_factor>
+          <atm_distance_factor sid="atm_distance_factor" type="float">1</atm_distance_factor>
+          <skyblendfac sid="skyblendfac" type="float">1</skyblendfac>
+          <sky_exposure sid="sky_exposure" type="float">1</sky_exposure>
+          <sky_colorspace sid="sky_colorspace" type="int">0</sky_colorspace>
+        </technique>
+      </extra>
+    </light>
+  </library_lights>
+  <library_images/>
+  <library_effects>
+    <effect id="Material-effect">
+      <profile_COMMON>
+        <technique sid="common">
+          <phong>
+            <emission>
+              <color sid="emission">0 0 0 1</color>
+            </emission>
+            <ambient>
+              <color sid="ambient">0 0 0 1</color>
+            </ambient>
+            <diffuse>
+              <color sid="diffuse">0.64 0.64 0.64 1</color>
+            </diffuse>
+            <specular>
+              <color sid="specular">0.5 0.5 0.5 1</color>
+            </specular>
+            <shininess>
+              <float sid="shininess">50</float>
+            </shininess>
+            <index_of_refraction>
+              <float sid="index_of_refraction">1</float>
+            </index_of_refraction>
+          </phong>
+        </technique>
+      </profile_COMMON>
+    </effect>
+  </library_effects>
+  <library_materials>
+    <material id="Material-material" name="Material">
+      <instance_effect url="#Material-effect"/>
+    </material>
+  </library_materials>
+  <library_geometries>
+    <geometry id="Cube-mesh" name="Cube">
+      <mesh>
+        <source id="Cube-mesh-positions">
+          <float_array id="Cube-mesh-positions-array" count="24">1 1 -1 1 -1 -1 -1 -0.9999998 -1 -0.9999997 1 -1 1 0.9999995 1 0.9999994 -1.000001 1 -1 -0.9999997 1 -1 1 1</float_array>
+          <technique_common>
+            <accessor source="#Cube-mesh-positions-array" count="8" stride="3">
+              <param name="X" type="float"/>
+              <param name="Y" type="float"/>
+              <param name="Z" type="float"/>
+            </accessor>
+          </technique_common>
+        </source>
+        <source id="Cube-mesh-normals">
+          <float_array id="Cube-mesh-normals-array" count="36">0 0 -1 0 0 1 1 0 -2.38419e-7 0 -1 -4.76837e-7 -1 2.38419e-7 -1.49012e-7 2.68221e-7 1 2.38419e-7 0 0 -1 0 0 1 1 -5.96046e-7 3.27825e-7 -4.76837e-7 -1 0 -1 2.38419e-7 -1.19209e-7 2.08616e-7 1 0</float_array>
+          <technique_common>
+            <accessor source="#Cube-mesh-normals-array" count="12" stride="3">
+              <param name="X" type="float"/>
+              <param name="Y" type="float"/>
+              <param name="Z" type="float"/>
+            </accessor>
+          </technique_common>
+        </source>
+        <vertices id="Cube-mesh-vertices">
+          <input semantic="POSITION" source="#Cube-mesh-positions"/>
+        </vertices>
+        <triangles material="Material-material" count="12">
+          <input semantic="VERTEX" source="#Cube-mesh-vertices" offset="0"/>
+          <input semantic="NORMAL" source="#Cube-mesh-normals" offset="1"/>
+          <p>0 0 2 0 3 0 7 1 5 1 4 1 4 2 1 2 0 2 5 3 2 3 1 3 2 4 7 4 3 4 0 5 7 5 4 5 0 6 1 6 2 6 7 7 6 7 5 7 4 8 5 8 1 8 5 9 6 9 2 9 2 10 6 10 7 10 0 11 3 11 7 11</p>
+        </triangles>
+      </mesh>
+    </geometry>
+  </library_geometries>
+  <library_controllers/>
+  <library_visual_scenes>
+    <visual_scene id="Scene" name="Scene">
+      <node id="Camera" name="Camera" type="NODE">
+        <matrix sid="transform">0.6859207 -0.3240135 0.6515582 7.481132 0.7276763 0.3054208 -0.6141704 -6.50764 0 0.8953956 0.4452714 5.343665 0 0 0 1</matrix>
+        <instance_camera url="#Camera-camera"/>
+      </node>
+      <node id="Lamp" name="Lamp" type="NODE">
+        <matrix sid="transform">-0.2908646 -0.7711008 0.5663932 4.076245 0.9551712 -0.1998834 0.2183912 1.005454 -0.05518906 0.6045247 0.7946723 5.903862 0 0 0 1</matrix>
+        <instance_light url="#Lamp-light"/>
+      </node>
+      <node id="Cube" name="Cube" type="NODE">
+        <matrix sid="transform">1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1</matrix>
+        <instance_geometry url="#Cube-mesh" name="Cube">
+          <bind_material>
+            <technique_common>
+              <instance_material symbol="Material-material" target="#Material-material"/>
+            </technique_common>
+          </bind_material>
+        </instance_geometry>
+      </node>
+    </visual_scene>
+  </library_visual_scenes>
+  <scene>
+    <instance_visual_scene url="#Scene"/>
+  </scene>
+</COLLADA>

+ 15 - 0
public/gearvr/appui.js

@@ -0,0 +1,15 @@
+//-----App ui-----
+
+function createApp() {
+    
+        let self = this
+    
+        return {
+            $cell: true,
+            $type: "div",
+            class: "propGrid max-width mdc-layout-grid mdc-layout-grid--align-left",
+            $components: []
+        }
+
+    }
+    

+ 0 - 4
public/gearvr/assets.json

@@ -6,10 +6,6 @@
     "sky":{
         "tag": "img",
         "src": "/../assets/skyes/sky3.jpg"
-    },
-    "plane":{
-        "tag": "a-asset-item",
-        "src": "/../assets/plane.dae"
     },
      "bg2":{
         "tag": "img",

+ 1 - 1
public/gearvr/index.vwf.config.yaml

@@ -4,5 +4,5 @@ info:
 model:
   vwf/model/aframe:
 view:
-  vwf/view/aframe:
+  vwf/view/aframe: {gearvr: true, wmrleft: true, wmrright: true}
   vwf/view/editor-new:

+ 18 - 109
public/gearvr/index.vwf.yaml

@@ -3,7 +3,7 @@
 ---
 extends: http://vwf.example.com/aframe/ascene.vwf
 properties:
-  fog: "type: linear; color: #ECECEC; far: 30; near: 0"
+  fog: "type: linear; color: #ECECEC; far: 20; near: 0"
   assets: "assets.json"
 children:
   myLight:
@@ -13,123 +13,32 @@ children:
       color: "white"
       position: "0 10 5"
       rotation: "0 0 0"
-  model:
-    extends: http://vwf.example.com/aframe/acolladamodel.vwf
+  groundPlane:
+    extends: http://vwf.example.com/aframe/aplane.vwf
     properties:
-      src: "#plane"
-      position: "-1.0 1.7 -3"
-      rotation: "0 -45 0"
-      scale: "10 10 10"
+        height: 50
+        width: 50
+        repeat: "10 10"
+        rotation: [-90, 0, 0]
+        wireframe: false
+        src: "#bg2"
   spaceText:
     extends: http://vwf.example.com/aframe/atext.vwf
     properties:
-      value: "Virtual World Framework & A-Frame"
-      color: "#ddd"
-      position: "-2 2.5 -2"
-  spaceText2:
-    extends: http://vwf.example.com/aframe/atext.vwf
-    properties:
-      value: "Project by Krestianstvo.org"
-      color: "#aaa"
-      position: "1 3 -4"
-  boxAnim:
+      value: "Virtual World Framework & VR Controllers"
+      color: "#2b5d83"
+      position: [-2, 2.5, -3]
+  cube:
     extends: http://vwf.example.com/aframe/abox.vwf
     properties:
-      position: "0 0 -3"
+      position: "0 1 -3"
       rotation: "0 0 0"
       color: "#3c7249"
-      depth: 2
-      height: 1
-      width: 1
-  box:
-    extends: http://vwf.example.com/aframe/abox.vwf
-    properties:
-      position: "-1 0.5 -3"
-      rotation: "0 -30 0"
-      color: "#3c7249"
-      depth: 2
+      depth: 1
       height: 1
       width: 1
-      clickable: true
-    events:
-      clickEvent:
-    methods:
-      clickEventMethod:
-        body: |
-          if (this.clickable) {
-          let genColor = this.generateColor();
-          this.color = genColor 
-          }
-      generateColor:
-        body: |
-          var letters = '0123456789ABCDEF';
-          var color = '#';
-          for (var i = 0; i < 6; i++) {
-          color += letters[Math.floor(this.random() * 16)];
-          } return color 
-    scripts:
-      - |
-        this.clickEvent = function(){
-          this.clickEventMethod();
-         }
-  controller:
+  newSky:
     extends: http://vwf.example.com/aframe/aentity.vwf
-    properties:
-      position: "0 0 -1"
-      color: "green"
-      depth: 1
     children:
-      gearvr-controls:
-        extends: http://vwf.example.com/aframe/gearvr-controlsComponent.vwf
-  sphere:
-    extends: http://vwf.example.com/aframe/asphere.vwf
-    properties:
-      position: "1 1.25 -4"
-      color: "#e0e014"
-      radius: 1
-      wireframe: true
-    children:
-      box2:
-        extends: http://vwf.example.com/aframe/abox.vwf
-        properties:
-          src: "#bg"
-          position: "2 -0.75 0"
-          color: "#2167a5"
-          depth: 1
-        children:
-          interpolation:
-            extends: http://vwf.example.com/aframe/interpolation-component.vwf
-            properties:
-              enabled: true
-              duration: 50
-              deltaPos: 0.1
-              deltaRot: 1
-        methods:
-          run:
-            body: |
-              var time = vwf.now;
-              let pos = AFRAME.utils.coordinates.parse(this.position);
-              this.position = [pos.x, pos.y, Math.sin(time)]
-              this.future( 0.01 ).run();  // schedule the next step
-  sky:
-    extends: http://vwf.example.com/aframe/asky.vwf
-    properties:
-      color: "#ECECEC"
-      src: "#sky"
-      fog: false
-  groundPlane:
-    extends: http://vwf.example.com/aframe/aplane.vwf
-    properties:
-      height: 50
-      width: 50
-      repeat: "10 10"
-      rotation: "-90 0 0"
-      color: "white"
-      wireframe: false
-      src: "#bg2"
-methods:
-  initialize:
-    body: |
-      var runBox = vwf_view.kernel.find("", "/sphere/box2")[0];
-      console.log(runBox);
-      vwf_view.kernel.callMethod(runBox, "run");
+      skyshader:
+        extends: http://vwf.example.com/aframe/app-skyshader-component.vwf

BIN
public/gearvr/webimg.jpg


+ 2 - 1
public/ohmlang-calc/index.vwf.yaml

@@ -131,4 +131,5 @@ children:
   sky:
     extends: http://vwf.example.com/aframe/asky.vwf
     properties:
-      color: "#ECECEC"
+      color: "#ECECEC"
+      side: "back"

+ 2 - 2
public/ohmlang-lsys/index.vwf.yaml

@@ -173,12 +173,12 @@ children:
                 parameters:
                     - step
                 body: |
-                    let pos = AFRAME.utils.coordinates.parse(this.drawNode.position);
+                    let pos = Object.assign({}, this.drawNode.position);
                     var x0 = pos.x;
                     var y0 = pos.y;
                     var xx = Math.sin(this.angleInRadians);
                     var yy = Math.cos(this.angleInRadians);
-                    let startPosition = AFRAME.utils.coordinates.parse(this.drawNode.position);
+                    let startPosition = Object.assign({}, this.drawNode.position);
                     let endPosition = {x: x0 + step * xx, y: y0 + step * yy, z: pos.z};
                     var drawPath = this.drawNode.linepath.path;
                     drawPath.push(startPosition);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 8200
public/web/lib/socketio/socket.io.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
public/web/lib/socketio/socket.io.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
public/web/lib/socketio/socket.io.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 6052
public/web/lib/socketio/socket.io.slim.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
public/web/lib/socketio/socket.io.slim.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
public/web/lib/socketio/socket.io.slim.min.js


+ 6 - 0
public/webapps.json

@@ -37,5 +37,11 @@
         "imgUrl": "./webrtc/webimg.jpg",
         "text": "Audio and video streaming for Avatars",
         "featured": false
+    },
+    "gearvr":{
+        "title":"Control in VR app",
+        "imgUrl": "./gearvr/webimg.jpg",
+        "text": "VR controler example",
+        "featured": false
     }
 }

+ 1 - 1
public/webrtc/index.vwf.config.yaml

@@ -6,4 +6,4 @@ model:
 view:
   vwf/view/aframe:
   vwf/view/editor-new:
-  vwf/view/webrtc: { debug: false, videoProperties: { create: false }, iceServers: [ { url: 'stun:stun.l.google.com:19302' }, { url: 'stun:stun.sipgate.net' }, { url: 'stun:217.10.68.152' }, { url: 'stun:stun.sipgate.net:10000' }, { url: 'stun:217.10.68.152:10000' }, { url: 'stun:23.21.150.121:3478' }, { url: 'stun:216.93.246.18:3478' }, { url: 'stun:66.228.45.110:3478' }, { url: 'stun:173.194.78.127:19302' }, { url: 'stun:74.125.142.127:19302' }, { url: 'stun:provserver.televolution.net' }, { url: 'stun:sip1.lakedestiny.cordiaip.com' }, { url: 'stun:stun1.voiceeclipse.net' }, { url: 'stun:stun01.sipphone.com' }, { url: 'stun:stun.callwithus.com' }, { url: 'stun:stun.counterpath.net' }, { url: 'stun:stun.endigovoip.com' } ] }
+  vwf/view/webrtc:

+ 1 - 0
public/webrtc/index.vwf.yaml

@@ -29,6 +29,7 @@ children:
       color: "#ECECEC"
       src: "#sky"
       fog: false
+      side: "back"
   spaceText:
     extends: http://vwf.example.com/aframe/atext.vwf
     properties:

+ 1 - 1
support/client/lib/index.html

@@ -31,7 +31,7 @@
 
     <script type="text/javascript" src="socket.io/socket.io.js"></script>
     <!-- <script type="text/javascript" src="socket.io-sessionid-patch.js"></script> -->
-    <script src="vwf/view/webrtc/adapter-latest.js"></script>
+    <script src="vwf/view/webrtc/dist/adapter-latest.js"></script>
 
 
     <script type="text/javascript" src="vwf/view/lib/cell.js"></script>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 8200
support/client/lib/socket.io/socket.io.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/socket.io/socket.io.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/socket.io/socket.io.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 6052
support/client/lib/socket.io/socket.io.slim.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/socket.io/socket.io.slim.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/socket.io/socket.io.slim.min.js


+ 5 - 5
support/client/lib/vwf.js

@@ -369,14 +369,14 @@
 
                 { library: "vwf/view/webrtc", 
              //       linkedLibraries: ["vwf/view/webrtc/adapter"],  
-                    active: false 
+                    active: true 
                 },
 
                 { library: "vwf/view/ohm", active: true },
                 { library: "vwf/view/osc", active: true },
 
                 
-                 { library: "vwf/view/aframe", active: false },
+                 { library: "vwf/view/aframe", active: true },
                 { library: "vwf/model/aframe/aframe-master", active: false },
                 { library: "vwf/model/aframe/extras/aframe-extras.loaders", active: false },
                 { library: "vwf/model/aframe/addon/aframe-interpolation", active: false },
@@ -410,7 +410,7 @@
                      { library: "vwf/model/ohm", active: true },
                      { library: "vwf/model/osc", active: true },
                   
-                     { library: "vwf/model/aframe", active: false },
+                     { library: "vwf/model/aframe", active: true },
                      { library: "vwf/model/aframeComponent", active: true },
 
                     { library: "vwf/model/object", active: true }
@@ -422,10 +422,10 @@
                      { library: "vwf/view/ohm", active: true },
                      { library: "vwf/view/osc", active: true },
                      
-                      { library: "vwf/view/aframe", active: false },
+                      { library: "vwf/view/aframe", active: true },
                       { library: "vwf/view/aframeComponent", active: true },
 
-                    { library: "vwf/view/webrtc", active: false}
+                    { library: "vwf/view/webrtc", active: true}
 
                   
                 ]

+ 181 - 23
support/client/lib/vwf/model/aframe.js

@@ -78,7 +78,7 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                     return found;
                 },
                 setAFrameProperty: function (propertyName, propertyValue, aframeObject) {
-                    
+                    //console.log(propertyValue);
                             if (propertyValue.hasOwnProperty('x')) {
                                 aframeObject.setAttribute(propertyName, propertyValue)
                             } else
@@ -345,10 +345,13 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                             aframeObject.setAttribute('repeat', propertyValue);
                             break;
 
-
-                        case "look-controls-enabled":
-                            aframeObject.setAttribute('look-controls', 'enabled', propertyValue);
+                        case "side":
+                            aframeObject.setAttribute('material', {'side': propertyValue});
                             break;
+
+                        // case "look-controls-enabled":
+                        //     aframeObject.setAttribute('look-controls', 'enabled', propertyValue);
+                        //     break;
                         case "wasd-controls":
                             aframeObject.setAttribute('wasd-controls', 'enabled', propertyValue);
                             break;
@@ -360,6 +363,29 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
 
                 }
 
+                if (value === undefined && aframeObject.nodeName == "A-SKY") {
+                    value = propertyValue;
+
+                    switch (propertyName) {
+
+                        case "color":
+                            aframeObject.setAttribute('color',propertyValue);
+                            break;
+
+                        case "side":
+                            aframeObject.setAttribute('side',propertyValue);
+                            break;
+
+                        case "src":
+                            aframeObject.setAttribute('src',propertyValue);
+                            break;
+
+                        default:
+                            value = undefined;
+                            break;
+                    }
+                }
+
                 if (value === undefined && aframeObject.nodeName == "A-TEXT") {
                     value = propertyValue;
 
@@ -388,6 +414,14 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
 
                     switch (propertyName) {
 
+                        case "color":
+                        aframeObject.setAttribute('background', {'color': propertyValue} );
+                        break;
+
+                        case "transparent":
+                        aframeObject.setAttribute('background', {'transparent': propertyValue} );
+                        break;
+
                         case "fog":
                             aframeObject.setAttribute('fog', propertyValue);
                             break;
@@ -501,6 +535,48 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                     }
                 }
 
+                if (value === undefined && aframeObject.nodeName == "A-OBJ-MODEL") {
+                    value = propertyValue;
+
+                    switch (propertyName) {
+
+                        case "src":
+                            aframeObject.setAttribute('src', propertyValue);
+                            break;
+
+                        case "mtl":
+                            aframeObject.setAttribute('mtl', propertyValue);
+                            break;
+
+                        default:
+                            value = undefined;
+                            break;
+                    }
+                }
+
+                if (value === undefined && aframeObject.nodeName == "A-SOUND") {
+                    value = propertyValue;
+
+                    switch (propertyName) {
+
+                        case "src":
+                            aframeObject.setAttribute('src', propertyValue);
+                            break;
+
+                        case "on":
+                            aframeObject.setAttribute('on', propertyValue);
+                            break;
+
+                        case "autoplay":
+                            aframeObject.setAttribute('autoplay', propertyValue);
+                            break;
+
+                        default:
+                            value = undefined;
+                            break;
+                    }
+                }
+
                 if (value === undefined && aframeObject.nodeName == "A-PLANE") {
                     value = propertyValue;
 
@@ -579,8 +655,16 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                     value = propertyValue;
                     switch (propertyName) {
 
-                        case "userHeight":
-                            aframeObject.setAttribute('camera', 'userHeight', propertyValue);
+                        case "user-height":
+                            aframeObject.setAttribute('user-height', propertyValue);
+                            break;
+
+                            case "look-controls-enabled":
+                            aframeObject.setAttribute('look-controls-enabled', propertyValue);
+                            break;
+
+                            case "wasd-controls-enabled":
+                            aframeObject.setAttribute('wasd-controls-enabled', propertyValue);
                             break;
 
                         // case "active":
@@ -674,6 +758,12 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                             value = aframeObject.getAttribute('color');
                             break;
 
+                        case "side":
+                        if (aframeObject.getAttribute('material')) {
+                            value = aframeObject.getAttribute('material').side;
+                        }
+                            break;
+
                         case "fog":
                             if (aframeObject.getAttribute('material')) {
                                 value = aframeObject.getAttribute('material').fog;
@@ -710,18 +800,18 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                         case "repeat":
                             value = aframeObject.getAttribute('repeat');
 
-                        case "look-controls-enabled":
-                            var look = aframeObject.getAttribute('look-controls-enabled');
-                            if (look !== null && look !== undefined) {
-                                value = aframeObject.getAttribute('look-controls').enabled;
-                            }
-                            break;
-                        case "wasd-controls":
-                            var wasd = aframeObject.getAttribute('wasd-controls');
-                            if (wasd !== null && wasd !== undefined) {
-                                value = aframeObject.getAttribute('wasd-controls').enabled;
-                            }
-                            break;
+                        // case "look-controls-enabled":
+                        //     var look = aframeObject.getAttribute('look-controls-enabled');
+                        //     if (look !== null && look !== undefined) {
+                        //         value = aframeObject.getAttribute('look-controls').enabled;
+                        //     }
+                        //     break;
+                        // case "wasd-controls":
+                        //     var wasd = aframeObject.getAttribute('wasd-controls');
+                        //     if (wasd !== null && wasd !== undefined) {
+                        //         value = aframeObject.getAttribute('wasd-controls').enabled;
+                        //     }
+                        //     break;
 
                         case "visible":
                             value = aframeObject.getAttribute('visible');
@@ -736,9 +826,36 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                         case "fog":
                             value = aframeObject.getAttribute('fog');
                             break;
+                        
+                        case "color":
+                        if (aframeObject.getAttribute('background')) {
+                            value = aframeObject.getAttribute('background').color;
+                        }
+                            break;
+
+                        case "transparent":
+                        if (aframeObject.getAttribute('background')) {
+                            value = aframeObject.getAttribute('background').transparent;
+                        }
+                            break;
                     }
                 }
 
+                if (value === undefined && aframeObject.nodeName == "A-SKY") {
+                    
+                                        switch (propertyName) {
+                                            case "color":
+                                                value = aframeObject.getAttribute('color');
+                                                break;
+                                            case "side":
+                                                value = aframeObject.getAttribute('side');
+                                                break;
+                                            case "src":
+                                                value = aframeObject.getAttribute('src');
+                                                break;
+                                        }
+                                    }
+
                 if (value === undefined && aframeObject.nodeName == "A-BOX") {
 
                     switch (propertyName) {
@@ -855,9 +972,17 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
 
 
                     switch (propertyName) {
-                        case "userHeight":
-                            value = aframeObject.getAttribute('camera').userHeight;
+                        case "user-height":
+                            value = aframeObject.getAttribute('user-height');
+                            break;
+                        case "look-controls-enabled":
+                            value = aframeObject.getAttribute('look-controls-enabled');
+                            break;
+
+                        case "wasd-controls-enabled":
+                            value = aframeObject.getAttribute('wasd-controls-enabled');
                             break;
+
                     }
 
                     //    switch (propertyName) {
@@ -892,6 +1017,33 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                     }
                 }
 
+                if (value === undefined && aframeObject.nodeName == "A-OBJ-MODEL") {
+
+                    switch (propertyName) {
+                        case "src":
+                            value = aframeObject.getAttribute('src');
+                            break;
+                        case "mtl":
+                            value = aframeObject.getAttribute('mtl');
+                            break;
+                    }
+                }
+
+                if (value === undefined && aframeObject.nodeName == "A-SOUND") {
+
+                    switch (propertyName) {
+                        case "src":
+                            value = aframeObject.getAttribute('src');
+                            break;
+                        case "on":
+                            value = aframeObject.getAttribute('on');
+                            break;
+                        case "autoplay":
+                            value = aframeObject.getAttribute('autoplay');
+                            break;
+                    }
+                }
+
                 if (value === undefined && aframeObject.nodeName == "A-GLTF-MODEL") {
                     
                                         switch (propertyName) {
@@ -920,9 +1072,13 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
 
         if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/ascene.vwf")) {
             aframeObj = document.createElement('a-scene');
+            let assetsElement = document.createElement('a-assets');
+            aframeObj.appendChild(assetsElement);
 
             self.state.scenes[node.ID] = aframeObj;
 
+        } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/asky.vwf")) {
+            aframeObj = document.createElement('a-sky');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/acamera.vwf")) {
             aframeObj = document.createElement('a-camera');
             aframeObj.setAttribute('camera', 'active', false);
@@ -931,8 +1087,6 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
             aframeObj = document.createElement('a-light');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/acursor.vwf")) {
             aframeObj = document.createElement('a-cursor');
-        } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/asky.vwf")) {
-            aframeObj = document.createElement('a-sky');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/a-sun-sky.vwf")) {
                 aframeObj = document.createElement('a-sun-sky');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/abox.vwf")) {
@@ -943,7 +1097,11 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
             aframeObj = document.createElement('a-text');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/acolladamodel.vwf")) {
             aframeObj = document.createElement('a-collada-model');
-        } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/agltfmodel.vwf")) {
+        } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/aobjmodel.vwf")) {
+            aframeObj = document.createElement('a-obj-model');
+        } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/asound.vwf")) {
+            aframeObj = document.createElement('a-sound');
+         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/agltfmodel.vwf")) {
             aframeObj = document.createElement('a-gltf-model');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/asphere.vwf")) {
             aframeObj = document.createElement('a-sphere');

+ 130 - 4
support/client/lib/vwf/model/aframe/addon/aframe-components.js

@@ -131,7 +131,7 @@ AFRAME.registerComponent('cursor-listener', {
             console.log('I was clicked at: ', evt.detail.intersection.point);
             let cursorID = 'cursor-avatar-' + vwf_view.kernel.moniker();
             if (evt.detail.cursorEl.id.includes(vwf_view.kernel.moniker())) {
-                vwf_view.kernel.fireEvent(evt.detail.target.id, "clickEvent")
+                vwf_view.kernel.fireEvent(evt.detail.intersection.object.el.id, "clickEvent")
             }
 
             //vwf_view.kernel.fireEvent(evt.detail.target.id, "clickEvent")
@@ -157,7 +157,7 @@ AFRAME.registerComponent('raycaster-listener', {
 
                 } else {
                     console.log('I was intersected at: ', evt.detail.intersection.point);
-                    vwf_view.kernel.fireEvent(evt.detail.target.id, "intersectEvent")
+                    vwf_view.kernel.fireEvent(evt.detail.intersection.object.el.id, "intersectEvent")
                 }
 
                 self.casters[evt.detail.el.id] = evt.detail.el;
@@ -176,7 +176,7 @@ AFRAME.registerComponent('raycaster-listener', {
                 if (self.intersected) {
                     console.log('Clear intersection');
                     if (Object.entries(self.casters).length == 1 && (self.casters[evt.detail.el.id] !== undefined)) {
-                        vwf_view.kernel.fireEvent(evt.detail.target.id, "clearIntersectEvent")
+                        vwf_view.kernel.fireEvent(evt.target.id, "clearIntersectEvent")
                     }
                     delete self.casters[evt.detail.el.id]
                 } else { }
@@ -369,4 +369,130 @@ AFRAME.registerComponent('sun', {
 
     tick: function (t) {
     }
-})
+})
+
+AFRAME.registerComponent('gearvrcontrol', {
+    
+        init: function () {
+            var self = this;
+            var controllerID = 'gearvr-' + vwf_view.kernel.moniker();
+            //this.gearel = document.querySelector('#gearvrcontrol');
+            this.el.addEventListener('triggerdown', function (event) {
+              vwf_view.kernel.callMethod(controllerID, "triggerdown", []);
+              });
+              this.el.addEventListener('triggerup', function (event) {
+               vwf_view.kernel.callMethod(controllerID, "triggerup", []);
+              });
+        },
+    
+        update: function () {
+        },
+    
+        tick: function (t) {
+        }
+    })
+
+
+    AFRAME.registerComponent('wmrvrcontrol', {
+        
+        schema: {
+            hand: { default: 'right' }
+        },
+    
+        update: function (old) {
+            this.hand = this.data.hand;
+        },
+
+            init: function () {
+                var self = this;
+                this.hand = this.data.hand;
+                var controllerID = 'wrmr-' + this.hand + '-' + vwf_view.kernel.moniker();
+                //this.gearel = document.querySelector('#gearvrcontrol');
+                this.el.addEventListener('triggerdown', function (event) {
+                  vwf_view.kernel.callMethod(controllerID, "triggerdown", []);
+                  });
+                  this.el.addEventListener('triggerup', function (event) {
+                   vwf_view.kernel.callMethod(controllerID, "triggerup", []);
+                  });
+            },
+        
+            tick: function (t) {
+            }
+        })
+
+        AFRAME.registerComponent('streamsound', {
+
+            schema: {
+                positional: { default: true }
+              },
+           
+            init: function () {
+                var self = this;
+
+                let driver = vwf.views["vwf/view/webrtc"];
+
+                this.listener = null;
+                this.stream = null;
+
+                if(!this.sound) {
+                    this.setupSound();
+                  }
+
+                  if (driver) {
+                    //let avatarID = 'avatar-' + vwf.moniker();
+                    let avatarID = this.el.id.slice(0, 27); //avatar-0RtnYBBTBU84OCNcAAFY
+                   let client = driver.state.clients[avatarID];
+                   if (client ){
+                       if (client.connection) {
+                    this.stream = client.connection.stream;
+                    if (this.stream){
+                        this.audioEl = new Audio();
+                        this.audioEl.srcObject = this.stream;
+            
+                    this.sound.setNodeSource(this.sound.context.createMediaStreamSource(this.stream));
+                    }
+                   }
+                }
+                  }
+
+            },
+
+            setupSound: function() {
+                var el = this.el;
+                var sceneEl = el.sceneEl;
+            
+                if (this.sound) {
+                  el.removeObject3D(this.attrName);
+                }
+            
+                if (!sceneEl.audioListener) {
+                  sceneEl.audioListener = new THREE.AudioListener();
+                  sceneEl.camera && sceneEl.camera.add(sceneEl.audioListener);
+                  sceneEl.addEventListener('camera-set-active', function(evt) {
+                    evt.detail.cameraEl.getObject3D('camera').add(sceneEl.audioListener);
+                  });
+                }
+                this.listener = sceneEl.audioListener;
+            
+                this.sound = this.data.positional
+                  ? new THREE.PositionalAudio(this.listener)
+                  : new THREE.Audio(this.listener);
+                el.setObject3D(this.attrName, this.sound);
+              },
+
+              remove: function() {
+                if (!this.sound) return;
+            
+                this.el.removeObject3D(this.attrName);
+                if (this.stream) {
+                  this.sound.disconnect();
+                }
+              },
+
+            update: function (old) {
+
+            },
+
+            tick: function (t) {
+            }
+            })

+ 17994 - 0
support/client/lib/vwf/model/aframe/addon/aframe-extras.controls.js

@@ -0,0 +1,17994 @@
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+require('./src/controls').registerAll();
+},{"./src/controls":78}],2:[function(require,module,exports){
+module.exports = Object.assign(function GamepadButton () {}, {
+	FACE_1: 0,
+	FACE_2: 1,
+	FACE_3: 2,
+	FACE_4: 3,
+
+	L_SHOULDER_1: 4,
+	R_SHOULDER_1: 5,
+	L_SHOULDER_2: 6,
+	R_SHOULDER_2: 7,
+
+	SELECT: 8,
+	START: 9,
+
+	DPAD_UP: 12,
+	DPAD_DOWN: 13,
+	DPAD_LEFT: 14,
+	DPAD_RIGHT: 15,
+
+	VENDOR: 16,
+});
+
+},{}],3:[function(require,module,exports){
+function GamepadButtonEvent (type, index, details) {
+  this.type = type;
+  this.index = index;
+  this.pressed = details.pressed;
+  this.value = details.value;
+}
+
+module.exports = GamepadButtonEvent;
+
+},{}],4:[function(require,module,exports){
+/**
+ * Polyfill for the additional KeyboardEvent properties defined in the D3E and
+ * D4E draft specifications, by @inexorabletash.
+ *
+ * See: https://github.com/inexorabletash/polyfill
+ */
+(function(global) {
+  var nativeKeyboardEvent = ('KeyboardEvent' in global);
+  if (!nativeKeyboardEvent)
+    global.KeyboardEvent = function KeyboardEvent() { throw TypeError('Illegal constructor'); };
+
+  global.KeyboardEvent.DOM_KEY_LOCATION_STANDARD = 0x00; // Default or unknown location
+  global.KeyboardEvent.DOM_KEY_LOCATION_LEFT          = 0x01; // e.g. Left Alt key
+  global.KeyboardEvent.DOM_KEY_LOCATION_RIGHT         = 0x02; // e.g. Right Alt key
+  global.KeyboardEvent.DOM_KEY_LOCATION_NUMPAD        = 0x03; // e.g. Numpad 0 or +
+
+  var STANDARD = window.KeyboardEvent.DOM_KEY_LOCATION_STANDARD,
+      LEFT = window.KeyboardEvent.DOM_KEY_LOCATION_LEFT,
+      RIGHT = window.KeyboardEvent.DOM_KEY_LOCATION_RIGHT,
+      NUMPAD = window.KeyboardEvent.DOM_KEY_LOCATION_NUMPAD;
+
+  //--------------------------------------------------------------------
+  //
+  // Utilities
+  //
+  //--------------------------------------------------------------------
+
+  function contains(s, ss) { return String(s).indexOf(ss) !== -1; }
+
+  var os = (function() {
+    if (contains(navigator.platform, 'Win')) { return 'win'; }
+    if (contains(navigator.platform, 'Mac')) { return 'mac'; }
+    if (contains(navigator.platform, 'CrOS')) { return 'cros'; }
+    if (contains(navigator.platform, 'Linux')) { return 'linux'; }
+    if (contains(navigator.userAgent, 'iPad') || contains(navigator.platform, 'iPod') || contains(navigator.platform, 'iPhone')) { return 'ios'; }
+    return '';
+  } ());
+
+  var browser = (function() {
+    if (contains(navigator.userAgent, 'Chrome/')) { return 'chrome'; }
+    if (contains(navigator.vendor, 'Apple')) { return 'safari'; }
+    if (contains(navigator.userAgent, 'MSIE')) { return 'ie'; }
+    if (contains(navigator.userAgent, 'Gecko/')) { return 'moz'; }
+    if (contains(navigator.userAgent, 'Opera/')) { return 'opera'; }
+    return '';
+  } ());
+
+  var browser_os = browser + '-' + os;
+
+  function mergeIf(baseTable, select, table) {
+    if (browser_os === select || browser === select || os === select) {
+      Object.keys(table).forEach(function(keyCode) {
+        baseTable[keyCode] = table[keyCode];
+      });
+    }
+  }
+
+  function remap(o, key) {
+    var r = {};
+    Object.keys(o).forEach(function(k) {
+      var item = o[k];
+      if (key in item) {
+        r[item[key]] = item;
+      }
+    });
+    return r;
+  }
+
+  function invert(o) {
+    var r = {};
+    Object.keys(o).forEach(function(k) {
+      r[o[k]] = k;
+    });
+    return r;
+  }
+
+  //--------------------------------------------------------------------
+  //
+  // Generic Mappings
+  //
+  //--------------------------------------------------------------------
+
+  // "keyInfo" is a dictionary:
+  //   code: string - name from DOM Level 3 KeyboardEvent code Values
+  //     https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3Events-code.html
+  //   location (optional): number - one of the DOM_KEY_LOCATION values
+  //   keyCap (optional): string - keyboard label in en-US locale
+  // USB code Usage ID from page 0x07 unless otherwise noted (Informative)
+
+  // Map of keyCode to keyInfo
+  var keyCodeToInfoTable = {
+    // 0x01 - VK_LBUTTON
+    // 0x02 - VK_RBUTTON
+    0x03: { code: 'Cancel' }, // [USB: 0x9b] char \x0018 ??? (Not in D3E)
+    // 0x04 - VK_MBUTTON
+    // 0x05 - VK_XBUTTON1
+    // 0x06 - VK_XBUTTON2
+    0x06: { code: 'Help' }, // [USB: 0x75] ???
+    // 0x07 - undefined
+    0x08: { code: 'Backspace' }, // [USB: 0x2a] Labelled Delete on Macintosh keyboards.
+    0x09: { code: 'Tab' }, // [USB: 0x2b]
+    // 0x0A-0x0B - reserved
+    0X0C: { code: 'Clear' }, // [USB: 0x9c] NumPad Center (Not in D3E)
+    0X0D: { code: 'Enter' }, // [USB: 0x28]
+    // 0x0E-0x0F - undefined
+
+    0x10: { code: 'Shift' },
+    0x11: { code: 'Control' },
+    0x12: { code: 'Alt' },
+    0x13: { code: 'Pause' }, // [USB: 0x48]
+    0x14: { code: 'CapsLock' }, // [USB: 0x39]
+    0x15: { code: 'KanaMode' }, // [USB: 0x88] - "HangulMode" for Korean layout
+    0x16: { code: 'HangulMode' }, // [USB: 0x90] 0x15 as well in MSDN VK table ???
+    0x17: { code: 'JunjaMode' }, // (Not in D3E)
+    0x18: { code: 'FinalMode' }, // (Not in D3E)
+    0x19: { code: 'KanjiMode' }, // [USB: 0x91] - "HanjaMode" for Korean layout
+    // 0x1A - undefined
+    0x1B: { code: 'Escape' }, // [USB: 0x29]
+    0x1C: { code: 'Convert' }, // [USB: 0x8a]
+    0x1D: { code: 'NonConvert' }, // [USB: 0x8b]
+    0x1E: { code: 'Accept' }, // (Not in D3E)
+    0x1F: { code: 'ModeChange' }, // (Not in D3E)
+
+    0x20: { code: 'Space' }, // [USB: 0x2c]
+    0x21: { code: 'PageUp' }, // [USB: 0x4b]
+    0x22: { code: 'PageDown' }, // [USB: 0x4e]
+    0x23: { code: 'End' }, // [USB: 0x4d]
+    0x24: { code: 'Home' }, // [USB: 0x4a]
+    0x25: { code: 'ArrowLeft' }, // [USB: 0x50]
+    0x26: { code: 'ArrowUp' }, // [USB: 0x52]
+    0x27: { code: 'ArrowRight' }, // [USB: 0x4f]
+    0x28: { code: 'ArrowDown' }, // [USB: 0x51]
+    0x29: { code: 'Select' }, // (Not in D3E)
+    0x2A: { code: 'Print' }, // (Not in D3E)
+    0x2B: { code: 'Execute' }, // [USB: 0x74] (Not in D3E)
+    0x2C: { code: 'PrintScreen' }, // [USB: 0x46]
+    0x2D: { code: 'Insert' }, // [USB: 0x49]
+    0x2E: { code: 'Delete' }, // [USB: 0x4c]
+    0x2F: { code: 'Help' }, // [USB: 0x75] ???
+
+    0x30: { code: 'Digit0', keyCap: '0' }, // [USB: 0x27] 0)
+    0x31: { code: 'Digit1', keyCap: '1' }, // [USB: 0x1e] 1!
+    0x32: { code: 'Digit2', keyCap: '2' }, // [USB: 0x1f] 2@
+    0x33: { code: 'Digit3', keyCap: '3' }, // [USB: 0x20] 3#
+    0x34: { code: 'Digit4', keyCap: '4' }, // [USB: 0x21] 4$
+    0x35: { code: 'Digit5', keyCap: '5' }, // [USB: 0x22] 5%
+    0x36: { code: 'Digit6', keyCap: '6' }, // [USB: 0x23] 6^
+    0x37: { code: 'Digit7', keyCap: '7' }, // [USB: 0x24] 7&
+    0x38: { code: 'Digit8', keyCap: '8' }, // [USB: 0x25] 8*
+    0x39: { code: 'Digit9', keyCap: '9' }, // [USB: 0x26] 9(
+    // 0x3A-0x40 - undefined
+
+    0x41: { code: 'KeyA', keyCap: 'a' }, // [USB: 0x04]
+    0x42: { code: 'KeyB', keyCap: 'b' }, // [USB: 0x05]
+    0x43: { code: 'KeyC', keyCap: 'c' }, // [USB: 0x06]
+    0x44: { code: 'KeyD', keyCap: 'd' }, // [USB: 0x07]
+    0x45: { code: 'KeyE', keyCap: 'e' }, // [USB: 0x08]
+    0x46: { code: 'KeyF', keyCap: 'f' }, // [USB: 0x09]
+    0x47: { code: 'KeyG', keyCap: 'g' }, // [USB: 0x0a]
+    0x48: { code: 'KeyH', keyCap: 'h' }, // [USB: 0x0b]
+    0x49: { code: 'KeyI', keyCap: 'i' }, // [USB: 0x0c]
+    0x4A: { code: 'KeyJ', keyCap: 'j' }, // [USB: 0x0d]
+    0x4B: { code: 'KeyK', keyCap: 'k' }, // [USB: 0x0e]
+    0x4C: { code: 'KeyL', keyCap: 'l' }, // [USB: 0x0f]
+    0x4D: { code: 'KeyM', keyCap: 'm' }, // [USB: 0x10]
+    0x4E: { code: 'KeyN', keyCap: 'n' }, // [USB: 0x11]
+    0x4F: { code: 'KeyO', keyCap: 'o' }, // [USB: 0x12]
+
+    0x50: { code: 'KeyP', keyCap: 'p' }, // [USB: 0x13]
+    0x51: { code: 'KeyQ', keyCap: 'q' }, // [USB: 0x14]
+    0x52: { code: 'KeyR', keyCap: 'r' }, // [USB: 0x15]
+    0x53: { code: 'KeyS', keyCap: 's' }, // [USB: 0x16]
+    0x54: { code: 'KeyT', keyCap: 't' }, // [USB: 0x17]
+    0x55: { code: 'KeyU', keyCap: 'u' }, // [USB: 0x18]
+    0x56: { code: 'KeyV', keyCap: 'v' }, // [USB: 0x19]
+    0x57: { code: 'KeyW', keyCap: 'w' }, // [USB: 0x1a]
+    0x58: { code: 'KeyX', keyCap: 'x' }, // [USB: 0x1b]
+    0x59: { code: 'KeyY', keyCap: 'y' }, // [USB: 0x1c]
+    0x5A: { code: 'KeyZ', keyCap: 'z' }, // [USB: 0x1d]
+    0x5B: { code: 'OSLeft', location: LEFT }, // [USB: 0xe3]
+    0x5C: { code: 'OSRight', location: RIGHT }, // [USB: 0xe7]
+    0x5D: { code: 'ContextMenu' }, // [USB: 0x65] Context Menu
+    // 0x5E - reserved
+    0x5F: { code: 'Standby' }, // [USB: 0x82] Sleep
+
+    0x60: { code: 'Numpad0', keyCap: '0', location: NUMPAD }, // [USB: 0x62]
+    0x61: { code: 'Numpad1', keyCap: '1', location: NUMPAD }, // [USB: 0x59]
+    0x62: { code: 'Numpad2', keyCap: '2', location: NUMPAD }, // [USB: 0x5a]
+    0x63: { code: 'Numpad3', keyCap: '3', location: NUMPAD }, // [USB: 0x5b]
+    0x64: { code: 'Numpad4', keyCap: '4', location: NUMPAD }, // [USB: 0x5c]
+    0x65: { code: 'Numpad5', keyCap: '5', location: NUMPAD }, // [USB: 0x5d]
+    0x66: { code: 'Numpad6', keyCap: '6', location: NUMPAD }, // [USB: 0x5e]
+    0x67: { code: 'Numpad7', keyCap: '7', location: NUMPAD }, // [USB: 0x5f]
+    0x68: { code: 'Numpad8', keyCap: '8', location: NUMPAD }, // [USB: 0x60]
+    0x69: { code: 'Numpad9', keyCap: '9', location: NUMPAD }, // [USB: 0x61]
+    0x6A: { code: 'NumpadMultiply', keyCap: '*', location: NUMPAD }, // [USB: 0x55]
+    0x6B: { code: 'NumpadAdd', keyCap: '+', location: NUMPAD }, // [USB: 0x57]
+    0x6C: { code: 'NumpadComma', keyCap: ',', location: NUMPAD }, // [USB: 0x85]
+    0x6D: { code: 'NumpadSubtract', keyCap: '-', location: NUMPAD }, // [USB: 0x56]
+    0x6E: { code: 'NumpadDecimal', keyCap: '.', location: NUMPAD }, // [USB: 0x63]
+    0x6F: { code: 'NumpadDivide', keyCap: '/', location: NUMPAD }, // [USB: 0x54]
+
+    0x70: { code: 'F1' }, // [USB: 0x3a]
+    0x71: { code: 'F2' }, // [USB: 0x3b]
+    0x72: { code: 'F3' }, // [USB: 0x3c]
+    0x73: { code: 'F4' }, // [USB: 0x3d]
+    0x74: { code: 'F5' }, // [USB: 0x3e]
+    0x75: { code: 'F6' }, // [USB: 0x3f]
+    0x76: { code: 'F7' }, // [USB: 0x40]
+    0x77: { code: 'F8' }, // [USB: 0x41]
+    0x78: { code: 'F9' }, // [USB: 0x42]
+    0x79: { code: 'F10' }, // [USB: 0x43]
+    0x7A: { code: 'F11' }, // [USB: 0x44]
+    0x7B: { code: 'F12' }, // [USB: 0x45]
+    0x7C: { code: 'F13' }, // [USB: 0x68]
+    0x7D: { code: 'F14' }, // [USB: 0x69]
+    0x7E: { code: 'F15' }, // [USB: 0x6a]
+    0x7F: { code: 'F16' }, // [USB: 0x6b]
+
+    0x80: { code: 'F17' }, // [USB: 0x6c]
+    0x81: { code: 'F18' }, // [USB: 0x6d]
+    0x82: { code: 'F19' }, // [USB: 0x6e]
+    0x83: { code: 'F20' }, // [USB: 0x6f]
+    0x84: { code: 'F21' }, // [USB: 0x70]
+    0x85: { code: 'F22' }, // [USB: 0x71]
+    0x86: { code: 'F23' }, // [USB: 0x72]
+    0x87: { code: 'F24' }, // [USB: 0x73]
+    // 0x88-0x8F - unassigned
+
+    0x90: { code: 'NumLock', location: NUMPAD }, // [USB: 0x53]
+    0x91: { code: 'ScrollLock' }, // [USB: 0x47]
+    // 0x92-0x96 - OEM specific
+    // 0x97-0x9F - unassigned
+
+    // NOTE: 0xA0-0xA5 usually mapped to 0x10-0x12 in browsers
+    0xA0: { code: 'ShiftLeft', location: LEFT }, // [USB: 0xe1]
+    0xA1: { code: 'ShiftRight', location: RIGHT }, // [USB: 0xe5]
+    0xA2: { code: 'ControlLeft', location: LEFT }, // [USB: 0xe0]
+    0xA3: { code: 'ControlRight', location: RIGHT }, // [USB: 0xe4]
+    0xA4: { code: 'AltLeft', location: LEFT }, // [USB: 0xe2]
+    0xA5: { code: 'AltRight', location: RIGHT }, // [USB: 0xe6]
+
+    0xA6: { code: 'BrowserBack' }, // [USB: 0x0c/0x0224]
+    0xA7: { code: 'BrowserForward' }, // [USB: 0x0c/0x0225]
+    0xA8: { code: 'BrowserRefresh' }, // [USB: 0x0c/0x0227]
+    0xA9: { code: 'BrowserStop' }, // [USB: 0x0c/0x0226]
+    0xAA: { code: 'BrowserSearch' }, // [USB: 0x0c/0x0221]
+    0xAB: { code: 'BrowserFavorites' }, // [USB: 0x0c/0x0228]
+    0xAC: { code: 'BrowserHome' }, // [USB: 0x0c/0x0222]
+    0xAD: { code: 'VolumeMute' }, // [USB: 0x7f]
+    0xAE: { code: 'VolumeDown' }, // [USB: 0x81]
+    0xAF: { code: 'VolumeUp' }, // [USB: 0x80]
+
+    0xB0: { code: 'MediaTrackNext' }, // [USB: 0x0c/0x00b5]
+    0xB1: { code: 'MediaTrackPrevious' }, // [USB: 0x0c/0x00b6]
+    0xB2: { code: 'MediaStop' }, // [USB: 0x0c/0x00b7]
+    0xB3: { code: 'MediaPlayPause' }, // [USB: 0x0c/0x00cd]
+    0xB4: { code: 'LaunchMail' }, // [USB: 0x0c/0x018a]
+    0xB5: { code: 'MediaSelect' },
+    0xB6: { code: 'LaunchApp1' },
+    0xB7: { code: 'LaunchApp2' },
+    // 0xB8-0xB9 - reserved
+    0xBA: { code: 'Semicolon',  keyCap: ';' }, // [USB: 0x33] ;: (US Standard 101)
+    0xBB: { code: 'Equal', keyCap: '=' }, // [USB: 0x2e] =+
+    0xBC: { code: 'Comma', keyCap: ',' }, // [USB: 0x36] ,<
+    0xBD: { code: 'Minus', keyCap: '-' }, // [USB: 0x2d] -_
+    0xBE: { code: 'Period', keyCap: '.' }, // [USB: 0x37] .>
+    0xBF: { code: 'Slash', keyCap: '/' }, // [USB: 0x38] /? (US Standard 101)
+
+    0xC0: { code: 'Backquote', keyCap: '`' }, // [USB: 0x35] `~ (US Standard 101)
+    // 0xC1-0xCF - reserved
+
+    // 0xD0-0xD7 - reserved
+    // 0xD8-0xDA - unassigned
+    0xDB: { code: 'BracketLeft', keyCap: '[' }, // [USB: 0x2f] [{ (US Standard 101)
+    0xDC: { code: 'Backslash',  keyCap: '\\' }, // [USB: 0x31] \| (US Standard 101)
+    0xDD: { code: 'BracketRight', keyCap: ']' }, // [USB: 0x30] ]} (US Standard 101)
+    0xDE: { code: 'Quote', keyCap: '\'' }, // [USB: 0x34] '" (US Standard 101)
+    // 0xDF - miscellaneous/varies
+
+    // 0xE0 - reserved
+    // 0xE1 - OEM specific
+    0xE2: { code: 'IntlBackslash',  keyCap: '\\' }, // [USB: 0x64] \| (UK Standard 102)
+    // 0xE3-0xE4 - OEM specific
+    0xE5: { code: 'Process' }, // (Not in D3E)
+    // 0xE6 - OEM specific
+    // 0xE7 - VK_PACKET
+    // 0xE8 - unassigned
+    // 0xE9-0xEF - OEM specific
+
+    // 0xF0-0xF5 - OEM specific
+    0xF6: { code: 'Attn' }, // [USB: 0x9a] (Not in D3E)
+    0xF7: { code: 'CrSel' }, // [USB: 0xa3] (Not in D3E)
+    0xF8: { code: 'ExSel' }, // [USB: 0xa4] (Not in D3E)
+    0xF9: { code: 'EraseEof' }, // (Not in D3E)
+    0xFA: { code: 'Play' }, // (Not in D3E)
+    0xFB: { code: 'ZoomToggle' }, // (Not in D3E)
+    // 0xFC - VK_NONAME - reserved
+    // 0xFD - VK_PA1
+    0xFE: { code: 'Clear' } // [USB: 0x9c] (Not in D3E)
+  };
+
+  // No legacy keyCode, but listed in D3E:
+
+  // code: usb
+  // 'IntlHash': 0x070032,
+  // 'IntlRo': 0x070087,
+  // 'IntlYen': 0x070089,
+  // 'NumpadBackspace': 0x0700bb,
+  // 'NumpadClear': 0x0700d8,
+  // 'NumpadClearEntry': 0x0700d9,
+  // 'NumpadMemoryAdd': 0x0700d3,
+  // 'NumpadMemoryClear': 0x0700d2,
+  // 'NumpadMemoryRecall': 0x0700d1,
+  // 'NumpadMemoryStore': 0x0700d0,
+  // 'NumpadMemorySubtract': 0x0700d4,
+  // 'NumpadParenLeft': 0x0700b6,
+  // 'NumpadParenRight': 0x0700b7,
+
+  //--------------------------------------------------------------------
+  //
+  // Browser/OS Specific Mappings
+  //
+  //--------------------------------------------------------------------
+
+  mergeIf(keyCodeToInfoTable,
+          'moz', {
+            0x3B: { code: 'Semicolon', keyCap: ';' }, // [USB: 0x33] ;: (US Standard 101)
+            0x3D: { code: 'Equal', keyCap: '=' }, // [USB: 0x2e] =+
+            0x6B: { code: 'Equal', keyCap: '=' }, // [USB: 0x2e] =+
+            0x6D: { code: 'Minus', keyCap: '-' }, // [USB: 0x2d] -_
+            0xBB: { code: 'NumpadAdd', keyCap: '+', location: NUMPAD }, // [USB: 0x57]
+            0xBD: { code: 'NumpadSubtract', keyCap: '-', location: NUMPAD } // [USB: 0x56]
+          });
+
+  mergeIf(keyCodeToInfoTable,
+          'moz-mac', {
+            0x0C: { code: 'NumLock', location: NUMPAD }, // [USB: 0x53]
+            0xAD: { code: 'Minus', keyCap: '-' } // [USB: 0x2d] -_
+          });
+
+  mergeIf(keyCodeToInfoTable,
+          'moz-win', {
+            0xAD: { code: 'Minus', keyCap: '-' } // [USB: 0x2d] -_
+          });
+
+  mergeIf(keyCodeToInfoTable,
+          'chrome-mac', {
+            0x5D: { code: 'OSRight', location: RIGHT } // [USB: 0xe7]
+          });
+
+  // Windows via Bootcamp (!)
+  if (0) {
+    mergeIf(keyCodeToInfoTable,
+            'chrome-win', {
+              0xC0: { code: 'Quote', keyCap: '\'' }, // [USB: 0x34] '" (US Standard 101)
+              0xDE: { code: 'Backslash',  keyCap: '\\' }, // [USB: 0x31] \| (US Standard 101)
+              0xDF: { code: 'Backquote', keyCap: '`' } // [USB: 0x35] `~ (US Standard 101)
+            });
+
+    mergeIf(keyCodeToInfoTable,
+            'ie', {
+              0xC0: { code: 'Quote', keyCap: '\'' }, // [USB: 0x34] '" (US Standard 101)
+              0xDE: { code: 'Backslash',  keyCap: '\\' }, // [USB: 0x31] \| (US Standard 101)
+              0xDF: { code: 'Backquote', keyCap: '`' } // [USB: 0x35] `~ (US Standard 101)
+            });
+  }
+
+  mergeIf(keyCodeToInfoTable,
+          'safari', {
+            0x03: { code: 'Enter' }, // [USB: 0x28] old Safari
+            0x19: { code: 'Tab' } // [USB: 0x2b] old Safari for Shift+Tab
+          });
+
+  mergeIf(keyCodeToInfoTable,
+          'ios', {
+            0x0A: { code: 'Enter', location: STANDARD } // [USB: 0x28]
+          });
+
+  mergeIf(keyCodeToInfoTable,
+          'safari-mac', {
+            0x5B: { code: 'OSLeft', location: LEFT }, // [USB: 0xe3]
+            0x5D: { code: 'OSRight', location: RIGHT }, // [USB: 0xe7]
+            0xE5: { code: 'KeyQ', keyCap: 'Q' } // [USB: 0x14] On alternate presses, Ctrl+Q sends this
+          });
+
+  //--------------------------------------------------------------------
+  //
+  // Identifier Mappings
+  //
+  //--------------------------------------------------------------------
+
+  // Cases where newer-ish browsers send keyIdentifier which can be
+  // used to disambiguate keys.
+
+  // keyIdentifierTable[keyIdentifier] -> keyInfo
+
+  var keyIdentifierTable = {};
+  if ('cros' === os) {
+    keyIdentifierTable['U+00A0'] = { code: 'ShiftLeft', location: LEFT };
+    keyIdentifierTable['U+00A1'] = { code: 'ShiftRight', location: RIGHT };
+    keyIdentifierTable['U+00A2'] = { code: 'ControlLeft', location: LEFT };
+    keyIdentifierTable['U+00A3'] = { code: 'ControlRight', location: RIGHT };
+    keyIdentifierTable['U+00A4'] = { code: 'AltLeft', location: LEFT };
+    keyIdentifierTable['U+00A5'] = { code: 'AltRight', location: RIGHT };
+  }
+  if ('chrome-mac' === browser_os) {
+    keyIdentifierTable['U+0010'] = { code: 'ContextMenu' };
+  }
+  if ('safari-mac' === browser_os) {
+    keyIdentifierTable['U+0010'] = { code: 'ContextMenu' };
+  }
+  if ('ios' === os) {
+    // These only generate keyup events
+    keyIdentifierTable['U+0010'] = { code: 'Function' };
+
+    keyIdentifierTable['U+001C'] = { code: 'ArrowLeft' };
+    keyIdentifierTable['U+001D'] = { code: 'ArrowRight' };
+    keyIdentifierTable['U+001E'] = { code: 'ArrowUp' };
+    keyIdentifierTable['U+001F'] = { code: 'ArrowDown' };
+
+    keyIdentifierTable['U+0001'] = { code: 'Home' }; // [USB: 0x4a] Fn + ArrowLeft
+    keyIdentifierTable['U+0004'] = { code: 'End' }; // [USB: 0x4d] Fn + ArrowRight
+    keyIdentifierTable['U+000B'] = { code: 'PageUp' }; // [USB: 0x4b] Fn + ArrowUp
+    keyIdentifierTable['U+000C'] = { code: 'PageDown' }; // [USB: 0x4e] Fn + ArrowDown
+  }
+
+  //--------------------------------------------------------------------
+  //
+  // Location Mappings
+  //
+  //--------------------------------------------------------------------
+
+  // Cases where newer-ish browsers send location/keyLocation which
+  // can be used to disambiguate keys.
+
+  // locationTable[location][keyCode] -> keyInfo
+  var locationTable = [];
+  locationTable[LEFT] = {
+    0x10: { code: 'ShiftLeft', location: LEFT }, // [USB: 0xe1]
+    0x11: { code: 'ControlLeft', location: LEFT }, // [USB: 0xe0]
+    0x12: { code: 'AltLeft', location: LEFT } // [USB: 0xe2]
+  };
+  locationTable[RIGHT] = {
+    0x10: { code: 'ShiftRight', location: RIGHT }, // [USB: 0xe5]
+    0x11: { code: 'ControlRight', location: RIGHT }, // [USB: 0xe4]
+    0x12: { code: 'AltRight', location: RIGHT } // [USB: 0xe6]
+  };
+  locationTable[NUMPAD] = {
+    0x0D: { code: 'NumpadEnter', location: NUMPAD } // [USB: 0x58]
+  };
+
+  mergeIf(locationTable[NUMPAD], 'moz', {
+    0x6D: { code: 'NumpadSubtract', location: NUMPAD }, // [USB: 0x56]
+    0x6B: { code: 'NumpadAdd', location: NUMPAD } // [USB: 0x57]
+  });
+  mergeIf(locationTable[LEFT], 'moz-mac', {
+    0xE0: { code: 'OSLeft', location: LEFT } // [USB: 0xe3]
+  });
+  mergeIf(locationTable[RIGHT], 'moz-mac', {
+    0xE0: { code: 'OSRight', location: RIGHT } // [USB: 0xe7]
+  });
+  mergeIf(locationTable[RIGHT], 'moz-win', {
+    0x5B: { code: 'OSRight', location: RIGHT } // [USB: 0xe7]
+  });
+
+
+  mergeIf(locationTable[RIGHT], 'mac', {
+    0x5D: { code: 'OSRight', location: RIGHT } // [USB: 0xe7]
+  });
+
+  mergeIf(locationTable[NUMPAD], 'chrome-mac', {
+    0x0C: { code: 'NumLock', location: NUMPAD } // [USB: 0x53]
+  });
+
+  mergeIf(locationTable[NUMPAD], 'safari-mac', {
+    0x0C: { code: 'NumLock', location: NUMPAD }, // [USB: 0x53]
+    0xBB: { code: 'NumpadAdd', location: NUMPAD }, // [USB: 0x57]
+    0xBD: { code: 'NumpadSubtract', location: NUMPAD }, // [USB: 0x56]
+    0xBE: { code: 'NumpadDecimal', location: NUMPAD }, // [USB: 0x63]
+    0xBF: { code: 'NumpadDivide', location: NUMPAD } // [USB: 0x54]
+  });
+
+
+  //--------------------------------------------------------------------
+  //
+  // Key Values
+  //
+  //--------------------------------------------------------------------
+
+  // Mapping from `code` values to `key` values. Values defined at:
+  // https://dvcs.w3.org/hg/dom3events/raw-file/tip/html/DOM3Events-key.html
+  // Entries are only provided when `key` differs from `code`. If
+  // printable, `shiftKey` has the shifted printable character. This
+  // assumes US Standard 101 layout
+
+  var codeToKeyTable = {
+    // Modifier Keys
+    ShiftLeft: { key: 'Shift' },
+    ShiftRight: { key: 'Shift' },
+    ControlLeft: { key: 'Control' },
+    ControlRight: { key: 'Control' },
+    AltLeft: { key: 'Alt' },
+    AltRight: { key: 'Alt' },
+    OSLeft: { key: 'OS' },
+    OSRight: { key: 'OS' },
+
+    // Whitespace Keys
+    NumpadEnter: { key: 'Enter' },
+    Space: { key: ' ' },
+
+    // Printable Keys
+    Digit0: { key: '0', shiftKey: ')' },
+    Digit1: { key: '1', shiftKey: '!' },
+    Digit2: { key: '2', shiftKey: '@' },
+    Digit3: { key: '3', shiftKey: '#' },
+    Digit4: { key: '4', shiftKey: '$' },
+    Digit5: { key: '5', shiftKey: '%' },
+    Digit6: { key: '6', shiftKey: '^' },
+    Digit7: { key: '7', shiftKey: '&' },
+    Digit8: { key: '8', shiftKey: '*' },
+    Digit9: { key: '9', shiftKey: '(' },
+    KeyA: { key: 'a', shiftKey: 'A' },
+    KeyB: { key: 'b', shiftKey: 'B' },
+    KeyC: { key: 'c', shiftKey: 'C' },
+    KeyD: { key: 'd', shiftKey: 'D' },
+    KeyE: { key: 'e', shiftKey: 'E' },
+    KeyF: { key: 'f', shiftKey: 'F' },
+    KeyG: { key: 'g', shiftKey: 'G' },
+    KeyH: { key: 'h', shiftKey: 'H' },
+    KeyI: { key: 'i', shiftKey: 'I' },
+    KeyJ: { key: 'j', shiftKey: 'J' },
+    KeyK: { key: 'k', shiftKey: 'K' },
+    KeyL: { key: 'l', shiftKey: 'L' },
+    KeyM: { key: 'm', shiftKey: 'M' },
+    KeyN: { key: 'n', shiftKey: 'N' },
+    KeyO: { key: 'o', shiftKey: 'O' },
+    KeyP: { key: 'p', shiftKey: 'P' },
+    KeyQ: { key: 'q', shiftKey: 'Q' },
+    KeyR: { key: 'r', shiftKey: 'R' },
+    KeyS: { key: 's', shiftKey: 'S' },
+    KeyT: { key: 't', shiftKey: 'T' },
+    KeyU: { key: 'u', shiftKey: 'U' },
+    KeyV: { key: 'v', shiftKey: 'V' },
+    KeyW: { key: 'w', shiftKey: 'W' },
+    KeyX: { key: 'x', shiftKey: 'X' },
+    KeyY: { key: 'y', shiftKey: 'Y' },
+    KeyZ: { key: 'z', shiftKey: 'Z' },
+    Numpad0: { key: '0' },
+    Numpad1: { key: '1' },
+    Numpad2: { key: '2' },
+    Numpad3: { key: '3' },
+    Numpad4: { key: '4' },
+    Numpad5: { key: '5' },
+    Numpad6: { key: '6' },
+    Numpad7: { key: '7' },
+    Numpad8: { key: '8' },
+    Numpad9: { key: '9' },
+    NumpadMultiply: { key: '*' },
+    NumpadAdd: { key: '+' },
+    NumpadComma: { key: ',' },
+    NumpadSubtract: { key: '-' },
+    NumpadDecimal: { key: '.' },
+    NumpadDivide: { key: '/' },
+    Semicolon: { key: ';', shiftKey: ':' },
+    Equal: { key: '=', shiftKey: '+' },
+    Comma: { key: ',', shiftKey: '<' },
+    Minus: { key: '-', shiftKey: '_' },
+    Period: { key: '.', shiftKey: '>' },
+    Slash: { key: '/', shiftKey: '?' },
+    Backquote: { key: '`', shiftKey: '~' },
+    BracketLeft: { key: '[', shiftKey: '{' },
+    Backslash: { key: '\\', shiftKey: '|' },
+    BracketRight: { key: ']', shiftKey: '}' },
+    Quote: { key: '\'', shiftKey: '"' },
+    IntlBackslash: { key: '\\', shiftKey: '|' }
+  };
+
+  mergeIf(codeToKeyTable, 'mac', {
+    OSLeft: { key: 'Meta' },
+    OSRight: { key: 'Meta' }
+  });
+
+  // Corrections for 'key' names in older browsers (e.g. FF36-)
+  // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.key#Key_values
+  var keyFixTable = {
+    Esc: 'Escape',
+    Nonconvert: 'NonConvert',
+    Left: 'ArrowLeft',
+    Up: 'ArrowUp',
+    Right: 'ArrowRight',
+    Down: 'ArrowDown',
+    Del: 'Delete',
+    Menu: 'ContextMenu',
+    MediaNextTrack: 'MediaTrackNext',
+    MediaPreviousTrack: 'MediaTrackPrevious',
+    SelectMedia: 'MediaSelect',
+    HalfWidth: 'Hankaku',
+    FullWidth: 'Zenkaku',
+    RomanCharacters: 'Romaji',
+    Crsel: 'CrSel',
+    Exsel: 'ExSel',
+    Zoom: 'ZoomToggle'
+  };
+
+  //--------------------------------------------------------------------
+  //
+  // Exported Functions
+  //
+  //--------------------------------------------------------------------
+
+
+  var codeTable = remap(keyCodeToInfoTable, 'code');
+
+  try {
+    var nativeLocation = nativeKeyboardEvent && ('location' in new KeyboardEvent(''));
+  } catch (_) {}
+
+  function keyInfoForEvent(event) {
+    var keyCode = 'keyCode' in event ? event.keyCode : 'which' in event ? event.which : 0;
+
+    var keyInfo = (function(){
+      if (nativeLocation || 'keyLocation' in event) {
+        var location = nativeLocation ? event.location : event.keyLocation;
+        if (location && keyCode in locationTable[location]) {
+          return locationTable[location][keyCode];
+        }
+      }
+      if ('keyIdentifier' in event && event.keyIdentifier in keyIdentifierTable) {
+        return keyIdentifierTable[event.keyIdentifier];
+      }
+      if (keyCode in keyCodeToInfoTable) {
+        return keyCodeToInfoTable[keyCode];
+      }
+      return null;
+    }());
+
+    // TODO: Track these down and move to general tables
+    if (0) {
+      // TODO: Map these for newerish browsers?
+      // TODO: iOS only?
+      // TODO: Override with more common keyIdentifier name?
+      switch (event.keyIdentifier) {
+      case 'U+0010': keyInfo = { code: 'Function' }; break;
+      case 'U+001C': keyInfo = { code: 'ArrowLeft' }; break;
+      case 'U+001D': keyInfo = { code: 'ArrowRight' }; break;
+      case 'U+001E': keyInfo = { code: 'ArrowUp' }; break;
+      case 'U+001F': keyInfo = { code: 'ArrowDown' }; break;
+      }
+    }
+
+    if (!keyInfo)
+      return null;
+
+    var key = (function() {
+      var entry = codeToKeyTable[keyInfo.code];
+      if (!entry) return keyInfo.code;
+      return (event.shiftKey && 'shiftKey' in entry) ? entry.shiftKey : entry.key;
+    }());
+
+    return {
+      code: keyInfo.code,
+      key: key,
+      location: keyInfo.location,
+      keyCap: keyInfo.keyCap
+    };
+  }
+
+  function queryKeyCap(code, locale) {
+    code = String(code);
+    if (!codeTable.hasOwnProperty(code)) return 'Undefined';
+    if (locale && String(locale).toLowerCase() !== 'en-us') throw Error('Unsupported locale');
+    var keyInfo = codeTable[code];
+    return keyInfo.keyCap || keyInfo.code || 'Undefined';
+  }
+
+  if ('KeyboardEvent' in global && 'defineProperty' in Object) {
+    (function() {
+      function define(o, p, v) {
+        if (p in o) return;
+        Object.defineProperty(o, p, v);
+      }
+
+      define(KeyboardEvent.prototype, 'code', { get: function() {
+        var keyInfo = keyInfoForEvent(this);
+        return keyInfo ? keyInfo.code : '';
+      }});
+
+      // Fix for nonstandard `key` values (FF36-)
+      if ('key' in KeyboardEvent.prototype) {
+        var desc = Object.getOwnPropertyDescriptor(KeyboardEvent.prototype, 'key');
+        Object.defineProperty(KeyboardEvent.prototype, 'key', { get: function() {
+          var key = desc.get.call(this);
+          return keyFixTable.hasOwnProperty(key) ? keyFixTable[key] : key;
+        }});
+      }
+
+      define(KeyboardEvent.prototype, 'key', { get: function() {
+        var keyInfo = keyInfoForEvent(this);
+        return (keyInfo && 'key' in keyInfo) ? keyInfo.key : 'Unidentified';
+      }});
+
+      define(KeyboardEvent.prototype, 'location', { get: function() {
+        var keyInfo = keyInfoForEvent(this);
+        return (keyInfo && 'location' in keyInfo) ? keyInfo.location : STANDARD;
+      }});
+
+      define(KeyboardEvent.prototype, 'locale', { get: function() {
+        return '';
+      }});
+    }());
+  }
+
+  if (!('queryKeyCap' in global.KeyboardEvent))
+    global.KeyboardEvent.queryKeyCap = queryKeyCap;
+
+  // Helper for IE8-
+  global.identifyKey = function(event) {
+    if ('code' in event)
+      return;
+
+    var keyInfo = keyInfoForEvent(event);
+    event.code = keyInfo ? keyInfo.code : '';
+    event.key = (keyInfo && 'key' in keyInfo) ? keyInfo.key : 'Unidentified';
+    event.location = ('location' in event) ? event.location :
+      ('keyLocation' in event) ? event.keyLocation :
+      (keyInfo && 'location' in keyInfo) ? keyInfo.location : STANDARD;
+    event.locale = '';
+  };
+
+} (window));
+
+},{}],5:[function(require,module,exports){
+var CANNON = require('cannon'),
+    math = require('./src/components/math');
+
+module.exports = {
+  'dynamic-body':   require('./src/components/body/dynamic-body'),
+  'static-body':    require('./src/components/body/static-body'),
+  'constraint':     require('./src/components/constraint'),
+  'system':         require('./src/system/physics'),
+
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
+
+    AFRAME = AFRAME || window.AFRAME;
+
+    math.registerAll();
+    if (!AFRAME.systems.physics)            AFRAME.registerSystem('physics',         this.system);
+    if (!AFRAME.components['dynamic-body']) AFRAME.registerComponent('dynamic-body', this['dynamic-body']);
+    if (!AFRAME.components['static-body'])  AFRAME.registerComponent('static-body',  this['static-body']);
+    if (!AFRAME.components['constraint'])   AFRAME.registerComponent('constraint',   this['constraint']);
+
+    this._registered = true;
+  }
+};
+
+// Export CANNON.js.
+window.CANNON = window.CANNON || CANNON;
+
+},{"./src/components/body/dynamic-body":8,"./src/components/body/static-body":9,"./src/components/constraint":10,"./src/components/math":11,"./src/system/physics":15,"cannon":17}],6:[function(require,module,exports){
+/**
+ * CANNON.shape2mesh
+ *
+ * Source: http://schteppe.github.io/cannon.js/build/cannon.demo.js
+ * Author: @schteppe
+ */
+var CANNON = require('cannon');
+
+CANNON.shape2mesh = function(body){
+    var obj = new THREE.Object3D();
+
+    for (var l = 0; l < body.shapes.length; l++) {
+        var shape = body.shapes[l];
+
+        var mesh;
+
+        switch(shape.type){
+
+        case CANNON.Shape.types.SPHERE:
+            var sphere_geometry = new THREE.SphereGeometry( shape.radius, 8, 8);
+            mesh = new THREE.Mesh( sphere_geometry, this.currentMaterial );
+            break;
+
+        case CANNON.Shape.types.PARTICLE:
+            mesh = new THREE.Mesh( this.particleGeo, this.particleMaterial );
+            var s = this.settings;
+            mesh.scale.set(s.particleSize,s.particleSize,s.particleSize);
+            break;
+
+        case CANNON.Shape.types.PLANE:
+            var geometry = new THREE.PlaneGeometry(10, 10, 4, 4);
+            mesh = new THREE.Object3D();
+            var submesh = new THREE.Object3D();
+            var ground = new THREE.Mesh( geometry, this.currentMaterial );
+            ground.scale.set(100, 100, 100);
+            submesh.add(ground);
+
+            ground.castShadow = true;
+            ground.receiveShadow = true;
+
+            mesh.add(submesh);
+            break;
+
+        case CANNON.Shape.types.BOX:
+            var box_geometry = new THREE.BoxGeometry(  shape.halfExtents.x*2,
+                                                        shape.halfExtents.y*2,
+                                                        shape.halfExtents.z*2 );
+            mesh = new THREE.Mesh( box_geometry, this.currentMaterial );
+            break;
+
+        case CANNON.Shape.types.CONVEXPOLYHEDRON:
+            var geo = new THREE.Geometry();
+
+            // Add vertices
+            for (var i = 0; i < shape.vertices.length; i++) {
+                var v = shape.vertices[i];
+                geo.vertices.push(new THREE.Vector3(v.x, v.y, v.z));
+            }
+
+            for(var i=0; i < shape.faces.length; i++){
+                var face = shape.faces[i];
+
+                // add triangles
+                var a = face[0];
+                for (var j = 1; j < face.length - 1; j++) {
+                    var b = face[j];
+                    var c = face[j + 1];
+                    geo.faces.push(new THREE.Face3(a, b, c));
+                }
+            }
+            geo.computeBoundingSphere();
+            geo.computeFaceNormals();
+            mesh = new THREE.Mesh( geo, this.currentMaterial );
+            break;
+
+        case CANNON.Shape.types.HEIGHTFIELD:
+            var geometry = new THREE.Geometry();
+
+            var v0 = new CANNON.Vec3();
+            var v1 = new CANNON.Vec3();
+            var v2 = new CANNON.Vec3();
+            for (var xi = 0; xi < shape.data.length - 1; xi++) {
+                for (var yi = 0; yi < shape.data[xi].length - 1; yi++) {
+                    for (var k = 0; k < 2; k++) {
+                        shape.getConvexTrianglePillar(xi, yi, k===0);
+                        v0.copy(shape.pillarConvex.vertices[0]);
+                        v1.copy(shape.pillarConvex.vertices[1]);
+                        v2.copy(shape.pillarConvex.vertices[2]);
+                        v0.vadd(shape.pillarOffset, v0);
+                        v1.vadd(shape.pillarOffset, v1);
+                        v2.vadd(shape.pillarOffset, v2);
+                        geometry.vertices.push(
+                            new THREE.Vector3(v0.x, v0.y, v0.z),
+                            new THREE.Vector3(v1.x, v1.y, v1.z),
+                            new THREE.Vector3(v2.x, v2.y, v2.z)
+                        );
+                        var i = geometry.vertices.length - 3;
+                        geometry.faces.push(new THREE.Face3(i, i+1, i+2));
+                    }
+                }
+            }
+            geometry.computeBoundingSphere();
+            geometry.computeFaceNormals();
+            mesh = new THREE.Mesh(geometry, this.currentMaterial);
+            break;
+
+        case CANNON.Shape.types.TRIMESH:
+            var geometry = new THREE.Geometry();
+
+            var v0 = new CANNON.Vec3();
+            var v1 = new CANNON.Vec3();
+            var v2 = new CANNON.Vec3();
+            for (var i = 0; i < shape.indices.length / 3; i++) {
+                shape.getTriangleVertices(i, v0, v1, v2);
+                geometry.vertices.push(
+                    new THREE.Vector3(v0.x, v0.y, v0.z),
+                    new THREE.Vector3(v1.x, v1.y, v1.z),
+                    new THREE.Vector3(v2.x, v2.y, v2.z)
+                );
+                var j = geometry.vertices.length - 3;
+                geometry.faces.push(new THREE.Face3(j, j+1, j+2));
+            }
+            geometry.computeBoundingSphere();
+            geometry.computeFaceNormals();
+            mesh = new THREE.Mesh(geometry, this.currentMaterial);
+            break;
+
+        default:
+            throw "Visual type not recognized: "+shape.type;
+        }
+
+        mesh.receiveShadow = true;
+        mesh.castShadow = true;
+        if(mesh.children){
+            for(var i=0; i<mesh.children.length; i++){
+                mesh.children[i].castShadow = true;
+                mesh.children[i].receiveShadow = true;
+                if(mesh.children[i]){
+                    for(var j=0; j<mesh.children[i].length; j++){
+                        mesh.children[i].children[j].castShadow = true;
+                        mesh.children[i].children[j].receiveShadow = true;
+                    }
+                }
+            }
+        }
+
+        var o = body.shapeOffsets[l];
+        var q = body.shapeOrientations[l];
+        mesh.position.set(o.x, o.y, o.z);
+        mesh.quaternion.set(q.x, q.y, q.z, q.w);
+
+        obj.add(mesh);
+    }
+
+    return obj;
+};
+
+module.exports = CANNON.shape2mesh;
+
+},{"cannon":17}],7:[function(require,module,exports){
+var CANNON = require('cannon'),
+    mesh2shape = require('three-to-cannon');
+
+require('../../../lib/CANNON-shape2mesh');
+
+module.exports = {
+  schema: {
+    shape: {default: 'auto', oneOf: ['auto', 'box', 'cylinder', 'sphere', 'hull', 'none']},
+    cylinderAxis: {default: 'y', oneOf: ['x', 'y', 'z']},
+    sphereRadius: {default: NaN}
+  },
+
+  /**
+   * Initializes a body component, assigning it to the physics system and binding listeners for
+   * parsing the elements geometry.
+   */
+  init: function () {
+    this.system = this.el.sceneEl.systems.physics;
+
+    if (this.el.sceneEl.hasLoaded) {
+      this.initBody();
+    } else {
+      this.el.sceneEl.addEventListener('loaded', this.initBody.bind(this));
+    }
+  },
+
+  /**
+   * Parses an element's geometry and component metadata to create a CANNON.Body instance for the
+   * component.
+   */
+  initBody: function () {
+    var shape,
+        el = this.el,
+        data = this.data,
+        pos = el.getAttribute('position');
+
+    this.body = new CANNON.Body({
+      mass: data.mass || 0,
+      material: this.system.material,
+      position: new CANNON.Vec3(pos.x, pos.y, pos.z),
+      linearDamping: data.linearDamping,
+      angularDamping: data.angularDamping
+    });
+
+    // Matrix World must be updated at root level, if scale is to be applied – updateMatrixWorld()
+    // only checks an object's parent, not the rest of the ancestors. Hence, a wrapping entity with
+    // scale="0.5 0.5 0.5" will be ignored.
+    // Reference: https://github.com/mrdoob/three.js/blob/master/src/core/Object3D.js#L511-L541
+    // Potential fix: https://github.com/mrdoob/three.js/pull/7019
+    this.el.object3D.updateMatrixWorld(true);
+
+    if(data.shape !== 'none') {
+      var options = data.shape === 'auto' ? undefined : AFRAME.utils.extend({}, this.data, {
+        type: mesh2shape.Type[data.shape.toUpperCase()]
+      });
+
+      shape = mesh2shape(this.el.object3D, options);
+
+      if (!shape) {
+        this.el.addEventListener('model-loaded', this.initBody.bind(this));
+        return;
+      }
+
+      this.body.addShape(shape, shape.offset, shape.orientation);
+
+      // Show wireframe
+      if (this.system.debug) {
+        this.createWireframe(this.body, shape);
+      }
+    }
+
+    // Apply rotation
+    var rot = el.getAttribute('rotation');
+    this.body.quaternion.setFromEuler(
+      THREE.Math.degToRad(rot.x),
+      THREE.Math.degToRad(rot.y),
+      THREE.Math.degToRad(rot.z),
+      'XYZ'
+    ).normalize();
+
+    this.el.body = this.body;
+    this.body.el = this.el;
+    this.isLoaded = true;
+
+    // If component wasn't initialized when play() was called, finish up.
+    if (this.isPlaying) {
+      this._play();
+    }
+
+    this.el.emit('body-loaded', {body: this.el.body});
+  },
+
+  /**
+   * Registers the component with the physics system, if ready.
+   */
+  play: function () {
+    if (this.isLoaded) this._play();
+  },
+
+  /**
+   * Internal helper to register component with physics system.
+   */
+  _play: function () {
+    this.system.addBehavior(this, this.system.Phase.SIMULATE);
+    this.system.addBody(this.body);
+    if (this.wireframe) this.el.sceneEl.object3D.add(this.wireframe);
+
+    this.syncToPhysics();
+  },
+
+  /**
+   * Unregisters the component with the physics system.
+   */
+  pause: function () {
+    if (!this.isLoaded) return;
+
+    this.system.removeBehavior(this, this.system.Phase.SIMULATE);
+    this.system.removeBody(this.body);
+    if (this.wireframe) this.el.sceneEl.object3D.remove(this.wireframe);
+  },
+
+  /**
+   * Removes the component and all physics and scene side effects.
+   */
+  remove: function () {
+    this.pause();
+    delete this.body.el;
+    delete this.body;
+    delete this.el.body;
+    delete this.wireframe;
+  },
+
+  /**
+   * Creates a wireframe for the body, for debugging.
+   * TODO(donmccurdy) – Refactor this into a standalone utility or component.
+   * @param  {CANNON.Body} body
+   * @param  {CANNON.Shape} shape
+   */
+  createWireframe: function (body, shape) {
+    var offset = shape.offset,
+        orientation = shape.orientation,
+        mesh = CANNON.shape2mesh(body).children[0];
+
+    this.wireframe = new THREE.LineSegments(
+      new THREE.EdgesGeometry(mesh.geometry),
+      new THREE.LineBasicMaterial({color: 0xff0000})
+    );
+
+    if (offset) {
+      this.wireframe.offset = offset.clone();
+    }
+
+    if (orientation) {
+      orientation.inverse(orientation);
+      this.wireframe.orientation = new THREE.Quaternion(
+        orientation.x,
+        orientation.y,
+        orientation.z,
+        orientation.w
+      );
+    }
+
+    this.syncWireframe();
+  },
+
+  /**
+   * Updates the debugging wireframe's position and rotation.
+   */
+  syncWireframe: function () {
+    var offset,
+        wireframe = this.wireframe;
+
+    if (!this.wireframe) return;
+
+    // Apply rotation. If the shape required custom orientation, also apply
+    // that on the wireframe.
+    wireframe.quaternion.copy(this.body.quaternion);
+    if (wireframe.orientation) {
+      wireframe.quaternion.multiply(wireframe.orientation);
+    }
+
+    // Apply position. If the shape required custom offset, also apply that on
+    // the wireframe.
+    wireframe.position.copy(this.body.position);
+    if (wireframe.offset) {
+      offset = wireframe.offset.clone().applyQuaternion(wireframe.quaternion);
+      wireframe.position.add(offset);
+    }
+
+    wireframe.updateMatrix();
+  },
+
+  /**
+   * Updates the CANNON.Body instance's position, velocity, and rotation, based on the scene.
+   */
+  syncToPhysics: (function () {
+    var q =  new THREE.Quaternion(),
+        v = new THREE.Vector3();
+    return function () {
+      var el = this.el,
+          parentEl = el.parentEl,
+          body = this.body;
+
+      if (!body) return;
+
+      if (el.components.velocity) body.velocity.copy(el.getAttribute('velocity'));
+
+      if (parentEl.isScene) {
+        body.quaternion.copy(el.object3D.quaternion);
+        body.position.copy(el.object3D.position);
+      } else {
+        el.object3D.getWorldQuaternion(q);
+        body.quaternion.copy(q);
+        el.object3D.getWorldPosition(v);
+        body.position.copy(v);
+      }
+
+      if (this.wireframe) this.syncWireframe();
+    };
+  }()),
+
+  /**
+   * Updates the scene object's position and rotation, based on the physics simulation.
+   */
+  syncFromPhysics: (function () {
+    var v = new THREE.Vector3(),
+        q1 = new THREE.Quaternion(),
+        q2 = new THREE.Quaternion();
+    return function () {
+      var el = this.el,
+          parentEl = el.parentEl,
+          body = this.body;
+
+      if (!body) return;
+
+      if (parentEl.isScene) {
+        el.setAttribute('quaternion', body.quaternion);
+        el.setAttribute('position', body.position);
+      } else {
+        // TODO - Nested rotation doesn't seem to be working as expected.
+        q1.copy(body.quaternion);
+        parentEl.object3D.getWorldQuaternion(q2);
+        q1.multiply(q2.inverse());
+        el.setAttribute('quaternion', {x: q1.x, y: q1.y, z: q1.z, w: q1.w});
+
+        v.copy(body.position);
+        parentEl.object3D.worldToLocal(v);
+        el.setAttribute('position', {x: v.x, y: v.y, z: v.z});
+      }
+
+      if (this.wireframe) this.syncWireframe();
+    };
+  }())
+};
+
+},{"../../../lib/CANNON-shape2mesh":6,"cannon":17,"three-to-cannon":73}],8:[function(require,module,exports){
+var Body = require('./body');
+
+/**
+ * Dynamic body.
+ *
+ * Moves according to physics simulation, and may collide with other objects.
+ */
+module.exports = AFRAME.utils.extend({}, Body, {
+  dependencies: ['quaternion', 'velocity'],
+
+  schema: AFRAME.utils.extend({}, Body.schema, {
+    mass:           { default: 5 },
+    linearDamping:  { default: 0.01 },
+    angularDamping: { default: 0.01 }
+  }),
+
+  step: function () {
+    this.syncFromPhysics();
+  }
+});
+
+},{"./body":7}],9:[function(require,module,exports){
+var Body = require('./body');
+
+/**
+ * Static body.
+ *
+ * Solid body with a fixed position. Unaffected by gravity and collisions, but
+ * other objects may collide with it.
+ */
+module.exports = AFRAME.utils.extend({}, Body, {
+  step: function () {
+    this.syncToPhysics();
+  }
+});
+
+},{"./body":7}],10:[function(require,module,exports){
+var CANNON = require('cannon');
+
+module.exports = {
+  dependencies: ['dynamic-body'],
+
+  multiple: true,
+
+  schema: {
+    // Type of constraint.
+    type: {default: 'lock', oneOf: ['coneTwist', 'distance', 'hinge', 'lock', 'pointToPoint']},
+
+    // Target (other) body for the constraint.
+    target: {type: 'selector'},
+
+    // Maximum force that should be applied to constraint the bodies.
+    maxForce: {default: 1e6, min: 0},
+
+    // If true, bodies can collide when they are connected.
+    collideConnected: {default: true},
+
+    // Wake up bodies when connected.
+    wakeUpBodies: {default: true},
+
+    // The distance to be kept between the bodies. If 0, will be set to current distance.
+    distance: {default: 0, min: 0},
+
+    // Offset of the hinge or point-to-point constraint, defined locally in the body.
+    pivot: {type: 'vec3'},
+    targetPivot: {type: 'vec3'},
+
+    // An axis that each body can rotate around, defined locally to that body.
+    axis: {type: 'vec3', default: { x: 0, y: 0, z: 1 }},
+    targetAxis: {type: 'vec3', default: { x: 0, y: 0, z: 1}}
+  },
+
+  init: function () {
+    this.system = this.el.sceneEl.systems.physics;
+    this.constraint = /* {CANNON.Constraint} */ null;
+  },
+
+  remove: function () {
+    if (!this.constraint) return;
+
+    this.system.world.removeConstraint(this.constraint);
+    this.constraint = null;
+  },
+
+  update: function () {
+    var el = this.el,
+        data = this.data;
+
+    this.remove();
+
+    if (!el.body || !data.target.body) {
+      (el.body ? data.target : el).addEventListener('body-loaded', this.update.bind(this, {}));
+      return;
+    }
+
+    this.constraint = this.createConstraint();
+    this.system.world.addConstraint(this.constraint);
+  },
+
+  /**
+   * Creates a new constraint, given current component data. The CANNON.js constructors are a bit
+   * different for each constraint type.
+   * @return {CANNON.Constraint}
+   */
+  createConstraint: function () {
+    var data = this.data,
+        pivot = new CANNON.Vec3(data.pivot.x, data.pivot.y, data.pivot.z),
+        targetPivot = new CANNON.Vec3(data.targetPivot.x, data.targetPivot.y, data.targetPivot.z),
+        axis = new CANNON.Vec3(data.axis.x, data.axis.y, data.axis.z),
+        targetAxis= new CANNON.Vec3(data.targetAxis.x, data.targetAxis.y, data.targetAxis.z);
+
+    var constraint;
+
+    switch (data.type) {
+      case 'lock':
+        constraint = new CANNON.LockConstraint(
+          this.el.body,
+          data.target.body,
+          {maxForce: data.maxForce}
+        );
+        break;
+
+      case 'distance':
+        constraint = new CANNON.DistanceConstraint(
+          this.el.body,
+          data.target.body,
+          data.distance,
+          data.maxForce
+        );
+        break;
+
+      case 'hinge':
+        constraint = new CANNON.HingeConstraint(
+          this.el.body,
+          data.target.body, {
+            pivotA: pivot,
+            pivotB: targetPivot,
+            axisA: axis,
+            axisB: targetAxis,
+            maxForce: data.maxForce
+          });
+        break;
+
+      case 'coneTwist':
+        constraint = new CANNON.ConeTwistConstraint(
+          this.el.body,
+          data.target.body, {
+            pivotA: pivot,
+            pivotB: targetPivot,
+            axisA: axis,
+            axisB: targetAxis,
+            maxForce: data.maxForce
+          });
+        break;
+
+      case 'pointToPoint':
+        constraint = new CANNON.PointToPointConstraint(
+          this.el.body,
+          pivot,
+          data.target.body,
+          targetPivot,
+          data.maxForce);
+        break;
+
+      default:
+        throw new Error('[constraint] Unexpected type: ' + data.type);
+    }
+
+    constraint.collideConnected = data.collideConnected;
+    return constraint;
+  }
+};
+
+},{"cannon":17}],11:[function(require,module,exports){
+module.exports = {
+  'velocity':   require('./velocity'),
+  'quaternion': require('./quaternion'),
+
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
+
+    AFRAME = AFRAME || window.AFRAME;
+
+    if (!AFRAME.components['velocity'])    AFRAME.registerComponent('velocity',   this.velocity);
+    if (!AFRAME.components['quaternion'])  AFRAME.registerComponent('quaternion', this.quaternion);
+
+    this._registered = true;
+  }
+};
+
+},{"./quaternion":12,"./velocity":13}],12:[function(require,module,exports){
+/**
+ * Quaternion.
+ *
+ * Represents orientation of object in three dimensions. Similar to `rotation`
+ * component, but avoids problems of gimbal lock.
+ *
+ * See: https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
+ */
+module.exports = {
+  schema: {type: 'vec4'},
+
+  play: function () {
+    var el = this.el,
+        q = el.object3D.quaternion;
+    if (el.hasAttribute('rotation')) {
+      el.components.rotation.update();
+      el.setAttribute('quaternion', {x: q.x, y: q.y, z: q.z, w: q.w});
+      el.removeAttribute('rotation');
+      this.update();
+    }
+  },
+
+  update: function () {
+    var data = this.data;
+    this.el.object3D.quaternion.set(data.x, data.y, data.z, data.w);
+  }
+};
+
+},{}],13:[function(require,module,exports){
+/**
+ * Velocity, in m/s.
+ */
+module.exports = {
+  schema: {type: 'vec3'},
+
+  init: function () {
+    this.system = this.el.sceneEl.systems.physics;
+
+    if (this.system) {
+      this.system.addBehavior(this, this.system.Phase.RENDER);
+    }
+  },
+
+  remove: function () {
+    if (this.system) {
+      this.system.removeBehavior(this, this.system.Phase.RENDER);
+    }
+  },
+
+  tick: function (t, dt) {
+    if (!dt) return;
+    if (this.system) return;
+    this.step(t, dt);
+  },
+
+  step: function (t, dt) {
+    if (!dt) return;
+
+    var physics = this.el.sceneEl.systems.physics || {data: {maxInterval: 1 / 60}},
+
+        // TODO - There's definitely a bug with getComputedAttribute and el.data.
+        velocity = this.el.getAttribute('velocity') || {x: 0, y: 0, z: 0},
+        position = this.el.getAttribute('position') || {x: 0, y: 0, z: 0};
+
+    dt = Math.min(dt, physics.data.maxInterval * 1000);
+
+    this.el.setAttribute('position', {
+      x: position.x + velocity.x * dt / 1000,
+      y: position.y + velocity.y * dt / 1000,
+      z: position.z + velocity.z * dt / 1000
+    });
+  }
+};
+
+},{}],14:[function(require,module,exports){
+module.exports = {
+  GRAVITY: -9.8,
+  MAX_INTERVAL: 4 / 60,
+  ITERATIONS: 10,
+  CONTACT_MATERIAL: {
+    friction:     0.01,
+    restitution:  0.3,
+    contactEquationStiffness: 1e8,
+    contactEquationRelaxation: 3,
+    frictionEquationStiffness: 1e8,
+    frictionEquationRegularization: 3
+  }
+};
+
+},{}],15:[function(require,module,exports){
+var CANNON = require('cannon'),
+    CONSTANTS = require('../constants'),
+    C_GRAV = CONSTANTS.GRAVITY,
+    C_MAT = CONSTANTS.CONTACT_MATERIAL;
+
+/**
+ * Physics system.
+ */
+module.exports = {
+  schema: {
+    gravity:                        { default: C_GRAV },
+    iterations:                     { default: CONSTANTS.ITERATIONS },
+    friction:                       { default: C_MAT.friction },
+    restitution:                    { default: C_MAT.restitution },
+    contactEquationStiffness:       { default: C_MAT.contactEquationStiffness },
+    contactEquationRelaxation:      { default: C_MAT.contactEquationRelaxation },
+    frictionEquationStiffness:      { default: C_MAT.frictionEquationStiffness },
+    frictionEquationRegularization: { default: C_MAT.frictionEquationRegularization },
+
+    // Never step more than four frames at once. Effectively pauses the scene
+    // when out of focus, and prevents weird "jumps" when focus returns.
+    maxInterval:                    { default: 4 / 60 },
+
+    // If true, show wireframes around physics bodies.
+    debug:                          { default: false },
+  },
+
+  /**
+   * Update phases, used to separate physics simulation from updates to A-Frame scene.
+   * @enum {string}
+   */
+  Phase: {
+    SIMULATE: 'sim',
+    RENDER:   'render'
+  },
+
+  /**
+   * Initializes the physics system.
+   */
+  init: function () {
+    var data = this.data;
+
+    // If true, show wireframes around physics bodies.
+    this.debug = data.debug;
+
+    this.children = {};
+    this.children[this.Phase.SIMULATE] = [];
+    this.children[this.Phase.RENDER] = [];
+
+    this.listeners = {};
+
+    this.world = new CANNON.World();
+    this.world.quatNormalizeSkip = 0;
+    this.world.quatNormalizeFast = false;
+    // this.world.solver.setSpookParams(300,10);
+    this.world.solver.iterations = data.iterations;
+    this.world.gravity.set(0, data.gravity, 0);
+    this.world.broadphase = new CANNON.NaiveBroadphase();
+
+    this.material = new CANNON.Material({name: 'defaultMaterial'});
+    this.contactMaterial = new CANNON.ContactMaterial(this.material, this.material, {
+        friction: data.friction,
+        restitution: data.restitution,
+        contactEquationStiffness: data.contactEquationStiffness,
+        contactEquationRelaxation: data.contactEquationRelaxation,
+        frictionEquationStiffness: data.frictionEquationStiffness,
+        frictionEquationRegularization: data.frictionEquationRegularization
+    });
+    this.world.addContactMaterial(this.contactMaterial);
+  },
+
+  /**
+   * Updates the physics world on each tick of the A-Frame scene. It would be
+   * entirely possible to separate the two – updating physics more or less
+   * frequently than the scene – if greater precision or performance were
+   * necessary.
+   * @param  {number} t
+   * @param  {number} dt
+   */
+  tick: function (t, dt) {
+    if (!dt) return;
+
+    this.world.step(Math.min(dt / 1000, this.data.maxInterval));
+
+    var i;
+    for (i = 0; i < this.children[this.Phase.SIMULATE].length; i++) {
+      this.children[this.Phase.SIMULATE][i].step(t, dt);
+    }
+
+    for (i = 0; i < this.children[this.Phase.RENDER].length; i++) {
+      this.children[this.Phase.RENDER][i].step(t, dt);
+    }
+  },
+
+  /**
+   * Adds a body to the scene, and binds collision events to the element.
+   * @param {CANNON.Body} body
+   */
+  addBody: function (body) {
+    this.listeners[body.id] = function (e) { body.el.emit('collide', e); };
+    body.addEventListener('collide', this.listeners[body.id]);
+    this.world.addBody(body);
+  },
+
+  /**
+   * Removes a body, and its listeners, from the scene.
+   * @param {CANNON.Body} body
+   */
+  removeBody: function (body) {
+    body.removeEventListener('collide', this.listeners[body.id]);
+    delete this.listeners[body.id];
+    this.world.removeBody(body);
+  },
+
+  /**
+   * Adds a component instance to the system, to be invoked on each tick during
+   * the given phase.
+   * @param {Component} component
+   * @param {string} phase
+   */
+  addBehavior: function (component, phase) {
+    this.children[phase].push(component);
+  },
+
+  /**
+   * Removes a component instance from the system.
+   * @param {Component} component
+   * @param {string} phase
+   */
+  removeBehavior: function (component, phase) {
+    this.children[phase].splice(this.children[phase].indexOf(component), 1);
+  },
+
+  /**
+   * Sets an option on the physics system, affecting future simulation steps.
+   * @param {string} opt
+   * @param {mixed} value
+   */
+  update: function (previousData) {
+    var data = this.data;
+
+    if (data.debug !== previousData.debug) {
+      console.warn('[physics] `debug` cannot be changed dynamically.');
+    }
+
+    if (data.maxInterval !== previousData.maxInterval); // noop;
+
+    if (data.gravity !== previousData.gravity) this.world.gravity.set(0, data.gravity, 0);
+
+    this.contactMaterial.friction = data.friction;
+    this.contactMaterial.restitution = data.restitution;
+    this.contactMaterial.contactEquationStiffness = data.contactEquationStiffness;
+    this.contactMaterial.contactEquationRelaxation = data.contactEquationRelaxation;
+    this.contactMaterial.frictionEquationStiffness = data.frictionEquationStiffness;
+    this.contactMaterial.frictionEquationRegularization = data.frictionEquationRegularization;
+  }
+};
+
+},{"../constants":14,"cannon":17}],16:[function(require,module,exports){
+module.exports={
+  "_from": "github:donmccurdy/cannon.js#v0.6.2-dev1",
+  "_id": "cannon@0.6.2",
+  "_inBundle": false,
+  "_integrity": "sha1-kuhwtr7Hd8jqU3mcndOx2tmf0RU=",
+  "_location": "/cannon",
+  "_phantomChildren": {},
+  "_requested": {
+    "type": "git",
+    "raw": "cannon@github:donmccurdy/cannon.js#v0.6.2-dev1",
+    "name": "cannon",
+    "escapedName": "cannon",
+    "rawSpec": "github:donmccurdy/cannon.js#v0.6.2-dev1",
+    "saveSpec": "github:donmccurdy/cannon.js#v0.6.2-dev1",
+    "fetchSpec": null,
+    "gitCommittish": "v0.6.2-dev1"
+  },
+  "_requiredBy": [
+    "/aframe-physics-system"
+  ],
+  "_resolved": "github:donmccurdy/cannon.js#022e8ba53fa83abf0ad8a0e4fd08623123838a17",
+  "_spec": "cannon@github:donmccurdy/cannon.js#v0.6.2-dev1",
+  "_where": "/Users/donmccurdy/Documents/Projects/aframe-extras/node_modules/aframe-physics-system",
+  "author": {
+    "name": "Stefan Hedman",
+    "email": "schteppe@gmail.com",
+    "url": "http://steffe.se"
+  },
+  "bugs": {
+    "url": "https://github.com/schteppe/cannon.js/issues"
+  },
+  "bundleDependencies": false,
+  "dependencies": {},
+  "deprecated": false,
+  "description": "A lightweight 3D physics engine written in JavaScript.",
+  "devDependencies": {
+    "browserify": "*",
+    "grunt": "~0.4.0",
+    "grunt-browserify": "^2.1.4",
+    "grunt-contrib-concat": "~0.1.3",
+    "grunt-contrib-jshint": "~0.1.1",
+    "grunt-contrib-nodeunit": "^0.4.1",
+    "grunt-contrib-uglify": "^0.5.1",
+    "grunt-contrib-yuidoc": "^0.5.2",
+    "jshint": "latest",
+    "nodeunit": "^0.9.0",
+    "uglify-js": "latest"
+  },
+  "engines": {
+    "node": "*"
+  },
+  "homepage": "https://github.com/schteppe/cannon.js",
+  "keywords": [
+    "cannon.js",
+    "cannon",
+    "physics",
+    "engine",
+    "3d"
+  ],
+  "licenses": [
+    {
+      "type": "MIT"
+    }
+  ],
+  "main": "./src/Cannon.js",
+  "name": "cannon",
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/schteppe/cannon.js.git"
+  },
+  "version": "0.6.2"
+}
+
+},{}],17:[function(require,module,exports){
+// Export classes
+module.exports = {
+    version :                       require('../package.json').version,
+
+    AABB :                          require('./collision/AABB'),
+    ArrayCollisionMatrix :          require('./collision/ArrayCollisionMatrix'),
+    Body :                          require('./objects/Body'),
+    Box :                           require('./shapes/Box'),
+    Broadphase :                    require('./collision/Broadphase'),
+    Constraint :                    require('./constraints/Constraint'),
+    ContactEquation :               require('./equations/ContactEquation'),
+    Narrowphase :                   require('./world/Narrowphase'),
+    ConeTwistConstraint :           require('./constraints/ConeTwistConstraint'),
+    ContactMaterial :               require('./material/ContactMaterial'),
+    ConvexPolyhedron :              require('./shapes/ConvexPolyhedron'),
+    Cylinder :                      require('./shapes/Cylinder'),
+    DistanceConstraint :            require('./constraints/DistanceConstraint'),
+    Equation :                      require('./equations/Equation'),
+    EventTarget :                   require('./utils/EventTarget'),
+    FrictionEquation :              require('./equations/FrictionEquation'),
+    GSSolver :                      require('./solver/GSSolver'),
+    GridBroadphase :                require('./collision/GridBroadphase'),
+    Heightfield :                   require('./shapes/Heightfield'),
+    HingeConstraint :               require('./constraints/HingeConstraint'),
+    LockConstraint :                require('./constraints/LockConstraint'),
+    Mat3 :                          require('./math/Mat3'),
+    Material :                      require('./material/Material'),
+    NaiveBroadphase :               require('./collision/NaiveBroadphase'),
+    ObjectCollisionMatrix :         require('./collision/ObjectCollisionMatrix'),
+    Pool :                          require('./utils/Pool'),
+    Particle :                      require('./shapes/Particle'),
+    Plane :                         require('./shapes/Plane'),
+    PointToPointConstraint :        require('./constraints/PointToPointConstraint'),
+    Quaternion :                    require('./math/Quaternion'),
+    Ray :                           require('./collision/Ray'),
+    RaycastVehicle :                require('./objects/RaycastVehicle'),
+    RaycastResult :                 require('./collision/RaycastResult'),
+    RigidVehicle :                  require('./objects/RigidVehicle'),
+    RotationalEquation :            require('./equations/RotationalEquation'),
+    RotationalMotorEquation :       require('./equations/RotationalMotorEquation'),
+    SAPBroadphase :                 require('./collision/SAPBroadphase'),
+    SPHSystem :                     require('./objects/SPHSystem'),
+    Shape :                         require('./shapes/Shape'),
+    Solver :                        require('./solver/Solver'),
+    Sphere :                        require('./shapes/Sphere'),
+    SplitSolver :                   require('./solver/SplitSolver'),
+    Spring :                        require('./objects/Spring'),
+    Transform :                     require('./math/Transform'),
+    Trimesh :                       require('./shapes/Trimesh'),
+    Vec3 :                          require('./math/Vec3'),
+    Vec3Pool :                      require('./utils/Vec3Pool'),
+    World :                         require('./world/World'),
+};
+
+},{"../package.json":16,"./collision/AABB":18,"./collision/ArrayCollisionMatrix":19,"./collision/Broadphase":20,"./collision/GridBroadphase":21,"./collision/NaiveBroadphase":22,"./collision/ObjectCollisionMatrix":23,"./collision/Ray":25,"./collision/RaycastResult":26,"./collision/SAPBroadphase":27,"./constraints/ConeTwistConstraint":28,"./constraints/Constraint":29,"./constraints/DistanceConstraint":30,"./constraints/HingeConstraint":31,"./constraints/LockConstraint":32,"./constraints/PointToPointConstraint":33,"./equations/ContactEquation":35,"./equations/Equation":36,"./equations/FrictionEquation":37,"./equations/RotationalEquation":38,"./equations/RotationalMotorEquation":39,"./material/ContactMaterial":40,"./material/Material":41,"./math/Mat3":43,"./math/Quaternion":44,"./math/Transform":45,"./math/Vec3":46,"./objects/Body":47,"./objects/RaycastVehicle":48,"./objects/RigidVehicle":49,"./objects/SPHSystem":50,"./objects/Spring":51,"./shapes/Box":53,"./shapes/ConvexPolyhedron":54,"./shapes/Cylinder":55,"./shapes/Heightfield":56,"./shapes/Particle":57,"./shapes/Plane":58,"./shapes/Shape":59,"./shapes/Sphere":60,"./shapes/Trimesh":61,"./solver/GSSolver":62,"./solver/Solver":63,"./solver/SplitSolver":64,"./utils/EventTarget":65,"./utils/Pool":67,"./utils/Vec3Pool":70,"./world/Narrowphase":71,"./world/World":72}],18:[function(require,module,exports){
+var Vec3 = require('../math/Vec3');
+var Utils = require('../utils/Utils');
+
+module.exports = AABB;
+
+/**
+ * Axis aligned bounding box class.
+ * @class AABB
+ * @constructor
+ * @param {Object} [options]
+ * @param {Vec3}   [options.upperBound]
+ * @param {Vec3}   [options.lowerBound]
+ */
+function AABB(options){
+    options = options || {};
+
+    /**
+     * The lower bound of the bounding box.
+     * @property lowerBound
+     * @type {Vec3}
+     */
+    this.lowerBound = new Vec3();
+    if(options.lowerBound){
+        this.lowerBound.copy(options.lowerBound);
+    }
+
+    /**
+     * The upper bound of the bounding box.
+     * @property upperBound
+     * @type {Vec3}
+     */
+    this.upperBound = new Vec3();
+    if(options.upperBound){
+        this.upperBound.copy(options.upperBound);
+    }
+}
+
+var tmp = new Vec3();
+
+/**
+ * Set the AABB bounds from a set of points.
+ * @method setFromPoints
+ * @param {Array} points An array of Vec3's.
+ * @param {Vec3} position
+ * @param {Quaternion} quaternion
+ * @param {number} skinSize
+ * @return {AABB} The self object
+ */
+AABB.prototype.setFromPoints = function(points, position, quaternion, skinSize){
+    var l = this.lowerBound,
+        u = this.upperBound,
+        q = quaternion;
+
+    // Set to the first point
+    l.copy(points[0]);
+    if(q){
+        q.vmult(l, l);
+    }
+    u.copy(l);
+
+    for(var i = 1; i<points.length; i++){
+        var p = points[i];
+
+        if(q){
+            q.vmult(p, tmp);
+            p = tmp;
+        }
+
+        if(p.x > u.x){ u.x = p.x; }
+        if(p.x < l.x){ l.x = p.x; }
+        if(p.y > u.y){ u.y = p.y; }
+        if(p.y < l.y){ l.y = p.y; }
+        if(p.z > u.z){ u.z = p.z; }
+        if(p.z < l.z){ l.z = p.z; }
+    }
+
+    // Add offset
+    if (position) {
+        position.vadd(l, l);
+        position.vadd(u, u);
+    }
+
+    if(skinSize){
+        l.x -= skinSize;
+        l.y -= skinSize;
+        l.z -= skinSize;
+        u.x += skinSize;
+        u.y += skinSize;
+        u.z += skinSize;
+    }
+
+    return this;
+};
+
+/**
+ * Copy bounds from an AABB to this AABB
+ * @method copy
+ * @param  {AABB} aabb Source to copy from
+ * @return {AABB} The this object, for chainability
+ */
+AABB.prototype.copy = function(aabb){
+    this.lowerBound.copy(aabb.lowerBound);
+    this.upperBound.copy(aabb.upperBound);
+    return this;
+};
+
+/**
+ * Clone an AABB
+ * @method clone
+ */
+AABB.prototype.clone = function(){
+    return new AABB().copy(this);
+};
+
+/**
+ * Extend this AABB so that it covers the given AABB too.
+ * @method extend
+ * @param  {AABB} aabb
+ */
+AABB.prototype.extend = function(aabb){
+    this.lowerBound.x = Math.min(this.lowerBound.x, aabb.lowerBound.x);
+    this.upperBound.x = Math.max(this.upperBound.x, aabb.upperBound.x);
+    this.lowerBound.y = Math.min(this.lowerBound.y, aabb.lowerBound.y);
+    this.upperBound.y = Math.max(this.upperBound.y, aabb.upperBound.y);
+    this.lowerBound.z = Math.min(this.lowerBound.z, aabb.lowerBound.z);
+    this.upperBound.z = Math.max(this.upperBound.z, aabb.upperBound.z);
+};
+
+/**
+ * Returns true if the given AABB overlaps this AABB.
+ * @method overlaps
+ * @param  {AABB} aabb
+ * @return {Boolean}
+ */
+AABB.prototype.overlaps = function(aabb){
+    var l1 = this.lowerBound,
+        u1 = this.upperBound,
+        l2 = aabb.lowerBound,
+        u2 = aabb.upperBound;
+
+    //      l2        u2
+    //      |---------|
+    // |--------|
+    // l1       u1
+
+    var overlapsX = ((l2.x <= u1.x && u1.x <= u2.x) || (l1.x <= u2.x && u2.x <= u1.x));
+    var overlapsY = ((l2.y <= u1.y && u1.y <= u2.y) || (l1.y <= u2.y && u2.y <= u1.y));
+    var overlapsZ = ((l2.z <= u1.z && u1.z <= u2.z) || (l1.z <= u2.z && u2.z <= u1.z));
+
+    return overlapsX && overlapsY && overlapsZ;
+};
+
+// Mostly for debugging
+AABB.prototype.volume = function(){
+    var l = this.lowerBound,
+        u = this.upperBound;
+    return (u.x - l.x) * (u.y - l.y) * (u.z - l.z);
+};
+
+
+/**
+ * Returns true if the given AABB is fully contained in this AABB.
+ * @method contains
+ * @param {AABB} aabb
+ * @return {Boolean}
+ */
+AABB.prototype.contains = function(aabb){
+    var l1 = this.lowerBound,
+        u1 = this.upperBound,
+        l2 = aabb.lowerBound,
+        u2 = aabb.upperBound;
+
+    //      l2        u2
+    //      |---------|
+    // |---------------|
+    // l1              u1
+
+    return (
+        (l1.x <= l2.x && u1.x >= u2.x) &&
+        (l1.y <= l2.y && u1.y >= u2.y) &&
+        (l1.z <= l2.z && u1.z >= u2.z)
+    );
+};
+
+/**
+ * @method getCorners
+ * @param {Vec3} a
+ * @param {Vec3} b
+ * @param {Vec3} c
+ * @param {Vec3} d
+ * @param {Vec3} e
+ * @param {Vec3} f
+ * @param {Vec3} g
+ * @param {Vec3} h
+ */
+AABB.prototype.getCorners = function(a, b, c, d, e, f, g, h){
+    var l = this.lowerBound,
+        u = this.upperBound;
+
+    a.copy(l);
+    b.set( u.x, l.y, l.z );
+    c.set( u.x, u.y, l.z );
+    d.set( l.x, u.y, u.z );
+    e.set( u.x, l.y, l.z );
+    f.set( l.x, u.y, l.z );
+    g.set( l.x, l.y, u.z );
+    h.copy(u);
+};
+
+var transformIntoFrame_corners = [
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3()
+];
+
+/**
+ * Get the representation of an AABB in another frame.
+ * @method toLocalFrame
+ * @param  {Transform} frame
+ * @param  {AABB} target
+ * @return {AABB} The "target" AABB object.
+ */
+AABB.prototype.toLocalFrame = function(frame, target){
+
+    var corners = transformIntoFrame_corners;
+    var a = corners[0];
+    var b = corners[1];
+    var c = corners[2];
+    var d = corners[3];
+    var e = corners[4];
+    var f = corners[5];
+    var g = corners[6];
+    var h = corners[7];
+
+    // Get corners in current frame
+    this.getCorners(a, b, c, d, e, f, g, h);
+
+    // Transform them to new local frame
+    for(var i=0; i !== 8; i++){
+        var corner = corners[i];
+        frame.pointToLocal(corner, corner);
+    }
+
+    return target.setFromPoints(corners);
+};
+
+/**
+ * Get the representation of an AABB in the global frame.
+ * @method toWorldFrame
+ * @param  {Transform} frame
+ * @param  {AABB} target
+ * @return {AABB} The "target" AABB object.
+ */
+AABB.prototype.toWorldFrame = function(frame, target){
+
+    var corners = transformIntoFrame_corners;
+    var a = corners[0];
+    var b = corners[1];
+    var c = corners[2];
+    var d = corners[3];
+    var e = corners[4];
+    var f = corners[5];
+    var g = corners[6];
+    var h = corners[7];
+
+    // Get corners in current frame
+    this.getCorners(a, b, c, d, e, f, g, h);
+
+    // Transform them to new local frame
+    for(var i=0; i !== 8; i++){
+        var corner = corners[i];
+        frame.pointToWorld(corner, corner);
+    }
+
+    return target.setFromPoints(corners);
+};
+
+/**
+ * Check if the AABB is hit by a ray.
+ * @param  {Ray} ray
+ * @return {number}
+ */
+AABB.prototype.overlapsRay = function(ray){
+    var t = 0;
+
+    // ray.direction is unit direction vector of ray
+    var dirFracX = 1 / ray._direction.x;
+    var dirFracY = 1 / ray._direction.y;
+    var dirFracZ = 1 / ray._direction.z;
+
+    // this.lowerBound is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner
+    var t1 = (this.lowerBound.x - ray.from.x) * dirFracX;
+    var t2 = (this.upperBound.x - ray.from.x) * dirFracX;
+    var t3 = (this.lowerBound.y - ray.from.y) * dirFracY;
+    var t4 = (this.upperBound.y - ray.from.y) * dirFracY;
+    var t5 = (this.lowerBound.z - ray.from.z) * dirFracZ;
+    var t6 = (this.upperBound.z - ray.from.z) * dirFracZ;
+
+    // var tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)));
+    // var tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)));
+    var tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6));
+    var tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6));
+
+    // if tmax < 0, ray (line) is intersecting AABB, but whole AABB is behing us
+    if (tmax < 0){
+        //t = tmax;
+        return false;
+    }
+
+    // if tmin > tmax, ray doesn't intersect AABB
+    if (tmin > tmax){
+        //t = tmax;
+        return false;
+    }
+
+    return true;
+};
+},{"../math/Vec3":46,"../utils/Utils":69}],19:[function(require,module,exports){
+module.exports = ArrayCollisionMatrix;
+
+/**
+ * Collision "matrix". It's actually a triangular-shaped array of whether two bodies are touching this step, for reference next step
+ * @class ArrayCollisionMatrix
+ * @constructor
+ */
+function ArrayCollisionMatrix() {
+
+    /**
+     * The matrix storage
+     * @property matrix
+     * @type {Array}
+     */
+    this.matrix = [];
+}
+
+/**
+ * Get an element
+ * @method get
+ * @param  {Number} i
+ * @param  {Number} j
+ * @return {Number}
+ */
+ArrayCollisionMatrix.prototype.get = function(i, j) {
+    i = i.index;
+    j = j.index;
+    if (j > i) {
+        var temp = j;
+        j = i;
+        i = temp;
+    }
+    return this.matrix[(i*(i + 1)>>1) + j-1];
+};
+
+/**
+ * Set an element
+ * @method set
+ * @param {Number} i
+ * @param {Number} j
+ * @param {Number} value
+ */
+ArrayCollisionMatrix.prototype.set = function(i, j, value) {
+    i = i.index;
+    j = j.index;
+    if (j > i) {
+        var temp = j;
+        j = i;
+        i = temp;
+    }
+    this.matrix[(i*(i + 1)>>1) + j-1] = value ? 1 : 0;
+};
+
+/**
+ * Sets all elements to zero
+ * @method reset
+ */
+ArrayCollisionMatrix.prototype.reset = function() {
+    for (var i=0, l=this.matrix.length; i!==l; i++) {
+        this.matrix[i]=0;
+    }
+};
+
+/**
+ * Sets the max number of objects
+ * @method setNumObjects
+ * @param {Number} n
+ */
+ArrayCollisionMatrix.prototype.setNumObjects = function(n) {
+    this.matrix.length = n*(n-1)>>1;
+};
+
+},{}],20:[function(require,module,exports){
+var Body = require('../objects/Body');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Shape = require('../shapes/Shape');
+var Plane = require('../shapes/Plane');
+
+module.exports = Broadphase;
+
+/**
+ * Base class for broadphase implementations
+ * @class Broadphase
+ * @constructor
+ * @author schteppe
+ */
+function Broadphase(){
+    /**
+    * The world to search for collisions in.
+    * @property world
+    * @type {World}
+    */
+    this.world = null;
+
+    /**
+     * If set to true, the broadphase uses bounding boxes for intersection test, else it uses bounding spheres.
+     * @property useBoundingBoxes
+     * @type {Boolean}
+     */
+    this.useBoundingBoxes = false;
+
+    /**
+     * Set to true if the objects in the world moved.
+     * @property {Boolean} dirty
+     */
+    this.dirty = true;
+}
+
+/**
+ * Get the collision pairs from the world
+ * @method collisionPairs
+ * @param {World} world The world to search in
+ * @param {Array} p1 Empty array to be filled with body objects
+ * @param {Array} p2 Empty array to be filled with body objects
+ */
+Broadphase.prototype.collisionPairs = function(world,p1,p2){
+    throw new Error("collisionPairs not implemented for this BroadPhase class!");
+};
+
+/**
+ * Check if a body pair needs to be intersection tested at all.
+ * @method needBroadphaseCollision
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @return {bool}
+ */
+Broadphase.prototype.needBroadphaseCollision = function(bodyA,bodyB){
+
+    // Check collision filter masks
+    if( (bodyA.collisionFilterGroup & bodyB.collisionFilterMask)===0 || (bodyB.collisionFilterGroup & bodyA.collisionFilterMask)===0){
+        return false;
+    }
+
+    // Check types
+    if(((bodyA.type & Body.STATIC)!==0 || bodyA.sleepState === Body.SLEEPING) &&
+       ((bodyB.type & Body.STATIC)!==0 || bodyB.sleepState === Body.SLEEPING)) {
+        // Both bodies are static or sleeping. Skip.
+        return false;
+    }
+
+    return true;
+};
+
+/**
+ * Check if the bounding volumes of two bodies intersect.
+ * @method intersectionTest
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {array} pairs1
+ * @param {array} pairs2
+  */
+Broadphase.prototype.intersectionTest = function(bodyA, bodyB, pairs1, pairs2){
+    if(this.useBoundingBoxes){
+        this.doBoundingBoxBroadphase(bodyA,bodyB,pairs1,pairs2);
+    } else {
+        this.doBoundingSphereBroadphase(bodyA,bodyB,pairs1,pairs2);
+    }
+};
+
+/**
+ * Check if the bounding spheres of two bodies are intersecting.
+ * @method doBoundingSphereBroadphase
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Array} pairs1 bodyA is appended to this array if intersection
+ * @param {Array} pairs2 bodyB is appended to this array if intersection
+ */
+var Broadphase_collisionPairs_r = new Vec3(), // Temp objects
+    Broadphase_collisionPairs_normal =  new Vec3(),
+    Broadphase_collisionPairs_quat =  new Quaternion(),
+    Broadphase_collisionPairs_relpos  =  new Vec3();
+Broadphase.prototype.doBoundingSphereBroadphase = function(bodyA,bodyB,pairs1,pairs2){
+    var r = Broadphase_collisionPairs_r;
+    bodyB.position.vsub(bodyA.position,r);
+    var boundingRadiusSum2 = Math.pow(bodyA.boundingRadius + bodyB.boundingRadius, 2);
+    var norm2 = r.norm2();
+    if(norm2 < boundingRadiusSum2){
+        pairs1.push(bodyA);
+        pairs2.push(bodyB);
+    }
+};
+
+/**
+ * Check if the bounding boxes of two bodies are intersecting.
+ * @method doBoundingBoxBroadphase
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Array} pairs1
+ * @param {Array} pairs2
+ */
+Broadphase.prototype.doBoundingBoxBroadphase = function(bodyA,bodyB,pairs1,pairs2){
+    if(bodyA.aabbNeedsUpdate){
+        bodyA.computeAABB();
+    }
+    if(bodyB.aabbNeedsUpdate){
+        bodyB.computeAABB();
+    }
+
+    // Check AABB / AABB
+    if(bodyA.aabb.overlaps(bodyB.aabb)){
+        pairs1.push(bodyA);
+        pairs2.push(bodyB);
+    }
+};
+
+/**
+ * Removes duplicate pairs from the pair arrays.
+ * @method makePairsUnique
+ * @param {Array} pairs1
+ * @param {Array} pairs2
+ */
+var Broadphase_makePairsUnique_temp = { keys:[] },
+    Broadphase_makePairsUnique_p1 = [],
+    Broadphase_makePairsUnique_p2 = [];
+Broadphase.prototype.makePairsUnique = function(pairs1,pairs2){
+    var t = Broadphase_makePairsUnique_temp,
+        p1 = Broadphase_makePairsUnique_p1,
+        p2 = Broadphase_makePairsUnique_p2,
+        N = pairs1.length;
+
+    for(var i=0; i!==N; i++){
+        p1[i] = pairs1[i];
+        p2[i] = pairs2[i];
+    }
+
+    pairs1.length = 0;
+    pairs2.length = 0;
+
+    for(var i=0; i!==N; i++){
+        var id1 = p1[i].id,
+            id2 = p2[i].id;
+        var key = id1 < id2 ? id1+","+id2 :  id2+","+id1;
+        t[key] = i;
+        t.keys.push(key);
+    }
+
+    for(var i=0; i!==t.keys.length; i++){
+        var key = t.keys.pop(),
+            pairIndex = t[key];
+        pairs1.push(p1[pairIndex]);
+        pairs2.push(p2[pairIndex]);
+        delete t[key];
+    }
+};
+
+/**
+ * To be implemented by subcasses
+ * @method setWorld
+ * @param {World} world
+ */
+Broadphase.prototype.setWorld = function(world){
+};
+
+/**
+ * Check if the bounding spheres of two bodies overlap.
+ * @method boundingSphereCheck
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @return {boolean}
+ */
+var bsc_dist = new Vec3();
+Broadphase.boundingSphereCheck = function(bodyA,bodyB){
+    var dist = bsc_dist;
+    bodyA.position.vsub(bodyB.position,dist);
+    return Math.pow(bodyA.shape.boundingSphereRadius + bodyB.shape.boundingSphereRadius,2) > dist.norm2();
+};
+
+/**
+ * Returns all the bodies within the AABB.
+ * @method aabbQuery
+ * @param  {World} world
+ * @param  {AABB} aabb
+ * @param  {array} result An array to store resulting bodies in.
+ * @return {array}
+ */
+Broadphase.prototype.aabbQuery = function(world, aabb, result){
+    console.warn('.aabbQuery is not implemented in this Broadphase subclass.');
+    return [];
+};
+},{"../math/Quaternion":44,"../math/Vec3":46,"../objects/Body":47,"../shapes/Plane":58,"../shapes/Shape":59}],21:[function(require,module,exports){
+module.exports = GridBroadphase;
+
+var Broadphase = require('./Broadphase');
+var Vec3 = require('../math/Vec3');
+var Shape = require('../shapes/Shape');
+
+/**
+ * Axis aligned uniform grid broadphase.
+ * @class GridBroadphase
+ * @constructor
+ * @extends Broadphase
+ * @todo Needs support for more than just planes and spheres.
+ * @param {Vec3} aabbMin
+ * @param {Vec3} aabbMax
+ * @param {Number} nx Number of boxes along x
+ * @param {Number} ny Number of boxes along y
+ * @param {Number} nz Number of boxes along z
+ */
+function GridBroadphase(aabbMin,aabbMax,nx,ny,nz){
+    Broadphase.apply(this);
+    this.nx = nx || 10;
+    this.ny = ny || 10;
+    this.nz = nz || 10;
+    this.aabbMin = aabbMin || new Vec3(100,100,100);
+    this.aabbMax = aabbMax || new Vec3(-100,-100,-100);
+	var nbins = this.nx * this.ny * this.nz;
+	if (nbins <= 0) {
+		throw "GridBroadphase: Each dimension's n must be >0";
+	}
+    this.bins = [];
+	this.binLengths = []; //Rather than continually resizing arrays (thrashing the memory), just record length and allow them to grow
+	this.bins.length = nbins;
+	this.binLengths.length = nbins;
+	for (var i=0;i<nbins;i++) {
+		this.bins[i]=[];
+		this.binLengths[i]=0;
+	}
+}
+GridBroadphase.prototype = new Broadphase();
+GridBroadphase.prototype.constructor = GridBroadphase;
+
+/**
+ * Get all the collision pairs in the physics world
+ * @method collisionPairs
+ * @param {World} world
+ * @param {Array} pairs1
+ * @param {Array} pairs2
+ */
+var GridBroadphase_collisionPairs_d = new Vec3();
+var GridBroadphase_collisionPairs_binPos = new Vec3();
+GridBroadphase.prototype.collisionPairs = function(world,pairs1,pairs2){
+    var N = world.numObjects(),
+        bodies = world.bodies;
+
+    var max = this.aabbMax,
+        min = this.aabbMin,
+        nx = this.nx,
+        ny = this.ny,
+        nz = this.nz;
+
+	var xstep = ny*nz;
+	var ystep = nz;
+	var zstep = 1;
+
+    var xmax = max.x,
+        ymax = max.y,
+        zmax = max.z,
+        xmin = min.x,
+        ymin = min.y,
+        zmin = min.z;
+
+    var xmult = nx / (xmax-xmin),
+        ymult = ny / (ymax-ymin),
+        zmult = nz / (zmax-zmin);
+
+    var binsizeX = (xmax - xmin) / nx,
+        binsizeY = (ymax - ymin) / ny,
+        binsizeZ = (zmax - zmin) / nz;
+
+	var binRadius = Math.sqrt(binsizeX*binsizeX + binsizeY*binsizeY + binsizeZ*binsizeZ) * 0.5;
+
+    var types = Shape.types;
+    var SPHERE =            types.SPHERE,
+        PLANE =             types.PLANE,
+        BOX =               types.BOX,
+        COMPOUND =          types.COMPOUND,
+        CONVEXPOLYHEDRON =  types.CONVEXPOLYHEDRON;
+
+    var bins=this.bins,
+		binLengths=this.binLengths,
+        Nbins=this.bins.length;
+
+    // Reset bins
+    for(var i=0; i!==Nbins; i++){
+        binLengths[i] = 0;
+    }
+
+    var ceil = Math.ceil;
+	var min = Math.min;
+	var max = Math.max;
+
+	function addBoxToBins(x0,y0,z0,x1,y1,z1,bi) {
+		var xoff0 = ((x0 - xmin) * xmult)|0,
+			yoff0 = ((y0 - ymin) * ymult)|0,
+			zoff0 = ((z0 - zmin) * zmult)|0,
+			xoff1 = ceil((x1 - xmin) * xmult),
+			yoff1 = ceil((y1 - ymin) * ymult),
+			zoff1 = ceil((z1 - zmin) * zmult);
+
+		if (xoff0 < 0) { xoff0 = 0; } else if (xoff0 >= nx) { xoff0 = nx - 1; }
+		if (yoff0 < 0) { yoff0 = 0; } else if (yoff0 >= ny) { yoff0 = ny - 1; }
+		if (zoff0 < 0) { zoff0 = 0; } else if (zoff0 >= nz) { zoff0 = nz - 1; }
+		if (xoff1 < 0) { xoff1 = 0; } else if (xoff1 >= nx) { xoff1 = nx - 1; }
+		if (yoff1 < 0) { yoff1 = 0; } else if (yoff1 >= ny) { yoff1 = ny - 1; }
+		if (zoff1 < 0) { zoff1 = 0; } else if (zoff1 >= nz) { zoff1 = nz - 1; }
+
+		xoff0 *= xstep;
+		yoff0 *= ystep;
+		zoff0 *= zstep;
+		xoff1 *= xstep;
+		yoff1 *= ystep;
+		zoff1 *= zstep;
+
+		for (var xoff = xoff0; xoff <= xoff1; xoff += xstep) {
+			for (var yoff = yoff0; yoff <= yoff1; yoff += ystep) {
+				for (var zoff = zoff0; zoff <= zoff1; zoff += zstep) {
+					var idx = xoff+yoff+zoff;
+					bins[idx][binLengths[idx]++] = bi;
+				}
+			}
+		}
+	}
+
+    // Put all bodies into the bins
+    for(var i=0; i!==N; i++){
+        var bi = bodies[i];
+        var si = bi.shape;
+
+        switch(si.type){
+        case SPHERE:
+            // Put in bin
+            // check if overlap with other bins
+            var x = bi.position.x,
+                y = bi.position.y,
+                z = bi.position.z;
+            var r = si.radius;
+
+			addBoxToBins(x-r, y-r, z-r, x+r, y+r, z+r, bi);
+            break;
+
+        case PLANE:
+            if(si.worldNormalNeedsUpdate){
+                si.computeWorldNormal(bi.quaternion);
+            }
+            var planeNormal = si.worldNormal;
+
+			//Relative position from origin of plane object to the first bin
+			//Incremented as we iterate through the bins
+			var xreset = xmin + binsizeX*0.5 - bi.position.x,
+				yreset = ymin + binsizeY*0.5 - bi.position.y,
+				zreset = zmin + binsizeZ*0.5 - bi.position.z;
+
+            var d = GridBroadphase_collisionPairs_d;
+			d.set(xreset, yreset, zreset);
+
+			for (var xi = 0, xoff = 0; xi !== nx; xi++, xoff += xstep, d.y = yreset, d.x += binsizeX) {
+				for (var yi = 0, yoff = 0; yi !== ny; yi++, yoff += ystep, d.z = zreset, d.y += binsizeY) {
+					for (var zi = 0, zoff = 0; zi !== nz; zi++, zoff += zstep, d.z += binsizeZ) {
+						if (d.dot(planeNormal) < binRadius) {
+							var idx = xoff + yoff + zoff;
+							bins[idx][binLengths[idx]++] = bi;
+						}
+					}
+				}
+			}
+            break;
+
+        default:
+			if (bi.aabbNeedsUpdate) {
+				bi.computeAABB();
+			}
+
+			addBoxToBins(
+				bi.aabb.lowerBound.x,
+				bi.aabb.lowerBound.y,
+				bi.aabb.lowerBound.z,
+				bi.aabb.upperBound.x,
+				bi.aabb.upperBound.y,
+				bi.aabb.upperBound.z,
+				bi);
+            break;
+        }
+    }
+
+    // Check each bin
+    for(var i=0; i!==Nbins; i++){
+		var binLength = binLengths[i];
+		//Skip bins with no potential collisions
+		if (binLength > 1) {
+			var bin = bins[i];
+
+			// Do N^2 broadphase inside
+			for(var xi=0; xi!==binLength; xi++){
+				var bi = bin[xi];
+				for(var yi=0; yi!==xi; yi++){
+					var bj = bin[yi];
+					if(this.needBroadphaseCollision(bi,bj)){
+						this.intersectionTest(bi,bj,pairs1,pairs2);
+					}
+				}
+			}
+		}
+    }
+
+//	for (var zi = 0, zoff=0; zi < nz; zi++, zoff+= zstep) {
+//		console.log("layer "+zi);
+//		for (var yi = 0, yoff=0; yi < ny; yi++, yoff += ystep) {
+//			var row = '';
+//			for (var xi = 0, xoff=0; xi < nx; xi++, xoff += xstep) {
+//				var idx = xoff + yoff + zoff;
+//				row += ' ' + binLengths[idx];
+//			}
+//			console.log(row);
+//		}
+//	}
+
+    this.makePairsUnique(pairs1,pairs2);
+};
+
+},{"../math/Vec3":46,"../shapes/Shape":59,"./Broadphase":20}],22:[function(require,module,exports){
+module.exports = NaiveBroadphase;
+
+var Broadphase = require('./Broadphase');
+var AABB = require('./AABB');
+
+/**
+ * Naive broadphase implementation, used in lack of better ones.
+ * @class NaiveBroadphase
+ * @constructor
+ * @description The naive broadphase looks at all possible pairs without restriction, therefore it has complexity N^2 (which is bad)
+ * @extends Broadphase
+ */
+function NaiveBroadphase(){
+    Broadphase.apply(this);
+}
+NaiveBroadphase.prototype = new Broadphase();
+NaiveBroadphase.prototype.constructor = NaiveBroadphase;
+
+/**
+ * Get all the collision pairs in the physics world
+ * @method collisionPairs
+ * @param {World} world
+ * @param {Array} pairs1
+ * @param {Array} pairs2
+ */
+NaiveBroadphase.prototype.collisionPairs = function(world,pairs1,pairs2){
+    var bodies = world.bodies,
+        n = bodies.length,
+        i,j,bi,bj;
+
+    // Naive N^2 ftw!
+    for(i=0; i!==n; i++){
+        for(j=0; j!==i; j++){
+
+            bi = bodies[i];
+            bj = bodies[j];
+
+            if(!this.needBroadphaseCollision(bi,bj)){
+                continue;
+            }
+
+            this.intersectionTest(bi,bj,pairs1,pairs2);
+        }
+    }
+};
+
+var tmpAABB = new AABB();
+
+/**
+ * Returns all the bodies within an AABB.
+ * @method aabbQuery
+ * @param  {World} world
+ * @param  {AABB} aabb
+ * @param {array} result An array to store resulting bodies in.
+ * @return {array}
+ */
+NaiveBroadphase.prototype.aabbQuery = function(world, aabb, result){
+    result = result || [];
+
+    for(var i = 0; i < world.bodies.length; i++){
+        var b = world.bodies[i];
+
+        if(b.aabbNeedsUpdate){
+            b.computeAABB();
+        }
+
+        // Ugly hack until Body gets aabb
+        if(b.aabb.overlaps(aabb)){
+            result.push(b);
+        }
+    }
+
+    return result;
+};
+},{"./AABB":18,"./Broadphase":20}],23:[function(require,module,exports){
+module.exports = ObjectCollisionMatrix;
+
+/**
+ * Records what objects are colliding with each other
+ * @class ObjectCollisionMatrix
+ * @constructor
+ */
+function ObjectCollisionMatrix() {
+
+    /**
+     * The matrix storage
+     * @property matrix
+     * @type {Object}
+     */
+	this.matrix = {};
+}
+
+/**
+ * @method get
+ * @param  {Number} i
+ * @param  {Number} j
+ * @return {Number}
+ */
+ObjectCollisionMatrix.prototype.get = function(i, j) {
+	i = i.id;
+	j = j.id;
+    if (j > i) {
+        var temp = j;
+        j = i;
+        i = temp;
+    }
+	return i+'-'+j in this.matrix;
+};
+
+/**
+ * @method set
+ * @param  {Number} i
+ * @param  {Number} j
+ * @param {Number} value
+ */
+ObjectCollisionMatrix.prototype.set = function(i, j, value) {
+	i = i.id;
+	j = j.id;
+    if (j > i) {
+        var temp = j;
+        j = i;
+        i = temp;
+	}
+	if (value) {
+		this.matrix[i+'-'+j] = true;
+	}
+	else {
+		delete this.matrix[i+'-'+j];
+	}
+};
+
+/**
+ * Empty the matrix
+ * @method reset
+ */
+ObjectCollisionMatrix.prototype.reset = function() {
+	this.matrix = {};
+};
+
+/**
+ * Set max number of objects
+ * @method setNumObjects
+ * @param {Number} n
+ */
+ObjectCollisionMatrix.prototype.setNumObjects = function(n) {
+};
+
+},{}],24:[function(require,module,exports){
+module.exports = OverlapKeeper;
+
+/**
+ * @class OverlapKeeper
+ * @constructor
+ */
+function OverlapKeeper() {
+    this.current = [];
+    this.previous = [];
+}
+
+OverlapKeeper.prototype.getKey = function(i, j) {
+    if (j < i) {
+        var temp = j;
+        j = i;
+        i = temp;
+    }
+    return (i << 16) | j;
+};
+
+
+/**
+ * @method set
+ * @param {Number} i
+ * @param {Number} j
+ */
+OverlapKeeper.prototype.set = function(i, j) {
+    // Insertion sort. This way the diff will have linear complexity.
+    var key = this.getKey(i, j);
+    var current = this.current;
+    var index = 0;
+    while(key > current[index]){
+        index++;
+    }
+    if(key === current[index]){
+        return; // Pair was already added
+    }
+    for(var j=current.length-1; j>=index; j--){
+        current[j + 1] = current[j];
+    }
+    current[index] = key;
+};
+
+/**
+ * @method tick
+ */
+OverlapKeeper.prototype.tick = function() {
+    var tmp = this.current;
+    this.current = this.previous;
+    this.previous = tmp;
+    this.current.length = 0;
+};
+
+function unpackAndPush(array, key){
+    array.push((key & 0xFFFF0000) >> 16, key & 0x0000FFFF);
+}
+
+/**
+ * @method getDiff
+ * @param  {array} additions
+ * @param  {array} removals
+ */
+OverlapKeeper.prototype.getDiff = function(additions, removals) {
+    var a = this.current;
+    var b = this.previous;
+    var al = a.length;
+    var bl = b.length;
+
+    var j=0;
+    for (var i = 0; i < al; i++) {
+        var found = false;
+        var keyA = a[i];
+        while(keyA > b[j]){
+            j++;
+        }
+        found = keyA === b[j];
+
+        if(!found){
+            unpackAndPush(additions, keyA);
+        }
+    }
+    j = 0;
+    for (var i = 0; i < bl; i++) {
+        var found = false;
+        var keyB = b[i];
+        while(keyB > a[j]){
+            j++;
+        }
+        found = a[j] === keyB;
+
+        if(!found){
+            unpackAndPush(removals, keyB);
+        }
+    }
+};
+},{}],25:[function(require,module,exports){
+module.exports = Ray;
+
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Transform = require('../math/Transform');
+var ConvexPolyhedron = require('../shapes/ConvexPolyhedron');
+var Box = require('../shapes/Box');
+var RaycastResult = require('../collision/RaycastResult');
+var Shape = require('../shapes/Shape');
+var AABB = require('../collision/AABB');
+
+/**
+ * A line in 3D space that intersects bodies and return points.
+ * @class Ray
+ * @constructor
+ * @param {Vec3} from
+ * @param {Vec3} to
+ */
+function Ray(from, to){
+    /**
+     * @property {Vec3} from
+     */
+    this.from = from ? from.clone() : new Vec3();
+
+    /**
+     * @property {Vec3} to
+     */
+    this.to = to ? to.clone() : new Vec3();
+
+    /**
+     * @private
+     * @property {Vec3} _direction
+     */
+    this._direction = new Vec3();
+
+    /**
+     * The precision of the ray. Used when checking parallelity etc.
+     * @property {Number} precision
+     */
+    this.precision = 0.0001;
+
+    /**
+     * Set to true if you want the Ray to take .collisionResponse flags into account on bodies and shapes.
+     * @property {Boolean} checkCollisionResponse
+     */
+    this.checkCollisionResponse = true;
+
+    /**
+     * If set to true, the ray skips any hits with normal.dot(rayDirection) < 0.
+     * @property {Boolean} skipBackfaces
+     */
+    this.skipBackfaces = false;
+
+    /**
+     * @property {number} collisionFilterMask
+     * @default -1
+     */
+    this.collisionFilterMask = -1;
+
+    /**
+     * @property {number} collisionFilterGroup
+     * @default -1
+     */
+    this.collisionFilterGroup = -1;
+
+    /**
+     * The intersection mode. Should be Ray.ANY, Ray.ALL or Ray.CLOSEST.
+     * @property {number} mode
+     */
+    this.mode = Ray.ANY;
+
+    /**
+     * Current result object.
+     * @property {RaycastResult} result
+     */
+    this.result = new RaycastResult();
+
+    /**
+     * Will be set to true during intersectWorld() if the ray hit anything.
+     * @property {Boolean} hasHit
+     */
+    this.hasHit = false;
+
+    /**
+     * Current, user-provided result callback. Will be used if mode is Ray.ALL.
+     * @property {Function} callback
+     */
+    this.callback = function(result){};
+}
+Ray.prototype.constructor = Ray;
+
+Ray.CLOSEST = 1;
+Ray.ANY = 2;
+Ray.ALL = 4;
+
+var tmpAABB = new AABB();
+var tmpArray = [];
+
+/**
+ * Do itersection against all bodies in the given World.
+ * @method intersectWorld
+ * @param  {World} world
+ * @param  {object} options
+ * @return {Boolean} True if the ray hit anything, otherwise false.
+ */
+Ray.prototype.intersectWorld = function (world, options) {
+    this.mode = options.mode || Ray.ANY;
+    this.result = options.result || new RaycastResult();
+    this.skipBackfaces = !!options.skipBackfaces;
+    this.collisionFilterMask = typeof(options.collisionFilterMask) !== 'undefined' ? options.collisionFilterMask : -1;
+    this.collisionFilterGroup = typeof(options.collisionFilterGroup) !== 'undefined' ? options.collisionFilterGroup : -1;
+    if(options.from){
+        this.from.copy(options.from);
+    }
+    if(options.to){
+        this.to.copy(options.to);
+    }
+    this.callback = options.callback || function(){};
+    this.hasHit = false;
+
+    this.result.reset();
+    this._updateDirection();
+
+    this.getAABB(tmpAABB);
+    tmpArray.length = 0;
+    world.broadphase.aabbQuery(world, tmpAABB, tmpArray);
+    this.intersectBodies(tmpArray);
+
+    return this.hasHit;
+};
+
+var v1 = new Vec3(),
+    v2 = new Vec3();
+
+/*
+ * As per "Barycentric Technique" as named here http://www.blackpawn.com/texts/pointinpoly/default.html But without the division
+ */
+Ray.pointInTriangle = pointInTriangle;
+function pointInTriangle(p, a, b, c) {
+    c.vsub(a,v0);
+    b.vsub(a,v1);
+    p.vsub(a,v2);
+
+    var dot00 = v0.dot( v0 );
+    var dot01 = v0.dot( v1 );
+    var dot02 = v0.dot( v2 );
+    var dot11 = v1.dot( v1 );
+    var dot12 = v1.dot( v2 );
+
+    var u,v;
+
+    return  ( (u = dot11 * dot02 - dot01 * dot12) >= 0 ) &&
+            ( (v = dot00 * dot12 - dot01 * dot02) >= 0 ) &&
+            ( u + v < ( dot00 * dot11 - dot01 * dot01 ) );
+}
+
+/**
+ * Shoot a ray at a body, get back information about the hit.
+ * @method intersectBody
+ * @private
+ * @param {Body} body
+ * @param {RaycastResult} [result] Deprecated - set the result property of the Ray instead.
+ */
+var intersectBody_xi = new Vec3();
+var intersectBody_qi = new Quaternion();
+Ray.prototype.intersectBody = function (body, result) {
+    if(result){
+        this.result = result;
+        this._updateDirection();
+    }
+    var checkCollisionResponse = this.checkCollisionResponse;
+
+    if(checkCollisionResponse && !body.collisionResponse){
+        return;
+    }
+
+    if((this.collisionFilterGroup & body.collisionFilterMask)===0 || (body.collisionFilterGroup & this.collisionFilterMask)===0){
+        return;
+    }
+
+    var xi = intersectBody_xi;
+    var qi = intersectBody_qi;
+
+    for (var i = 0, N = body.shapes.length; i < N; i++) {
+        var shape = body.shapes[i];
+
+        if(checkCollisionResponse && !shape.collisionResponse){
+            continue; // Skip
+        }
+
+        body.quaternion.mult(body.shapeOrientations[i], qi);
+        body.quaternion.vmult(body.shapeOffsets[i], xi);
+        xi.vadd(body.position, xi);
+
+        this.intersectShape(
+            shape,
+            qi,
+            xi,
+            body
+        );
+
+        if(this.result._shouldStop){
+            break;
+        }
+    }
+};
+
+/**
+ * @method intersectBodies
+ * @param {Array} bodies An array of Body objects.
+ * @param {RaycastResult} [result] Deprecated
+ */
+Ray.prototype.intersectBodies = function (bodies, result) {
+    if(result){
+        this.result = result;
+        this._updateDirection();
+    }
+
+    for ( var i = 0, l = bodies.length; !this.result._shouldStop && i < l; i ++ ) {
+        this.intersectBody(bodies[i]);
+    }
+};
+
+/**
+ * Updates the _direction vector.
+ * @private
+ * @method _updateDirection
+ */
+Ray.prototype._updateDirection = function(){
+    this.to.vsub(this.from, this._direction);
+    this._direction.normalize();
+};
+
+/**
+ * @method intersectShape
+ * @private
+ * @param {Shape} shape
+ * @param {Quaternion} quat
+ * @param {Vec3} position
+ * @param {Body} body
+ */
+Ray.prototype.intersectShape = function(shape, quat, position, body){
+    var from = this.from;
+
+
+    // Checking boundingSphere
+    var distance = distanceFromIntersection(from, this._direction, position);
+    if ( distance > shape.boundingSphereRadius ) {
+        return;
+    }
+
+    var intersectMethod = this[shape.type];
+    if(intersectMethod){
+        intersectMethod.call(this, shape, quat, position, body, shape);
+    }
+};
+
+var vector = new Vec3();
+var normal = new Vec3();
+var intersectPoint = new Vec3();
+
+var a = new Vec3();
+var b = new Vec3();
+var c = new Vec3();
+var d = new Vec3();
+
+var tmpRaycastResult = new RaycastResult();
+
+/**
+ * @method intersectBox
+ * @private
+ * @param  {Shape} shape
+ * @param  {Quaternion} quat
+ * @param  {Vec3} position
+ * @param  {Body} body
+ */
+Ray.prototype.intersectBox = function(shape, quat, position, body, reportedShape){
+    return this.intersectConvex(shape.convexPolyhedronRepresentation, quat, position, body, reportedShape);
+};
+Ray.prototype[Shape.types.BOX] = Ray.prototype.intersectBox;
+
+/**
+ * @method intersectPlane
+ * @private
+ * @param  {Shape} shape
+ * @param  {Quaternion} quat
+ * @param  {Vec3} position
+ * @param  {Body} body
+ */
+Ray.prototype.intersectPlane = function(shape, quat, position, body, reportedShape){
+    var from = this.from;
+    var to = this.to;
+    var direction = this._direction;
+
+    // Get plane normal
+    var worldNormal = new Vec3(0, 0, 1);
+    quat.vmult(worldNormal, worldNormal);
+
+    var len = new Vec3();
+    from.vsub(position, len);
+    var planeToFrom = len.dot(worldNormal);
+    to.vsub(position, len);
+    var planeToTo = len.dot(worldNormal);
+
+    if(planeToFrom * planeToTo > 0){
+        // "from" and "to" are on the same side of the plane... bail out
+        return;
+    }
+
+    if(from.distanceTo(to) < planeToFrom){
+        return;
+    }
+
+    var n_dot_dir = worldNormal.dot(direction);
+
+    if (Math.abs(n_dot_dir) < this.precision) {
+        // No intersection
+        return;
+    }
+
+    var planePointToFrom = new Vec3();
+    var dir_scaled_with_t = new Vec3();
+    var hitPointWorld = new Vec3();
+
+    from.vsub(position, planePointToFrom);
+    var t = -worldNormal.dot(planePointToFrom) / n_dot_dir;
+    direction.scale(t, dir_scaled_with_t);
+    from.vadd(dir_scaled_with_t, hitPointWorld);
+
+    this.reportIntersection(worldNormal, hitPointWorld, reportedShape, body, -1);
+};
+Ray.prototype[Shape.types.PLANE] = Ray.prototype.intersectPlane;
+
+/**
+ * Get the world AABB of the ray.
+ * @method getAABB
+ * @param  {AABB} aabb
+ */
+Ray.prototype.getAABB = function(result){
+    var to = this.to;
+    var from = this.from;
+    result.lowerBound.x = Math.min(to.x, from.x);
+    result.lowerBound.y = Math.min(to.y, from.y);
+    result.lowerBound.z = Math.min(to.z, from.z);
+    result.upperBound.x = Math.max(to.x, from.x);
+    result.upperBound.y = Math.max(to.y, from.y);
+    result.upperBound.z = Math.max(to.z, from.z);
+};
+
+var intersectConvexOptions = {
+    faceList: [0]
+};
+var worldPillarOffset = new Vec3();
+var intersectHeightfield_localRay = new Ray();
+var intersectHeightfield_index = [];
+var intersectHeightfield_minMax = [];
+
+/**
+ * @method intersectHeightfield
+ * @private
+ * @param  {Shape} shape
+ * @param  {Quaternion} quat
+ * @param  {Vec3} position
+ * @param  {Body} body
+ */
+Ray.prototype.intersectHeightfield = function(shape, quat, position, body, reportedShape){
+    var data = shape.data,
+        w = shape.elementSize;
+
+    // Convert the ray to local heightfield coordinates
+    var localRay = intersectHeightfield_localRay; //new Ray(this.from, this.to);
+    localRay.from.copy(this.from);
+    localRay.to.copy(this.to);
+    Transform.pointToLocalFrame(position, quat, localRay.from, localRay.from);
+    Transform.pointToLocalFrame(position, quat, localRay.to, localRay.to);
+    localRay._updateDirection();
+
+    // Get the index of the data points to test against
+    var index = intersectHeightfield_index;
+    var iMinX, iMinY, iMaxX, iMaxY;
+
+    // Set to max
+    iMinX = iMinY = 0;
+    iMaxX = iMaxY = shape.data.length - 1;
+
+    var aabb = new AABB();
+    localRay.getAABB(aabb);
+
+    shape.getIndexOfPosition(aabb.lowerBound.x, aabb.lowerBound.y, index, true);
+    iMinX = Math.max(iMinX, index[0]);
+    iMinY = Math.max(iMinY, index[1]);
+    shape.getIndexOfPosition(aabb.upperBound.x, aabb.upperBound.y, index, true);
+    iMaxX = Math.min(iMaxX, index[0] + 1);
+    iMaxY = Math.min(iMaxY, index[1] + 1);
+
+    for(var i = iMinX; i < iMaxX; i++){
+        for(var j = iMinY; j < iMaxY; j++){
+
+            if(this.result._shouldStop){
+                return;
+            }
+
+            shape.getAabbAtIndex(i, j, aabb);
+            if(!aabb.overlapsRay(localRay)){
+                continue;
+            }
+
+            // Lower triangle
+            shape.getConvexTrianglePillar(i, j, false);
+            Transform.pointToWorldFrame(position, quat, shape.pillarOffset, worldPillarOffset);
+            this.intersectConvex(shape.pillarConvex, quat, worldPillarOffset, body, reportedShape, intersectConvexOptions);
+
+            if(this.result._shouldStop){
+                return;
+            }
+
+            // Upper triangle
+            shape.getConvexTrianglePillar(i, j, true);
+            Transform.pointToWorldFrame(position, quat, shape.pillarOffset, worldPillarOffset);
+            this.intersectConvex(shape.pillarConvex, quat, worldPillarOffset, body, reportedShape, intersectConvexOptions);
+        }
+    }
+};
+Ray.prototype[Shape.types.HEIGHTFIELD] = Ray.prototype.intersectHeightfield;
+
+var Ray_intersectSphere_intersectionPoint = new Vec3();
+var Ray_intersectSphere_normal = new Vec3();
+
+/**
+ * @method intersectSphere
+ * @private
+ * @param  {Shape} shape
+ * @param  {Quaternion} quat
+ * @param  {Vec3} position
+ * @param  {Body} body
+ */
+Ray.prototype.intersectSphere = function(shape, quat, position, body, reportedShape){
+    var from = this.from,
+        to = this.to,
+        r = shape.radius;
+
+    var a = Math.pow(to.x - from.x, 2) + Math.pow(to.y - from.y, 2) + Math.pow(to.z - from.z, 2);
+    var b = 2 * ((to.x - from.x) * (from.x - position.x) + (to.y - from.y) * (from.y - position.y) + (to.z - from.z) * (from.z - position.z));
+    var c = Math.pow(from.x - position.x, 2) + Math.pow(from.y - position.y, 2) + Math.pow(from.z - position.z, 2) - Math.pow(r, 2);
+
+    var delta = Math.pow(b, 2) - 4 * a * c;
+
+    var intersectionPoint = Ray_intersectSphere_intersectionPoint;
+    var normal = Ray_intersectSphere_normal;
+
+    if(delta < 0){
+        // No intersection
+        return;
+
+    } else if(delta === 0){
+        // single intersection point
+        from.lerp(to, delta, intersectionPoint);
+
+        intersectionPoint.vsub(position, normal);
+        normal.normalize();
+
+        this.reportIntersection(normal, intersectionPoint, reportedShape, body, -1);
+
+    } else {
+        var d1 = (- b - Math.sqrt(delta)) / (2 * a);
+        var d2 = (- b + Math.sqrt(delta)) / (2 * a);
+
+        if(d1 >= 0 && d1 <= 1){
+            from.lerp(to, d1, intersectionPoint);
+            intersectionPoint.vsub(position, normal);
+            normal.normalize();
+            this.reportIntersection(normal, intersectionPoint, reportedShape, body, -1);
+        }
+
+        if(this.result._shouldStop){
+            return;
+        }
+
+        if(d2 >= 0 && d2 <= 1){
+            from.lerp(to, d2, intersectionPoint);
+            intersectionPoint.vsub(position, normal);
+            normal.normalize();
+            this.reportIntersection(normal, intersectionPoint, reportedShape, body, -1);
+        }
+    }
+};
+Ray.prototype[Shape.types.SPHERE] = Ray.prototype.intersectSphere;
+
+
+var intersectConvex_normal = new Vec3();
+var intersectConvex_minDistNormal = new Vec3();
+var intersectConvex_minDistIntersect = new Vec3();
+var intersectConvex_vector = new Vec3();
+
+/**
+ * @method intersectConvex
+ * @private
+ * @param  {Shape} shape
+ * @param  {Quaternion} quat
+ * @param  {Vec3} position
+ * @param  {Body} body
+ * @param {object} [options]
+ * @param {array} [options.faceList]
+ */
+Ray.prototype.intersectConvex = function intersectConvex(
+    shape,
+    quat,
+    position,
+    body,
+    reportedShape,
+    options
+){
+    var minDistNormal = intersectConvex_minDistNormal;
+    var normal = intersectConvex_normal;
+    var vector = intersectConvex_vector;
+    var minDistIntersect = intersectConvex_minDistIntersect;
+    var faceList = (options && options.faceList) || null;
+
+    // Checking faces
+    var faces = shape.faces,
+        vertices = shape.vertices,
+        normals = shape.faceNormals;
+    var direction = this._direction;
+
+    var from = this.from;
+    var to = this.to;
+    var fromToDistance = from.distanceTo(to);
+
+    var minDist = -1;
+    var Nfaces = faceList ? faceList.length : faces.length;
+    var result = this.result;
+
+    for (var j = 0; !result._shouldStop && j < Nfaces; j++) {
+        var fi = faceList ? faceList[j] : j;
+
+        var face = faces[fi];
+        var faceNormal = normals[fi];
+        var q = quat;
+        var x = position;
+
+        // determine if ray intersects the plane of the face
+        // note: this works regardless of the direction of the face normal
+
+        // Get plane point in world coordinates...
+        vector.copy(vertices[face[0]]);
+        q.vmult(vector,vector);
+        vector.vadd(x,vector);
+
+        // ...but make it relative to the ray from. We'll fix this later.
+        vector.vsub(from,vector);
+
+        // Get plane normal
+        q.vmult(faceNormal,normal);
+
+        // If this dot product is negative, we have something interesting
+        var dot = direction.dot(normal);
+
+        // Bail out if ray and plane are parallel
+        if ( Math.abs( dot ) < this.precision ){
+            continue;
+        }
+
+        // calc distance to plane
+        var scalar = normal.dot(vector) / dot;
+
+        // if negative distance, then plane is behind ray
+        if (scalar < 0){
+            continue;
+        }
+
+        // if (dot < 0) {
+
+        // Intersection point is from + direction * scalar
+        direction.mult(scalar,intersectPoint);
+        intersectPoint.vadd(from,intersectPoint);
+
+        // a is the point we compare points b and c with.
+        a.copy(vertices[face[0]]);
+        q.vmult(a,a);
+        x.vadd(a,a);
+
+        for(var i = 1; !result._shouldStop && i < face.length - 1; i++){
+            // Transform 3 vertices to world coords
+            b.copy(vertices[face[i]]);
+            c.copy(vertices[face[i+1]]);
+            q.vmult(b,b);
+            q.vmult(c,c);
+            x.vadd(b,b);
+            x.vadd(c,c);
+
+            var distance = intersectPoint.distanceTo(from);
+
+            if(!(pointInTriangle(intersectPoint, a, b, c) || pointInTriangle(intersectPoint, b, a, c)) || distance > fromToDistance){
+                continue;
+            }
+
+            this.reportIntersection(normal, intersectPoint, reportedShape, body, fi);
+        }
+        // }
+    }
+};
+Ray.prototype[Shape.types.CONVEXPOLYHEDRON] = Ray.prototype.intersectConvex;
+
+var intersectTrimesh_normal = new Vec3();
+var intersectTrimesh_localDirection = new Vec3();
+var intersectTrimesh_localFrom = new Vec3();
+var intersectTrimesh_localTo = new Vec3();
+var intersectTrimesh_worldNormal = new Vec3();
+var intersectTrimesh_worldIntersectPoint = new Vec3();
+var intersectTrimesh_localAABB = new AABB();
+var intersectTrimesh_triangles = [];
+var intersectTrimesh_treeTransform = new Transform();
+
+/**
+ * @method intersectTrimesh
+ * @private
+ * @param  {Shape} shape
+ * @param  {Quaternion} quat
+ * @param  {Vec3} position
+ * @param  {Body} body
+ * @param {object} [options]
+ * @todo Optimize by transforming the world to local space first.
+ * @todo Use Octree lookup
+ */
+Ray.prototype.intersectTrimesh = function intersectTrimesh(
+    mesh,
+    quat,
+    position,
+    body,
+    reportedShape,
+    options
+){
+    var normal = intersectTrimesh_normal;
+    var triangles = intersectTrimesh_triangles;
+    var treeTransform = intersectTrimesh_treeTransform;
+    var minDistNormal = intersectConvex_minDistNormal;
+    var vector = intersectConvex_vector;
+    var minDistIntersect = intersectConvex_minDistIntersect;
+    var localAABB = intersectTrimesh_localAABB;
+    var localDirection = intersectTrimesh_localDirection;
+    var localFrom = intersectTrimesh_localFrom;
+    var localTo = intersectTrimesh_localTo;
+    var worldIntersectPoint = intersectTrimesh_worldIntersectPoint;
+    var worldNormal = intersectTrimesh_worldNormal;
+    var faceList = (options && options.faceList) || null;
+
+    // Checking faces
+    var indices = mesh.indices,
+        vertices = mesh.vertices,
+        normals = mesh.faceNormals;
+
+    var from = this.from;
+    var to = this.to;
+    var direction = this._direction;
+
+    var minDist = -1;
+    treeTransform.position.copy(position);
+    treeTransform.quaternion.copy(quat);
+
+    // Transform ray to local space!
+    Transform.vectorToLocalFrame(position, quat, direction, localDirection);
+    Transform.pointToLocalFrame(position, quat, from, localFrom);
+    Transform.pointToLocalFrame(position, quat, to, localTo);
+
+    localTo.x *= mesh.scale.x;
+    localTo.y *= mesh.scale.y;
+    localTo.z *= mesh.scale.z;
+    localFrom.x *= mesh.scale.x;
+    localFrom.y *= mesh.scale.y;
+    localFrom.z *= mesh.scale.z;
+
+    localTo.vsub(localFrom, localDirection);
+    localDirection.normalize();
+
+    var fromToDistanceSquared = localFrom.distanceSquared(localTo);
+
+    mesh.tree.rayQuery(this, treeTransform, triangles);
+
+    for (var i = 0, N = triangles.length; !this.result._shouldStop && i !== N; i++) {
+        var trianglesIndex = triangles[i];
+
+        mesh.getNormal(trianglesIndex, normal);
+
+        // determine if ray intersects the plane of the face
+        // note: this works regardless of the direction of the face normal
+
+        // Get plane point in world coordinates...
+        mesh.getVertex(indices[trianglesIndex * 3], a);
+
+        // ...but make it relative to the ray from. We'll fix this later.
+        a.vsub(localFrom,vector);
+
+        // If this dot product is negative, we have something interesting
+        var dot = localDirection.dot(normal);
+
+        // Bail out if ray and plane are parallel
+        // if (Math.abs( dot ) < this.precision){
+        //     continue;
+        // }
+
+        // calc distance to plane
+        var scalar = normal.dot(vector) / dot;
+
+        // if negative distance, then plane is behind ray
+        if (scalar < 0){
+            continue;
+        }
+
+        // Intersection point is from + direction * scalar
+        localDirection.scale(scalar,intersectPoint);
+        intersectPoint.vadd(localFrom,intersectPoint);
+
+        // Get triangle vertices
+        mesh.getVertex(indices[trianglesIndex * 3 + 1], b);
+        mesh.getVertex(indices[trianglesIndex * 3 + 2], c);
+
+        var squaredDistance = intersectPoint.distanceSquared(localFrom);
+
+        if(!(pointInTriangle(intersectPoint, b, a, c) || pointInTriangle(intersectPoint, a, b, c)) || squaredDistance > fromToDistanceSquared){
+            continue;
+        }
+
+        // transform intersectpoint and normal to world
+        Transform.vectorToWorldFrame(quat, normal, worldNormal);
+        Transform.pointToWorldFrame(position, quat, intersectPoint, worldIntersectPoint);
+        this.reportIntersection(worldNormal, worldIntersectPoint, reportedShape, body, trianglesIndex);
+    }
+    triangles.length = 0;
+};
+Ray.prototype[Shape.types.TRIMESH] = Ray.prototype.intersectTrimesh;
+
+
+/**
+ * @method reportIntersection
+ * @private
+ * @param  {Vec3} normal
+ * @param  {Vec3} hitPointWorld
+ * @param  {Shape} shape
+ * @param  {Body} body
+ * @return {boolean} True if the intersections should continue
+ */
+Ray.prototype.reportIntersection = function(normal, hitPointWorld, shape, body, hitFaceIndex){
+    var from = this.from;
+    var to = this.to;
+    var distance = from.distanceTo(hitPointWorld);
+    var result = this.result;
+
+    // Skip back faces?
+    if(this.skipBackfaces && normal.dot(this._direction) > 0){
+        return;
+    }
+
+    result.hitFaceIndex = typeof(hitFaceIndex) !== 'undefined' ? hitFaceIndex : -1;
+
+    switch(this.mode){
+    case Ray.ALL:
+        this.hasHit = true;
+        result.set(
+            from,
+            to,
+            normal,
+            hitPointWorld,
+            shape,
+            body,
+            distance
+        );
+        result.hasHit = true;
+        this.callback(result);
+        break;
+
+    case Ray.CLOSEST:
+
+        // Store if closer than current closest
+        if(distance < result.distance || !result.hasHit){
+            this.hasHit = true;
+            result.hasHit = true;
+            result.set(
+                from,
+                to,
+                normal,
+                hitPointWorld,
+                shape,
+                body,
+                distance
+            );
+        }
+        break;
+
+    case Ray.ANY:
+
+        // Report and stop.
+        this.hasHit = true;
+        result.hasHit = true;
+        result.set(
+            from,
+            to,
+            normal,
+            hitPointWorld,
+            shape,
+            body,
+            distance
+        );
+        result._shouldStop = true;
+        break;
+    }
+};
+
+var v0 = new Vec3(),
+    intersect = new Vec3();
+function distanceFromIntersection(from, direction, position) {
+
+    // v0 is vector from from to position
+    position.vsub(from,v0);
+    var dot = v0.dot(direction);
+
+    // intersect = direction*dot + from
+    direction.mult(dot,intersect);
+    intersect.vadd(from,intersect);
+
+    var distance = position.distanceTo(intersect);
+
+    return distance;
+}
+
+
+},{"../collision/AABB":18,"../collision/RaycastResult":26,"../math/Quaternion":44,"../math/Transform":45,"../math/Vec3":46,"../shapes/Box":53,"../shapes/ConvexPolyhedron":54,"../shapes/Shape":59}],26:[function(require,module,exports){
+var Vec3 = require('../math/Vec3');
+
+module.exports = RaycastResult;
+
+/**
+ * Storage for Ray casting data.
+ * @class RaycastResult
+ * @constructor
+ */
+function RaycastResult(){
+
+	/**
+	 * @property {Vec3} rayFromWorld
+	 */
+	this.rayFromWorld = new Vec3();
+
+	/**
+	 * @property {Vec3} rayToWorld
+	 */
+	this.rayToWorld = new Vec3();
+
+	/**
+	 * @property {Vec3} hitNormalWorld
+	 */
+	this.hitNormalWorld = new Vec3();
+
+	/**
+	 * @property {Vec3} hitPointWorld
+	 */
+	this.hitPointWorld = new Vec3();
+
+	/**
+	 * @property {boolean} hasHit
+	 */
+	this.hasHit = false;
+
+	/**
+	 * The hit shape, or null.
+	 * @property {Shape} shape
+	 */
+	this.shape = null;
+
+	/**
+	 * The hit body, or null.
+	 * @property {Body} body
+	 */
+	this.body = null;
+
+	/**
+	 * The index of the hit triangle, if the hit shape was a trimesh.
+	 * @property {number} hitFaceIndex
+	 * @default -1
+	 */
+	this.hitFaceIndex = -1;
+
+	/**
+	 * Distance to the hit. Will be set to -1 if there was no hit.
+	 * @property {number} distance
+	 * @default -1
+	 */
+	this.distance = -1;
+
+	/**
+	 * If the ray should stop traversing the bodies.
+	 * @private
+	 * @property {Boolean} _shouldStop
+	 * @default false
+	 */
+	this._shouldStop = false;
+}
+
+/**
+ * Reset all result data.
+ * @method reset
+ */
+RaycastResult.prototype.reset = function () {
+	this.rayFromWorld.setZero();
+	this.rayToWorld.setZero();
+	this.hitNormalWorld.setZero();
+	this.hitPointWorld.setZero();
+	this.hasHit = false;
+	this.shape = null;
+	this.body = null;
+	this.hitFaceIndex = -1;
+	this.distance = -1;
+	this._shouldStop = false;
+};
+
+/**
+ * @method abort
+ */
+RaycastResult.prototype.abort = function(){
+	this._shouldStop = true;
+};
+
+/**
+ * @method set
+ * @param {Vec3} rayFromWorld
+ * @param {Vec3} rayToWorld
+ * @param {Vec3} hitNormalWorld
+ * @param {Vec3} hitPointWorld
+ * @param {Shape} shape
+ * @param {Body} body
+ * @param {number} distance
+ */
+RaycastResult.prototype.set = function(
+	rayFromWorld,
+	rayToWorld,
+	hitNormalWorld,
+	hitPointWorld,
+	shape,
+	body,
+	distance
+){
+	this.rayFromWorld.copy(rayFromWorld);
+	this.rayToWorld.copy(rayToWorld);
+	this.hitNormalWorld.copy(hitNormalWorld);
+	this.hitPointWorld.copy(hitPointWorld);
+	this.shape = shape;
+	this.body = body;
+	this.distance = distance;
+};
+},{"../math/Vec3":46}],27:[function(require,module,exports){
+var Shape = require('../shapes/Shape');
+var Broadphase = require('../collision/Broadphase');
+
+module.exports = SAPBroadphase;
+
+/**
+ * Sweep and prune broadphase along one axis.
+ *
+ * @class SAPBroadphase
+ * @constructor
+ * @param {World} [world]
+ * @extends Broadphase
+ */
+function SAPBroadphase(world){
+    Broadphase.apply(this);
+
+    /**
+     * List of bodies currently in the broadphase.
+     * @property axisList
+     * @type {Array}
+     */
+    this.axisList = [];
+
+    /**
+     * The world to search in.
+     * @property world
+     * @type {World}
+     */
+    this.world = null;
+
+    /**
+     * Axis to sort the bodies along. Set to 0 for x axis, and 1 for y axis. For best performance, choose an axis that the bodies are spread out more on.
+     * @property axisIndex
+     * @type {Number}
+     */
+    this.axisIndex = 0;
+
+    var axisList = this.axisList;
+
+    this._addBodyHandler = function(e){
+        axisList.push(e.body);
+    };
+
+    this._removeBodyHandler = function(e){
+        var idx = axisList.indexOf(e.body);
+        if(idx !== -1){
+            axisList.splice(idx,1);
+        }
+    };
+
+    if(world){
+        this.setWorld(world);
+    }
+}
+SAPBroadphase.prototype = new Broadphase();
+
+/**
+ * Change the world
+ * @method setWorld
+ * @param  {World} world
+ */
+SAPBroadphase.prototype.setWorld = function(world){
+    // Clear the old axis array
+    this.axisList.length = 0;
+
+    // Add all bodies from the new world
+    for(var i=0; i<world.bodies.length; i++){
+        this.axisList.push(world.bodies[i]);
+    }
+
+    // Remove old handlers, if any
+    world.removeEventListener("addBody", this._addBodyHandler);
+    world.removeEventListener("removeBody", this._removeBodyHandler);
+
+    // Add handlers to update the list of bodies.
+    world.addEventListener("addBody", this._addBodyHandler);
+    world.addEventListener("removeBody", this._removeBodyHandler);
+
+    this.world = world;
+    this.dirty = true;
+};
+
+/**
+ * @static
+ * @method insertionSortX
+ * @param  {Array} a
+ * @return {Array}
+ */
+SAPBroadphase.insertionSortX = function(a) {
+    for(var i=1,l=a.length;i<l;i++) {
+        var v = a[i];
+        for(var j=i - 1;j>=0;j--) {
+            if(a[j].aabb.lowerBound.x <= v.aabb.lowerBound.x){
+                break;
+            }
+            a[j+1] = a[j];
+        }
+        a[j+1] = v;
+    }
+    return a;
+};
+
+/**
+ * @static
+ * @method insertionSortY
+ * @param  {Array} a
+ * @return {Array}
+ */
+SAPBroadphase.insertionSortY = function(a) {
+    for(var i=1,l=a.length;i<l;i++) {
+        var v = a[i];
+        for(var j=i - 1;j>=0;j--) {
+            if(a[j].aabb.lowerBound.y <= v.aabb.lowerBound.y){
+                break;
+            }
+            a[j+1] = a[j];
+        }
+        a[j+1] = v;
+    }
+    return a;
+};
+
+/**
+ * @static
+ * @method insertionSortZ
+ * @param  {Array} a
+ * @return {Array}
+ */
+SAPBroadphase.insertionSortZ = function(a) {
+    for(var i=1,l=a.length;i<l;i++) {
+        var v = a[i];
+        for(var j=i - 1;j>=0;j--) {
+            if(a[j].aabb.lowerBound.z <= v.aabb.lowerBound.z){
+                break;
+            }
+            a[j+1] = a[j];
+        }
+        a[j+1] = v;
+    }
+    return a;
+};
+
+/**
+ * Collect all collision pairs
+ * @method collisionPairs
+ * @param  {World} world
+ * @param  {Array} p1
+ * @param  {Array} p2
+ */
+SAPBroadphase.prototype.collisionPairs = function(world,p1,p2){
+    var bodies = this.axisList,
+        N = bodies.length,
+        axisIndex = this.axisIndex,
+        i, j;
+
+    if(this.dirty){
+        this.sortList();
+        this.dirty = false;
+    }
+
+    // Look through the list
+    for(i=0; i !== N; i++){
+        var bi = bodies[i];
+
+        for(j=i+1; j < N; j++){
+            var bj = bodies[j];
+
+            if(!this.needBroadphaseCollision(bi,bj)){
+                continue;
+            }
+
+            if(!SAPBroadphase.checkBounds(bi,bj,axisIndex)){
+                break;
+            }
+
+            this.intersectionTest(bi,bj,p1,p2);
+        }
+    }
+};
+
+SAPBroadphase.prototype.sortList = function(){
+    var axisList = this.axisList;
+    var axisIndex = this.axisIndex;
+    var N = axisList.length;
+
+    // Update AABBs
+    for(var i = 0; i!==N; i++){
+        var bi = axisList[i];
+        if(bi.aabbNeedsUpdate){
+            bi.computeAABB();
+        }
+    }
+
+    // Sort the list
+    if(axisIndex === 0){
+        SAPBroadphase.insertionSortX(axisList);
+    } else if(axisIndex === 1){
+        SAPBroadphase.insertionSortY(axisList);
+    } else if(axisIndex === 2){
+        SAPBroadphase.insertionSortZ(axisList);
+    }
+};
+
+/**
+ * Check if the bounds of two bodies overlap, along the given SAP axis.
+ * @static
+ * @method checkBounds
+ * @param  {Body} bi
+ * @param  {Body} bj
+ * @param  {Number} axisIndex
+ * @return {Boolean}
+ */
+SAPBroadphase.checkBounds = function(bi, bj, axisIndex){
+    var biPos;
+    var bjPos;
+
+    if(axisIndex === 0){
+        biPos = bi.position.x;
+        bjPos = bj.position.x;
+    } else if(axisIndex === 1){
+        biPos = bi.position.y;
+        bjPos = bj.position.y;
+    } else if(axisIndex === 2){
+        biPos = bi.position.z;
+        bjPos = bj.position.z;
+    }
+
+    var ri = bi.boundingRadius,
+        rj = bj.boundingRadius,
+        boundA1 = biPos - ri,
+        boundA2 = biPos + ri,
+        boundB1 = bjPos - rj,
+        boundB2 = bjPos + rj;
+
+    return boundB1 < boundA2;
+};
+
+/**
+ * Computes the variance of the body positions and estimates the best
+ * axis to use. Will automatically set property .axisIndex.
+ * @method autoDetectAxis
+ */
+SAPBroadphase.prototype.autoDetectAxis = function(){
+    var sumX=0,
+        sumX2=0,
+        sumY=0,
+        sumY2=0,
+        sumZ=0,
+        sumZ2=0,
+        bodies = this.axisList,
+        N = bodies.length,
+        invN=1/N;
+
+    for(var i=0; i!==N; i++){
+        var b = bodies[i];
+
+        var centerX = b.position.x;
+        sumX += centerX;
+        sumX2 += centerX*centerX;
+
+        var centerY = b.position.y;
+        sumY += centerY;
+        sumY2 += centerY*centerY;
+
+        var centerZ = b.position.z;
+        sumZ += centerZ;
+        sumZ2 += centerZ*centerZ;
+    }
+
+    var varianceX = sumX2 - sumX*sumX*invN,
+        varianceY = sumY2 - sumY*sumY*invN,
+        varianceZ = sumZ2 - sumZ*sumZ*invN;
+
+    if(varianceX > varianceY){
+        if(varianceX > varianceZ){
+            this.axisIndex = 0;
+        } else{
+            this.axisIndex = 2;
+        }
+    } else if(varianceY > varianceZ){
+        this.axisIndex = 1;
+    } else{
+        this.axisIndex = 2;
+    }
+};
+
+/**
+ * Returns all the bodies within an AABB.
+ * @method aabbQuery
+ * @param  {World} world
+ * @param  {AABB} aabb
+ * @param {array} result An array to store resulting bodies in.
+ * @return {array}
+ */
+SAPBroadphase.prototype.aabbQuery = function(world, aabb, result){
+    result = result || [];
+
+    if(this.dirty){
+        this.sortList();
+        this.dirty = false;
+    }
+
+    var axisIndex = this.axisIndex, axis = 'x';
+    if(axisIndex === 1){ axis = 'y'; }
+    if(axisIndex === 2){ axis = 'z'; }
+
+    var axisList = this.axisList;
+    var lower = aabb.lowerBound[axis];
+    var upper = aabb.upperBound[axis];
+    for(var i = 0; i < axisList.length; i++){
+        var b = axisList[i];
+
+        if(b.aabbNeedsUpdate){
+            b.computeAABB();
+        }
+
+        if(b.aabb.overlaps(aabb)){
+            result.push(b);
+        }
+    }
+
+    return result;
+};
+},{"../collision/Broadphase":20,"../shapes/Shape":59}],28:[function(require,module,exports){
+module.exports = ConeTwistConstraint;
+
+var Constraint = require('./Constraint');
+var PointToPointConstraint = require('./PointToPointConstraint');
+var ConeEquation = require('../equations/ConeEquation');
+var RotationalEquation = require('../equations/RotationalEquation');
+var ContactEquation = require('../equations/ContactEquation');
+var Vec3 = require('../math/Vec3');
+
+/**
+ * @class ConeTwistConstraint
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {object} [options]
+ * @param {Vec3} [options.pivotA]
+ * @param {Vec3} [options.pivotB]
+ * @param {Vec3} [options.axisA]
+ * @param {Vec3} [options.axisB]
+ * @param {Number} [options.maxForce=1e6]
+ * @extends PointToPointConstraint
+ */
+function ConeTwistConstraint(bodyA, bodyB, options){
+    options = options || {};
+    var maxForce = typeof(options.maxForce) !== 'undefined' ? options.maxForce : 1e6;
+
+    // Set pivot point in between
+    var pivotA = options.pivotA ? options.pivotA.clone() : new Vec3();
+    var pivotB = options.pivotB ? options.pivotB.clone() : new Vec3();
+    this.axisA = options.axisA ? options.axisA.clone() : new Vec3();
+    this.axisB = options.axisB ? options.axisB.clone() : new Vec3();
+
+    PointToPointConstraint.call(this, bodyA, pivotA, bodyB, pivotB, maxForce);
+
+    this.collideConnected = !!options.collideConnected;
+
+    this.angle = typeof(options.angle) !== 'undefined' ? options.angle : 0;
+
+    /**
+     * @property {ConeEquation} coneEquation
+     */
+    var c = this.coneEquation = new ConeEquation(bodyA,bodyB,options);
+
+    /**
+     * @property {RotationalEquation} twistEquation
+     */
+    var t = this.twistEquation = new RotationalEquation(bodyA,bodyB,options);
+    this.twistAngle = typeof(options.twistAngle) !== 'undefined' ? options.twistAngle : 0;
+
+    // Make the cone equation push the bodies toward the cone axis, not outward
+    c.maxForce = 0;
+    c.minForce = -maxForce;
+
+    // Make the twist equation add torque toward the initial position
+    t.maxForce = 0;
+    t.minForce = -maxForce;
+
+    this.equations.push(c, t);
+}
+ConeTwistConstraint.prototype = new PointToPointConstraint();
+ConeTwistConstraint.constructor = ConeTwistConstraint;
+
+var ConeTwistConstraint_update_tmpVec1 = new Vec3();
+var ConeTwistConstraint_update_tmpVec2 = new Vec3();
+
+ConeTwistConstraint.prototype.update = function(){
+    var bodyA = this.bodyA,
+        bodyB = this.bodyB,
+        cone = this.coneEquation,
+        twist = this.twistEquation;
+
+    PointToPointConstraint.prototype.update.call(this);
+
+    // Update the axes to the cone constraint
+    bodyA.vectorToWorldFrame(this.axisA, cone.axisA);
+    bodyB.vectorToWorldFrame(this.axisB, cone.axisB);
+
+    // Update the world axes in the twist constraint
+    this.axisA.tangents(twist.axisA, twist.axisA);
+    bodyA.vectorToWorldFrame(twist.axisA, twist.axisA);
+
+    this.axisB.tangents(twist.axisB, twist.axisB);
+    bodyB.vectorToWorldFrame(twist.axisB, twist.axisB);
+
+    cone.angle = this.angle;
+    twist.maxAngle = this.twistAngle;
+};
+
+
+},{"../equations/ConeEquation":34,"../equations/ContactEquation":35,"../equations/RotationalEquation":38,"../math/Vec3":46,"./Constraint":29,"./PointToPointConstraint":33}],29:[function(require,module,exports){
+module.exports = Constraint;
+
+var Utils = require('../utils/Utils');
+
+/**
+ * Constraint base class
+ * @class Constraint
+ * @author schteppe
+ * @constructor
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {object} [options]
+ * @param {boolean} [options.collideConnected=true]
+ * @param {boolean} [options.wakeUpBodies=true]
+ */
+function Constraint(bodyA, bodyB, options){
+    options = Utils.defaults(options,{
+        collideConnected : true,
+        wakeUpBodies : true,
+    });
+
+    /**
+     * Equations to be solved in this constraint
+     * @property equations
+     * @type {Array}
+     */
+    this.equations = [];
+
+    /**
+     * @property {Body} bodyA
+     */
+    this.bodyA = bodyA;
+
+    /**
+     * @property {Body} bodyB
+     */
+    this.bodyB = bodyB;
+
+    /**
+     * @property {Number} id
+     */
+    this.id = Constraint.idCounter++;
+
+    /**
+     * Set to true if you want the bodies to collide when they are connected.
+     * @property collideConnected
+     * @type {boolean}
+     */
+    this.collideConnected = options.collideConnected;
+
+    if(options.wakeUpBodies){
+        if(bodyA){
+            bodyA.wakeUp();
+        }
+        if(bodyB){
+            bodyB.wakeUp();
+        }
+    }
+}
+
+/**
+ * Update all the equations with data.
+ * @method update
+ */
+Constraint.prototype.update = function(){
+    throw new Error("method update() not implmemented in this Constraint subclass!");
+};
+
+/**
+ * Enables all equations in the constraint.
+ * @method enable
+ */
+Constraint.prototype.enable = function(){
+    var eqs = this.equations;
+    for(var i=0; i<eqs.length; i++){
+        eqs[i].enabled = true;
+    }
+};
+
+/**
+ * Disables all equations in the constraint.
+ * @method disable
+ */
+Constraint.prototype.disable = function(){
+    var eqs = this.equations;
+    for(var i=0; i<eqs.length; i++){
+        eqs[i].enabled = false;
+    }
+};
+
+Constraint.idCounter = 0;
+
+},{"../utils/Utils":69}],30:[function(require,module,exports){
+module.exports = DistanceConstraint;
+
+var Constraint = require('./Constraint');
+var ContactEquation = require('../equations/ContactEquation');
+
+/**
+ * Constrains two bodies to be at a constant distance from each others center of mass.
+ * @class DistanceConstraint
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Number} [distance] The distance to keep. If undefined, it will be set to the current distance between bodyA and bodyB
+ * @param {Number} [maxForce=1e6]
+ * @extends Constraint
+ */
+function DistanceConstraint(bodyA,bodyB,distance,maxForce){
+    Constraint.call(this,bodyA,bodyB);
+
+    if(typeof(distance)==="undefined") {
+        distance = bodyA.position.distanceTo(bodyB.position);
+    }
+
+    if(typeof(maxForce)==="undefined") {
+        maxForce = 1e6;
+    }
+
+    /**
+     * @property {number} distance
+     */
+    this.distance = distance;
+
+    /**
+     * @property {ContactEquation} distanceEquation
+     */
+    var eq = this.distanceEquation = new ContactEquation(bodyA, bodyB);
+    this.equations.push(eq);
+
+    // Make it bidirectional
+    eq.minForce = -maxForce;
+    eq.maxForce =  maxForce;
+}
+DistanceConstraint.prototype = new Constraint();
+
+DistanceConstraint.prototype.update = function(){
+    var bodyA = this.bodyA;
+    var bodyB = this.bodyB;
+    var eq = this.distanceEquation;
+    var halfDist = this.distance * 0.5;
+    var normal = eq.ni;
+
+    bodyB.position.vsub(bodyA.position, normal);
+    normal.normalize();
+    normal.mult(halfDist, eq.ri);
+    normal.mult(-halfDist, eq.rj);
+};
+},{"../equations/ContactEquation":35,"./Constraint":29}],31:[function(require,module,exports){
+module.exports = HingeConstraint;
+
+var Constraint = require('./Constraint');
+var PointToPointConstraint = require('./PointToPointConstraint');
+var RotationalEquation = require('../equations/RotationalEquation');
+var RotationalMotorEquation = require('../equations/RotationalMotorEquation');
+var ContactEquation = require('../equations/ContactEquation');
+var Vec3 = require('../math/Vec3');
+
+/**
+ * Hinge constraint. Think of it as a door hinge. It tries to keep the door in the correct place and with the correct orientation.
+ * @class HingeConstraint
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {object} [options]
+ * @param {Vec3} [options.pivotA] A point defined locally in bodyA. This defines the offset of axisA.
+ * @param {Vec3} [options.axisA] An axis that bodyA can rotate around, defined locally in bodyA.
+ * @param {Vec3} [options.pivotB]
+ * @param {Vec3} [options.axisB]
+ * @param {Number} [options.maxForce=1e6]
+ * @extends PointToPointConstraint
+ */
+function HingeConstraint(bodyA, bodyB, options){
+    options = options || {};
+    var maxForce = typeof(options.maxForce) !== 'undefined' ? options.maxForce : 1e6;
+    var pivotA = options.pivotA ? options.pivotA.clone() : new Vec3();
+    var pivotB = options.pivotB ? options.pivotB.clone() : new Vec3();
+
+    PointToPointConstraint.call(this, bodyA, pivotA, bodyB, pivotB, maxForce);
+
+    /**
+     * Rotation axis, defined locally in bodyA.
+     * @property {Vec3} axisA
+     */
+    var axisA = this.axisA = options.axisA ? options.axisA.clone() : new Vec3(1,0,0);
+    axisA.normalize();
+
+    /**
+     * Rotation axis, defined locally in bodyB.
+     * @property {Vec3} axisB
+     */
+    var axisB = this.axisB = options.axisB ? options.axisB.clone() : new Vec3(1,0,0);
+    axisB.normalize();
+
+    /**
+     * @property {RotationalEquation} rotationalEquation1
+     */
+    var r1 = this.rotationalEquation1 = new RotationalEquation(bodyA,bodyB,options);
+
+    /**
+     * @property {RotationalEquation} rotationalEquation2
+     */
+    var r2 = this.rotationalEquation2 = new RotationalEquation(bodyA,bodyB,options);
+
+    /**
+     * @property {RotationalMotorEquation} motorEquation
+     */
+    var motor = this.motorEquation = new RotationalMotorEquation(bodyA,bodyB,maxForce);
+    motor.enabled = false; // Not enabled by default
+
+    // Equations to be fed to the solver
+    this.equations.push(
+        r1, // rotational1
+        r2, // rotational2
+        motor
+    );
+}
+HingeConstraint.prototype = new PointToPointConstraint();
+HingeConstraint.constructor = HingeConstraint;
+
+/**
+ * @method enableMotor
+ */
+HingeConstraint.prototype.enableMotor = function(){
+    this.motorEquation.enabled = true;
+};
+
+/**
+ * @method disableMotor
+ */
+HingeConstraint.prototype.disableMotor = function(){
+    this.motorEquation.enabled = false;
+};
+
+/**
+ * @method setMotorSpeed
+ * @param {number} speed
+ */
+HingeConstraint.prototype.setMotorSpeed = function(speed){
+    this.motorEquation.targetVelocity = speed;
+};
+
+/**
+ * @method setMotorMaxForce
+ * @param {number} maxForce
+ */
+HingeConstraint.prototype.setMotorMaxForce = function(maxForce){
+    this.motorEquation.maxForce = maxForce;
+    this.motorEquation.minForce = -maxForce;
+};
+
+var HingeConstraint_update_tmpVec1 = new Vec3();
+var HingeConstraint_update_tmpVec2 = new Vec3();
+
+HingeConstraint.prototype.update = function(){
+    var bodyA = this.bodyA,
+        bodyB = this.bodyB,
+        motor = this.motorEquation,
+        r1 = this.rotationalEquation1,
+        r2 = this.rotationalEquation2,
+        worldAxisA = HingeConstraint_update_tmpVec1,
+        worldAxisB = HingeConstraint_update_tmpVec2;
+
+    var axisA = this.axisA;
+    var axisB = this.axisB;
+
+    PointToPointConstraint.prototype.update.call(this);
+
+    // Get world axes
+    bodyA.quaternion.vmult(axisA, worldAxisA);
+    bodyB.quaternion.vmult(axisB, worldAxisB);
+
+    worldAxisA.tangents(r1.axisA, r2.axisA);
+    r1.axisB.copy(worldAxisB);
+    r2.axisB.copy(worldAxisB);
+
+    if(this.motorEquation.enabled){
+        bodyA.quaternion.vmult(this.axisA, motor.axisA);
+        bodyB.quaternion.vmult(this.axisB, motor.axisB);
+    }
+};
+
+
+},{"../equations/ContactEquation":35,"../equations/RotationalEquation":38,"../equations/RotationalMotorEquation":39,"../math/Vec3":46,"./Constraint":29,"./PointToPointConstraint":33}],32:[function(require,module,exports){
+module.exports = LockConstraint;
+
+var Constraint = require('./Constraint');
+var PointToPointConstraint = require('./PointToPointConstraint');
+var RotationalEquation = require('../equations/RotationalEquation');
+var RotationalMotorEquation = require('../equations/RotationalMotorEquation');
+var ContactEquation = require('../equations/ContactEquation');
+var Vec3 = require('../math/Vec3');
+
+/**
+ * Lock constraint. Will remove all degrees of freedom between the bodies.
+ * @class LockConstraint
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {object} [options]
+ * @param {Number} [options.maxForce=1e6]
+ * @extends PointToPointConstraint
+ */
+function LockConstraint(bodyA, bodyB, options){
+    options = options || {};
+    var maxForce = typeof(options.maxForce) !== 'undefined' ? options.maxForce : 1e6;
+
+    // Set pivot point in between
+    var pivotA = new Vec3();
+    var pivotB = new Vec3();
+    var halfWay = new Vec3();
+    bodyA.position.vadd(bodyB.position, halfWay);
+    halfWay.scale(0.5, halfWay);
+    bodyB.pointToLocalFrame(halfWay, pivotB);
+    bodyA.pointToLocalFrame(halfWay, pivotA);
+
+    // The point-to-point constraint will keep a point shared between the bodies
+    PointToPointConstraint.call(this, bodyA, pivotA, bodyB, pivotB, maxForce);
+
+    // Store initial rotation of the bodies as unit vectors in the local body spaces
+    this.xA = bodyA.vectorToLocalFrame(Vec3.UNIT_X);
+    this.xB = bodyB.vectorToLocalFrame(Vec3.UNIT_X);
+    this.yA = bodyA.vectorToLocalFrame(Vec3.UNIT_Y);
+    this.yB = bodyB.vectorToLocalFrame(Vec3.UNIT_Y);
+    this.zA = bodyA.vectorToLocalFrame(Vec3.UNIT_Z);
+    this.zB = bodyB.vectorToLocalFrame(Vec3.UNIT_Z);
+
+    // ...and the following rotational equations will keep all rotational DOF's in place
+
+    /**
+     * @property {RotationalEquation} rotationalEquation1
+     */
+    var r1 = this.rotationalEquation1 = new RotationalEquation(bodyA,bodyB,options);
+
+    /**
+     * @property {RotationalEquation} rotationalEquation2
+     */
+    var r2 = this.rotationalEquation2 = new RotationalEquation(bodyA,bodyB,options);
+
+    /**
+     * @property {RotationalEquation} rotationalEquation3
+     */
+    var r3 = this.rotationalEquation3 = new RotationalEquation(bodyA,bodyB,options);
+
+    this.equations.push(r1, r2, r3);
+}
+LockConstraint.prototype = new PointToPointConstraint();
+LockConstraint.constructor = LockConstraint;
+
+var LockConstraint_update_tmpVec1 = new Vec3();
+var LockConstraint_update_tmpVec2 = new Vec3();
+
+LockConstraint.prototype.update = function(){
+    var bodyA = this.bodyA,
+        bodyB = this.bodyB,
+        motor = this.motorEquation,
+        r1 = this.rotationalEquation1,
+        r2 = this.rotationalEquation2,
+        r3 = this.rotationalEquation3,
+        worldAxisA = LockConstraint_update_tmpVec1,
+        worldAxisB = LockConstraint_update_tmpVec2;
+
+    PointToPointConstraint.prototype.update.call(this);
+
+    // These vector pairs must be orthogonal
+    bodyA.vectorToWorldFrame(this.xA, r1.axisA);
+    bodyB.vectorToWorldFrame(this.yB, r1.axisB);
+
+    bodyA.vectorToWorldFrame(this.yA, r2.axisA);
+    bodyB.vectorToWorldFrame(this.zB, r2.axisB);
+
+    bodyA.vectorToWorldFrame(this.zA, r3.axisA);
+    bodyB.vectorToWorldFrame(this.xB, r3.axisB);
+};
+
+
+},{"../equations/ContactEquation":35,"../equations/RotationalEquation":38,"../equations/RotationalMotorEquation":39,"../math/Vec3":46,"./Constraint":29,"./PointToPointConstraint":33}],33:[function(require,module,exports){
+module.exports = PointToPointConstraint;
+
+var Constraint = require('./Constraint');
+var ContactEquation = require('../equations/ContactEquation');
+var Vec3 = require('../math/Vec3');
+
+/**
+ * Connects two bodies at given offset points.
+ * @class PointToPointConstraint
+ * @extends Constraint
+ * @constructor
+ * @param {Body} bodyA
+ * @param {Vec3} pivotA The point relative to the center of mass of bodyA which bodyA is constrained to.
+ * @param {Body} bodyB Body that will be constrained in a similar way to the same point as bodyA. We will therefore get a link between bodyA and bodyB. If not specified, bodyA will be constrained to a static point.
+ * @param {Vec3} pivotB See pivotA.
+ * @param {Number} maxForce The maximum force that should be applied to constrain the bodies.
+ *
+ * @example
+ *     var bodyA = new Body({ mass: 1 });
+ *     var bodyB = new Body({ mass: 1 });
+ *     bodyA.position.set(-1, 0, 0);
+ *     bodyB.position.set(1, 0, 0);
+ *     bodyA.addShape(shapeA);
+ *     bodyB.addShape(shapeB);
+ *     world.addBody(bodyA);
+ *     world.addBody(bodyB);
+ *     var localPivotA = new Vec3(1, 0, 0);
+ *     var localPivotB = new Vec3(-1, 0, 0);
+ *     var constraint = new PointToPointConstraint(bodyA, localPivotA, bodyB, localPivotB);
+ *     world.addConstraint(constraint);
+ */
+function PointToPointConstraint(bodyA,pivotA,bodyB,pivotB,maxForce){
+    Constraint.call(this,bodyA,bodyB);
+
+    maxForce = typeof(maxForce) !== 'undefined' ? maxForce : 1e6;
+
+    /**
+     * Pivot, defined locally in bodyA.
+     * @property {Vec3} pivotA
+     */
+    this.pivotA = pivotA ? pivotA.clone() : new Vec3();
+
+    /**
+     * Pivot, defined locally in bodyB.
+     * @property {Vec3} pivotB
+     */
+    this.pivotB = pivotB ? pivotB.clone() : new Vec3();
+
+    /**
+     * @property {ContactEquation} equationX
+     */
+    var x = this.equationX = new ContactEquation(bodyA,bodyB);
+
+    /**
+     * @property {ContactEquation} equationY
+     */
+    var y = this.equationY = new ContactEquation(bodyA,bodyB);
+
+    /**
+     * @property {ContactEquation} equationZ
+     */
+    var z = this.equationZ = new ContactEquation(bodyA,bodyB);
+
+    // Equations to be fed to the solver
+    this.equations.push(x, y, z);
+
+    // Make the equations bidirectional
+    x.minForce = y.minForce = z.minForce = -maxForce;
+    x.maxForce = y.maxForce = z.maxForce =  maxForce;
+
+    x.ni.set(1, 0, 0);
+    y.ni.set(0, 1, 0);
+    z.ni.set(0, 0, 1);
+}
+PointToPointConstraint.prototype = new Constraint();
+
+PointToPointConstraint.prototype.update = function(){
+    var bodyA = this.bodyA;
+    var bodyB = this.bodyB;
+    var x = this.equationX;
+    var y = this.equationY;
+    var z = this.equationZ;
+
+    // Rotate the pivots to world space
+    bodyA.quaternion.vmult(this.pivotA,x.ri);
+    bodyB.quaternion.vmult(this.pivotB,x.rj);
+
+    y.ri.copy(x.ri);
+    y.rj.copy(x.rj);
+    z.ri.copy(x.ri);
+    z.rj.copy(x.rj);
+};
+},{"../equations/ContactEquation":35,"../math/Vec3":46,"./Constraint":29}],34:[function(require,module,exports){
+module.exports = ConeEquation;
+
+var Vec3 = require('../math/Vec3');
+var Mat3 = require('../math/Mat3');
+var Equation = require('./Equation');
+
+/**
+ * Cone equation. Works to keep the given body world vectors aligned, or tilted within a given angle from each other.
+ * @class ConeEquation
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Vec3} [options.axisA] Local axis in A
+ * @param {Vec3} [options.axisB] Local axis in B
+ * @param {Vec3} [options.angle] The "cone angle" to keep
+ * @param {number} [options.maxForce=1e6]
+ * @extends Equation
+ */
+function ConeEquation(bodyA, bodyB, options){
+    options = options || {};
+    var maxForce = typeof(options.maxForce) !== 'undefined' ? options.maxForce : 1e6;
+
+    Equation.call(this,bodyA,bodyB,-maxForce, maxForce);
+
+    this.axisA = options.axisA ? options.axisA.clone() : new Vec3(1, 0, 0);
+    this.axisB = options.axisB ? options.axisB.clone() : new Vec3(0, 1, 0);
+
+    /**
+     * The cone angle to keep
+     * @property {number} angle
+     */
+    this.angle = typeof(options.angle) !== 'undefined' ? options.angle : 0;
+}
+
+ConeEquation.prototype = new Equation();
+ConeEquation.prototype.constructor = ConeEquation;
+
+var tmpVec1 = new Vec3();
+var tmpVec2 = new Vec3();
+
+ConeEquation.prototype.computeB = function(h){
+    var a = this.a,
+        b = this.b,
+
+        ni = this.axisA,
+        nj = this.axisB,
+
+        nixnj = tmpVec1,
+        njxni = tmpVec2,
+
+        GA = this.jacobianElementA,
+        GB = this.jacobianElementB;
+
+    // Caluclate cross products
+    ni.cross(nj, nixnj);
+    nj.cross(ni, njxni);
+
+    // The angle between two vector is:
+    // cos(theta) = a * b / (length(a) * length(b) = { len(a) = len(b) = 1 } = a * b
+
+    // g = a * b
+    // gdot = (b x a) * wi + (a x b) * wj
+    // G = [0 bxa 0 axb]
+    // W = [vi wi vj wj]
+    GA.rotational.copy(njxni);
+    GB.rotational.copy(nixnj);
+
+    var g = Math.cos(this.angle) - ni.dot(nj),
+        GW = this.computeGW(),
+        GiMf = this.computeGiMf();
+
+    var B = - g * a - GW * b - h * GiMf;
+
+    return B;
+};
+
+
+},{"../math/Mat3":43,"../math/Vec3":46,"./Equation":36}],35:[function(require,module,exports){
+module.exports = ContactEquation;
+
+var Equation = require('./Equation');
+var Vec3 = require('../math/Vec3');
+var Mat3 = require('../math/Mat3');
+
+/**
+ * Contact/non-penetration constraint equation
+ * @class ContactEquation
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @extends Equation
+ */
+function ContactEquation(bodyA, bodyB, maxForce){
+    maxForce = typeof(maxForce) !== 'undefined' ? maxForce : 1e6;
+    Equation.call(this, bodyA, bodyB, 0, maxForce);
+
+    /**
+     * @property restitution
+     * @type {Number}
+     */
+    this.restitution = 0.0; // "bounciness": u1 = -e*u0
+
+    /**
+     * World-oriented vector that goes from the center of bi to the contact point.
+     * @property {Vec3} ri
+     */
+    this.ri = new Vec3();
+
+    /**
+     * World-oriented vector that starts in body j position and goes to the contact point.
+     * @property {Vec3} rj
+     */
+    this.rj = new Vec3();
+
+    /**
+     * Contact normal, pointing out of body i.
+     * @property {Vec3} ni
+     */
+    this.ni = new Vec3();
+}
+
+ContactEquation.prototype = new Equation();
+ContactEquation.prototype.constructor = ContactEquation;
+
+var ContactEquation_computeB_temp1 = new Vec3(); // Temp vectors
+var ContactEquation_computeB_temp2 = new Vec3();
+var ContactEquation_computeB_temp3 = new Vec3();
+ContactEquation.prototype.computeB = function(h){
+    var a = this.a,
+        b = this.b,
+        bi = this.bi,
+        bj = this.bj,
+        ri = this.ri,
+        rj = this.rj,
+        rixn = ContactEquation_computeB_temp1,
+        rjxn = ContactEquation_computeB_temp2,
+
+        vi = bi.velocity,
+        wi = bi.angularVelocity,
+        fi = bi.force,
+        taui = bi.torque,
+
+        vj = bj.velocity,
+        wj = bj.angularVelocity,
+        fj = bj.force,
+        tauj = bj.torque,
+
+        penetrationVec = ContactEquation_computeB_temp3,
+
+        GA = this.jacobianElementA,
+        GB = this.jacobianElementB,
+
+        n = this.ni;
+
+    // Caluclate cross products
+    ri.cross(n,rixn);
+    rj.cross(n,rjxn);
+
+    // g = xj+rj -(xi+ri)
+    // G = [ -ni  -rixn  ni  rjxn ]
+    n.negate(GA.spatial);
+    rixn.negate(GA.rotational);
+    GB.spatial.copy(n);
+    GB.rotational.copy(rjxn);
+
+    // Calculate the penetration vector
+    penetrationVec.copy(bj.position);
+    penetrationVec.vadd(rj,penetrationVec);
+    penetrationVec.vsub(bi.position,penetrationVec);
+    penetrationVec.vsub(ri,penetrationVec);
+
+    var g = n.dot(penetrationVec);
+
+    // Compute iteration
+    var ePlusOne = this.restitution + 1;
+    var GW = ePlusOne * vj.dot(n) - ePlusOne * vi.dot(n) + wj.dot(rjxn) - wi.dot(rixn);
+    var GiMf = this.computeGiMf();
+
+    var B = - g * a - GW * b - h*GiMf;
+
+    return B;
+};
+
+var ContactEquation_getImpactVelocityAlongNormal_vi = new Vec3();
+var ContactEquation_getImpactVelocityAlongNormal_vj = new Vec3();
+var ContactEquation_getImpactVelocityAlongNormal_xi = new Vec3();
+var ContactEquation_getImpactVelocityAlongNormal_xj = new Vec3();
+var ContactEquation_getImpactVelocityAlongNormal_relVel = new Vec3();
+
+/**
+ * Get the current relative velocity in the contact point.
+ * @method getImpactVelocityAlongNormal
+ * @return {number}
+ */
+ContactEquation.prototype.getImpactVelocityAlongNormal = function(){
+    var vi = ContactEquation_getImpactVelocityAlongNormal_vi;
+    var vj = ContactEquation_getImpactVelocityAlongNormal_vj;
+    var xi = ContactEquation_getImpactVelocityAlongNormal_xi;
+    var xj = ContactEquation_getImpactVelocityAlongNormal_xj;
+    var relVel = ContactEquation_getImpactVelocityAlongNormal_relVel;
+
+    this.bi.position.vadd(this.ri, xi);
+    this.bj.position.vadd(this.rj, xj);
+
+    this.bi.getVelocityAtWorldPoint(xi, vi);
+    this.bj.getVelocityAtWorldPoint(xj, vj);
+
+    vi.vsub(vj, relVel);
+
+    return this.ni.dot(relVel);
+};
+
+
+},{"../math/Mat3":43,"../math/Vec3":46,"./Equation":36}],36:[function(require,module,exports){
+module.exports = Equation;
+
+var JacobianElement = require('../math/JacobianElement'),
+    Vec3 = require('../math/Vec3');
+
+/**
+ * Equation base class
+ * @class Equation
+ * @constructor
+ * @author schteppe
+ * @param {Body} bi
+ * @param {Body} bj
+ * @param {Number} minForce Minimum (read: negative max) force to be applied by the constraint.
+ * @param {Number} maxForce Maximum (read: positive max) force to be applied by the constraint.
+ */
+function Equation(bi,bj,minForce,maxForce){
+    this.id = Equation.id++;
+
+    /**
+     * @property {number} minForce
+     */
+    this.minForce = typeof(minForce)==="undefined" ? -1e6 : minForce;
+
+    /**
+     * @property {number} maxForce
+     */
+    this.maxForce = typeof(maxForce)==="undefined" ? 1e6 : maxForce;
+
+    /**
+     * @property bi
+     * @type {Body}
+     */
+    this.bi = bi;
+
+    /**
+     * @property bj
+     * @type {Body}
+     */
+    this.bj = bj;
+
+    /**
+     * SPOOK parameter
+     * @property {number} a
+     */
+    this.a = 0.0;
+
+    /**
+     * SPOOK parameter
+     * @property {number} b
+     */
+    this.b = 0.0;
+
+    /**
+     * SPOOK parameter
+     * @property {number} eps
+     */
+    this.eps = 0.0;
+
+    /**
+     * @property {JacobianElement} jacobianElementA
+     */
+    this.jacobianElementA = new JacobianElement();
+
+    /**
+     * @property {JacobianElement} jacobianElementB
+     */
+    this.jacobianElementB = new JacobianElement();
+
+    /**
+     * @property {boolean} enabled
+     * @default true
+     */
+    this.enabled = true;
+
+    /**
+     * A number, proportional to the force added to the bodies.
+     * @property {number} multiplier
+     * @readonly
+     */
+    this.multiplier = 0;
+
+    // Set typical spook params
+    this.setSpookParams(1e7,4,1/60);
+}
+Equation.prototype.constructor = Equation;
+
+Equation.id = 0;
+
+/**
+ * Recalculates a,b,eps.
+ * @method setSpookParams
+ */
+Equation.prototype.setSpookParams = function(stiffness,relaxation,timeStep){
+    var d = relaxation,
+        k = stiffness,
+        h = timeStep;
+    this.a = 4.0 / (h * (1 + 4 * d));
+    this.b = (4.0 * d) / (1 + 4 * d);
+    this.eps = 4.0 / (h * h * k * (1 + 4 * d));
+};
+
+/**
+ * Computes the RHS of the SPOOK equation
+ * @method computeB
+ * @return {Number}
+ */
+Equation.prototype.computeB = function(a,b,h){
+    var GW = this.computeGW(),
+        Gq = this.computeGq(),
+        GiMf = this.computeGiMf();
+    return - Gq * a - GW * b - GiMf*h;
+};
+
+/**
+ * Computes G*q, where q are the generalized body coordinates
+ * @method computeGq
+ * @return {Number}
+ */
+Equation.prototype.computeGq = function(){
+    var GA = this.jacobianElementA,
+        GB = this.jacobianElementB,
+        bi = this.bi,
+        bj = this.bj,
+        xi = bi.position,
+        xj = bj.position;
+    return GA.spatial.dot(xi) + GB.spatial.dot(xj);
+};
+
+var zero = new Vec3();
+
+/**
+ * Computes G*W, where W are the body velocities
+ * @method computeGW
+ * @return {Number}
+ */
+Equation.prototype.computeGW = function(){
+    var GA = this.jacobianElementA,
+        GB = this.jacobianElementB,
+        bi = this.bi,
+        bj = this.bj,
+        vi = bi.velocity,
+        vj = bj.velocity,
+        wi = bi.angularVelocity,
+        wj = bj.angularVelocity;
+    return GA.multiplyVectors(vi,wi) + GB.multiplyVectors(vj,wj);
+};
+
+
+/**
+ * Computes G*Wlambda, where W are the body velocities
+ * @method computeGWlambda
+ * @return {Number}
+ */
+Equation.prototype.computeGWlambda = function(){
+    var GA = this.jacobianElementA,
+        GB = this.jacobianElementB,
+        bi = this.bi,
+        bj = this.bj,
+        vi = bi.vlambda,
+        vj = bj.vlambda,
+        wi = bi.wlambda,
+        wj = bj.wlambda;
+    return GA.multiplyVectors(vi,wi) + GB.multiplyVectors(vj,wj);
+};
+
+/**
+ * Computes G*inv(M)*f, where M is the mass matrix with diagonal blocks for each body, and f are the forces on the bodies.
+ * @method computeGiMf
+ * @return {Number}
+ */
+var iMfi = new Vec3(),
+    iMfj = new Vec3(),
+    invIi_vmult_taui = new Vec3(),
+    invIj_vmult_tauj = new Vec3();
+Equation.prototype.computeGiMf = function(){
+    var GA = this.jacobianElementA,
+        GB = this.jacobianElementB,
+        bi = this.bi,
+        bj = this.bj,
+        fi = bi.force,
+        ti = bi.torque,
+        fj = bj.force,
+        tj = bj.torque,
+        invMassi = bi.invMassSolve,
+        invMassj = bj.invMassSolve;
+
+    fi.scale(invMassi,iMfi);
+    fj.scale(invMassj,iMfj);
+
+    bi.invInertiaWorldSolve.vmult(ti,invIi_vmult_taui);
+    bj.invInertiaWorldSolve.vmult(tj,invIj_vmult_tauj);
+
+    return GA.multiplyVectors(iMfi,invIi_vmult_taui) + GB.multiplyVectors(iMfj,invIj_vmult_tauj);
+};
+
+/**
+ * Computes G*inv(M)*G'
+ * @method computeGiMGt
+ * @return {Number}
+ */
+var tmp = new Vec3();
+Equation.prototype.computeGiMGt = function(){
+    var GA = this.jacobianElementA,
+        GB = this.jacobianElementB,
+        bi = this.bi,
+        bj = this.bj,
+        invMassi = bi.invMassSolve,
+        invMassj = bj.invMassSolve,
+        invIi = bi.invInertiaWorldSolve,
+        invIj = bj.invInertiaWorldSolve,
+        result = invMassi + invMassj;
+
+    invIi.vmult(GA.rotational,tmp);
+    result += tmp.dot(GA.rotational);
+
+    invIj.vmult(GB.rotational,tmp);
+    result += tmp.dot(GB.rotational);
+
+    return  result;
+};
+
+var addToWlambda_temp = new Vec3(),
+    addToWlambda_Gi = new Vec3(),
+    addToWlambda_Gj = new Vec3(),
+    addToWlambda_ri = new Vec3(),
+    addToWlambda_rj = new Vec3(),
+    addToWlambda_Mdiag = new Vec3();
+
+/**
+ * Add constraint velocity to the bodies.
+ * @method addToWlambda
+ * @param {Number} deltalambda
+ */
+Equation.prototype.addToWlambda = function(deltalambda){
+    var GA = this.jacobianElementA,
+        GB = this.jacobianElementB,
+        bi = this.bi,
+        bj = this.bj,
+        temp = addToWlambda_temp;
+
+    // Add to linear velocity
+    // v_lambda += inv(M) * delta_lamba * G
+    bi.vlambda.addScaledVector(bi.invMassSolve * deltalambda, GA.spatial, bi.vlambda);
+    bj.vlambda.addScaledVector(bj.invMassSolve * deltalambda, GB.spatial, bj.vlambda);
+
+    // Add to angular velocity
+    bi.invInertiaWorldSolve.vmult(GA.rotational,temp);
+    bi.wlambda.addScaledVector(deltalambda, temp, bi.wlambda);
+
+    bj.invInertiaWorldSolve.vmult(GB.rotational,temp);
+    bj.wlambda.addScaledVector(deltalambda, temp, bj.wlambda);
+};
+
+/**
+ * Compute the denominator part of the SPOOK equation: C = G*inv(M)*G' + eps
+ * @method computeInvC
+ * @param  {Number} eps
+ * @return {Number}
+ */
+Equation.prototype.computeC = function(){
+    return this.computeGiMGt() + this.eps;
+};
+
+},{"../math/JacobianElement":42,"../math/Vec3":46}],37:[function(require,module,exports){
+module.exports = FrictionEquation;
+
+var Equation = require('./Equation');
+var Vec3 = require('../math/Vec3');
+var Mat3 = require('../math/Mat3');
+
+/**
+ * Constrains the slipping in a contact along a tangent
+ * @class FrictionEquation
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Number} slipForce should be +-F_friction = +-mu * F_normal = +-mu * m * g
+ * @extends Equation
+ */
+function FrictionEquation(bodyA, bodyB, slipForce){
+    Equation.call(this,bodyA, bodyB, -slipForce, slipForce);
+    this.ri = new Vec3();
+    this.rj = new Vec3();
+    this.t = new Vec3(); // tangent
+}
+
+FrictionEquation.prototype = new Equation();
+FrictionEquation.prototype.constructor = FrictionEquation;
+
+var FrictionEquation_computeB_temp1 = new Vec3();
+var FrictionEquation_computeB_temp2 = new Vec3();
+FrictionEquation.prototype.computeB = function(h){
+    var a = this.a,
+        b = this.b,
+        bi = this.bi,
+        bj = this.bj,
+        ri = this.ri,
+        rj = this.rj,
+        rixt = FrictionEquation_computeB_temp1,
+        rjxt = FrictionEquation_computeB_temp2,
+        t = this.t;
+
+    // Caluclate cross products
+    ri.cross(t,rixt);
+    rj.cross(t,rjxt);
+
+    // G = [-t -rixt t rjxt]
+    // And remember, this is a pure velocity constraint, g is always zero!
+    var GA = this.jacobianElementA,
+        GB = this.jacobianElementB;
+    t.negate(GA.spatial);
+    rixt.negate(GA.rotational);
+    GB.spatial.copy(t);
+    GB.rotational.copy(rjxt);
+
+    var GW = this.computeGW();
+    var GiMf = this.computeGiMf();
+
+    var B = - GW * b - h * GiMf;
+
+    return B;
+};
+
+},{"../math/Mat3":43,"../math/Vec3":46,"./Equation":36}],38:[function(require,module,exports){
+module.exports = RotationalEquation;
+
+var Vec3 = require('../math/Vec3');
+var Mat3 = require('../math/Mat3');
+var Equation = require('./Equation');
+
+/**
+ * Rotational constraint. Works to keep the local vectors orthogonal to each other in world space.
+ * @class RotationalEquation
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Vec3} [options.axisA]
+ * @param {Vec3} [options.axisB]
+ * @param {number} [options.maxForce]
+ * @extends Equation
+ */
+function RotationalEquation(bodyA, bodyB, options){
+    options = options || {};
+    var maxForce = typeof(options.maxForce) !== 'undefined' ? options.maxForce : 1e6;
+
+    Equation.call(this,bodyA,bodyB,-maxForce, maxForce);
+
+    this.axisA = options.axisA ? options.axisA.clone() : new Vec3(1, 0, 0);
+    this.axisB = options.axisB ? options.axisB.clone() : new Vec3(0, 1, 0);
+
+    this.maxAngle = Math.PI / 2;
+}
+
+RotationalEquation.prototype = new Equation();
+RotationalEquation.prototype.constructor = RotationalEquation;
+
+var tmpVec1 = new Vec3();
+var tmpVec2 = new Vec3();
+
+RotationalEquation.prototype.computeB = function(h){
+    var a = this.a,
+        b = this.b,
+
+        ni = this.axisA,
+        nj = this.axisB,
+
+        nixnj = tmpVec1,
+        njxni = tmpVec2,
+
+        GA = this.jacobianElementA,
+        GB = this.jacobianElementB;
+
+    // Caluclate cross products
+    ni.cross(nj, nixnj);
+    nj.cross(ni, njxni);
+
+    // g = ni * nj
+    // gdot = (nj x ni) * wi + (ni x nj) * wj
+    // G = [0 njxni 0 nixnj]
+    // W = [vi wi vj wj]
+    GA.rotational.copy(njxni);
+    GB.rotational.copy(nixnj);
+
+    var g = Math.cos(this.maxAngle) - ni.dot(nj),
+        GW = this.computeGW(),
+        GiMf = this.computeGiMf();
+
+    var B = - g * a - GW * b - h * GiMf;
+
+    return B;
+};
+
+
+},{"../math/Mat3":43,"../math/Vec3":46,"./Equation":36}],39:[function(require,module,exports){
+module.exports = RotationalMotorEquation;
+
+var Vec3 = require('../math/Vec3');
+var Mat3 = require('../math/Mat3');
+var Equation = require('./Equation');
+
+/**
+ * Rotational motor constraint. Tries to keep the relative angular velocity of the bodies to a given value.
+ * @class RotationalMotorEquation
+ * @constructor
+ * @author schteppe
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Number} maxForce
+ * @extends Equation
+ */
+function RotationalMotorEquation(bodyA, bodyB, maxForce){
+    maxForce = typeof(maxForce)!=='undefined' ? maxForce : 1e6;
+    Equation.call(this,bodyA,bodyB,-maxForce,maxForce);
+
+    /**
+     * World oriented rotational axis
+     * @property {Vec3} axisA
+     */
+    this.axisA = new Vec3();
+
+    /**
+     * World oriented rotational axis
+     * @property {Vec3} axisB
+     */
+    this.axisB = new Vec3(); // World oriented rotational axis
+
+    /**
+     * Motor velocity
+     * @property {Number} targetVelocity
+     */
+    this.targetVelocity = 0;
+}
+
+RotationalMotorEquation.prototype = new Equation();
+RotationalMotorEquation.prototype.constructor = RotationalMotorEquation;
+
+RotationalMotorEquation.prototype.computeB = function(h){
+    var a = this.a,
+        b = this.b,
+        bi = this.bi,
+        bj = this.bj,
+
+        axisA = this.axisA,
+        axisB = this.axisB,
+
+        GA = this.jacobianElementA,
+        GB = this.jacobianElementB;
+
+    // g = 0
+    // gdot = axisA * wi - axisB * wj
+    // gdot = G * W = G * [vi wi vj wj]
+    // =>
+    // G = [0 axisA 0 -axisB]
+
+    GA.rotational.copy(axisA);
+    axisB.negate(GB.rotational);
+
+    var GW = this.computeGW() - this.targetVelocity,
+        GiMf = this.computeGiMf();
+
+    var B = - GW * b - h * GiMf;
+
+    return B;
+};
+
+},{"../math/Mat3":43,"../math/Vec3":46,"./Equation":36}],40:[function(require,module,exports){
+var Utils = require('../utils/Utils');
+
+module.exports = ContactMaterial;
+
+/**
+ * Defines what happens when two materials meet.
+ * @class ContactMaterial
+ * @constructor
+ * @param {Material} m1
+ * @param {Material} m2
+ * @param {object} [options]
+ * @param {Number} [options.friction=0.3]
+ * @param {Number} [options.restitution=0.3]
+ * @param {number} [options.contactEquationStiffness=1e7]
+ * @param {number} [options.contactEquationRelaxation=3]
+ * @param {number} [options.frictionEquationStiffness=1e7]
+ * @param {Number} [options.frictionEquationRelaxation=3]
+ */
+function ContactMaterial(m1, m2, options){
+    options = Utils.defaults(options, {
+        friction: 0.3,
+        restitution: 0.3,
+        contactEquationStiffness: 1e7,
+        contactEquationRelaxation: 3,
+        frictionEquationStiffness: 1e7,
+        frictionEquationRelaxation: 3
+    });
+
+    /**
+     * Identifier of this material
+     * @property {Number} id
+     */
+    this.id = ContactMaterial.idCounter++;
+
+    /**
+     * Participating materials
+     * @property {Array} materials
+     * @todo  Should be .materialA and .materialB instead
+     */
+    this.materials = [m1, m2];
+
+    /**
+     * Friction coefficient
+     * @property {Number} friction
+     */
+    this.friction = options.friction;
+
+    /**
+     * Restitution coefficient
+     * @property {Number} restitution
+     */
+    this.restitution = options.restitution;
+
+    /**
+     * Stiffness of the produced contact equations
+     * @property {Number} contactEquationStiffness
+     */
+    this.contactEquationStiffness = options.contactEquationStiffness;
+
+    /**
+     * Relaxation time of the produced contact equations
+     * @property {Number} contactEquationRelaxation
+     */
+    this.contactEquationRelaxation = options.contactEquationRelaxation;
+
+    /**
+     * Stiffness of the produced friction equations
+     * @property {Number} frictionEquationStiffness
+     */
+    this.frictionEquationStiffness = options.frictionEquationStiffness;
+
+    /**
+     * Relaxation time of the produced friction equations
+     * @property {Number} frictionEquationRelaxation
+     */
+    this.frictionEquationRelaxation = options.frictionEquationRelaxation;
+}
+
+ContactMaterial.idCounter = 0;
+
+},{"../utils/Utils":69}],41:[function(require,module,exports){
+module.exports = Material;
+
+/**
+ * Defines a physics material.
+ * @class Material
+ * @constructor
+ * @param {object} [options]
+ * @author schteppe
+ */
+function Material(options){
+    var name = '';
+    options = options || {};
+
+    // Backwards compatibility fix
+    if(typeof(options) === 'string'){
+        name = options;
+        options = {};
+    } else if(typeof(options) === 'object') {
+        name = '';
+    }
+
+    /**
+     * @property name
+     * @type {String}
+     */
+    this.name = name;
+
+    /**
+     * material id.
+     * @property id
+     * @type {number}
+     */
+    this.id = Material.idCounter++;
+
+    /**
+     * Friction for this material. If non-negative, it will be used instead of the friction given by ContactMaterials. If there's no matching ContactMaterial, the value from .defaultContactMaterial in the World will be used.
+     * @property {number} friction
+     */
+    this.friction = typeof(options.friction) !== 'undefined' ? options.friction : -1;
+
+    /**
+     * Restitution for this material. If non-negative, it will be used instead of the restitution given by ContactMaterials. If there's no matching ContactMaterial, the value from .defaultContactMaterial in the World will be used.
+     * @property {number} restitution
+     */
+    this.restitution = typeof(options.restitution) !== 'undefined' ? options.restitution : -1;
+}
+
+Material.idCounter = 0;
+
+},{}],42:[function(require,module,exports){
+module.exports = JacobianElement;
+
+var Vec3 = require('./Vec3');
+
+/**
+ * An element containing 6 entries, 3 spatial and 3 rotational degrees of freedom.
+ * @class JacobianElement
+ * @constructor
+ */
+function JacobianElement(){
+
+    /**
+     * @property {Vec3} spatial
+     */
+    this.spatial = new Vec3();
+
+    /**
+     * @property {Vec3} rotational
+     */
+    this.rotational = new Vec3();
+}
+
+/**
+ * Multiply with other JacobianElement
+ * @method multiplyElement
+ * @param  {JacobianElement} element
+ * @return {Number}
+ */
+JacobianElement.prototype.multiplyElement = function(element){
+    return element.spatial.dot(this.spatial) + element.rotational.dot(this.rotational);
+};
+
+/**
+ * Multiply with two vectors
+ * @method multiplyVectors
+ * @param  {Vec3} spatial
+ * @param  {Vec3} rotational
+ * @return {Number}
+ */
+JacobianElement.prototype.multiplyVectors = function(spatial,rotational){
+    return spatial.dot(this.spatial) + rotational.dot(this.rotational);
+};
+
+},{"./Vec3":46}],43:[function(require,module,exports){
+module.exports = Mat3;
+
+var Vec3 = require('./Vec3');
+
+/**
+ * A 3x3 matrix.
+ * @class Mat3
+ * @constructor
+ * @param array elements Array of nine elements. Optional.
+ * @author schteppe / http://github.com/schteppe
+ */
+function Mat3(elements){
+    /**
+     * A vector of length 9, containing all matrix elements
+     * @property {Array} elements
+     */
+    if(elements){
+        this.elements = elements;
+    } else {
+        this.elements = [0,0,0,0,0,0,0,0,0];
+    }
+}
+
+/**
+ * Sets the matrix to identity
+ * @method identity
+ * @todo Should perhaps be renamed to setIdentity() to be more clear.
+ * @todo Create another function that immediately creates an identity matrix eg. eye()
+ */
+Mat3.prototype.identity = function(){
+    var e = this.elements;
+    e[0] = 1;
+    e[1] = 0;
+    e[2] = 0;
+
+    e[3] = 0;
+    e[4] = 1;
+    e[5] = 0;
+
+    e[6] = 0;
+    e[7] = 0;
+    e[8] = 1;
+};
+
+/**
+ * Set all elements to zero
+ * @method setZero
+ */
+Mat3.prototype.setZero = function(){
+    var e = this.elements;
+    e[0] = 0;
+    e[1] = 0;
+    e[2] = 0;
+    e[3] = 0;
+    e[4] = 0;
+    e[5] = 0;
+    e[6] = 0;
+    e[7] = 0;
+    e[8] = 0;
+};
+
+/**
+ * Sets the matrix diagonal elements from a Vec3
+ * @method setTrace
+ * @param {Vec3} vec3
+ */
+Mat3.prototype.setTrace = function(vec3){
+    var e = this.elements;
+    e[0] = vec3.x;
+    e[4] = vec3.y;
+    e[8] = vec3.z;
+};
+
+/**
+ * Gets the matrix diagonal elements
+ * @method getTrace
+ * @return {Vec3}
+ */
+Mat3.prototype.getTrace = function(target){
+    var target = target || new Vec3();
+    var e = this.elements;
+    target.x = e[0];
+    target.y = e[4];
+    target.z = e[8];
+};
+
+/**
+ * Matrix-Vector multiplication
+ * @method vmult
+ * @param {Vec3} v The vector to multiply with
+ * @param {Vec3} target Optional, target to save the result in.
+ */
+Mat3.prototype.vmult = function(v,target){
+    target = target || new Vec3();
+
+    var e = this.elements,
+        x = v.x,
+        y = v.y,
+        z = v.z;
+    target.x = e[0]*x + e[1]*y + e[2]*z;
+    target.y = e[3]*x + e[4]*y + e[5]*z;
+    target.z = e[6]*x + e[7]*y + e[8]*z;
+
+    return target;
+};
+
+/**
+ * Matrix-scalar multiplication
+ * @method smult
+ * @param {Number} s
+ */
+Mat3.prototype.smult = function(s){
+    for(var i=0; i<this.elements.length; i++){
+        this.elements[i] *= s;
+    }
+};
+
+/**
+ * Matrix multiplication
+ * @method mmult
+ * @param {Mat3} m Matrix to multiply with from left side.
+ * @return {Mat3} The result.
+ */
+Mat3.prototype.mmult = function(m,target){
+    var r = target || new Mat3();
+    for(var i=0; i<3; i++){
+        for(var j=0; j<3; j++){
+            var sum = 0.0;
+            for(var k=0; k<3; k++){
+                sum += m.elements[i+k*3] * this.elements[k+j*3];
+            }
+            r.elements[i+j*3] = sum;
+        }
+    }
+    return r;
+};
+
+/**
+ * Scale each column of the matrix
+ * @method scale
+ * @param {Vec3} v
+ * @return {Mat3} The result.
+ */
+Mat3.prototype.scale = function(v,target){
+    target = target || new Mat3();
+    var e = this.elements,
+        t = target.elements;
+    for(var i=0; i!==3; i++){
+        t[3*i + 0] = v.x * e[3*i + 0];
+        t[3*i + 1] = v.y * e[3*i + 1];
+        t[3*i + 2] = v.z * e[3*i + 2];
+    }
+    return target;
+};
+
+/**
+ * Solve Ax=b
+ * @method solve
+ * @param {Vec3} b The right hand side
+ * @param {Vec3} target Optional. Target vector to save in.
+ * @return {Vec3} The solution x
+ * @todo should reuse arrays
+ */
+Mat3.prototype.solve = function(b,target){
+    target = target || new Vec3();
+
+    // Construct equations
+    var nr = 3; // num rows
+    var nc = 4; // num cols
+    var eqns = [];
+    for(var i=0; i<nr*nc; i++){
+        eqns.push(0);
+    }
+    var i,j;
+    for(i=0; i<3; i++){
+        for(j=0; j<3; j++){
+            eqns[i+nc*j] = this.elements[i+3*j];
+        }
+    }
+    eqns[3+4*0] = b.x;
+    eqns[3+4*1] = b.y;
+    eqns[3+4*2] = b.z;
+
+    // Compute right upper triangular version of the matrix - Gauss elimination
+    var n = 3, k = n, np;
+    var kp = 4; // num rows
+    var p, els;
+    do {
+        i = k - n;
+        if (eqns[i+nc*i] === 0) {
+            // the pivot is null, swap lines
+            for (j = i + 1; j < k; j++) {
+                if (eqns[i+nc*j] !== 0) {
+                    np = kp;
+                    do {  // do ligne( i ) = ligne( i ) + ligne( k )
+                        p = kp - np;
+                        eqns[p+nc*i] += eqns[p+nc*j];
+                    } while (--np);
+                    break;
+                }
+            }
+        }
+        if (eqns[i+nc*i] !== 0) {
+            for (j = i + 1; j < k; j++) {
+                var multiplier = eqns[i+nc*j] / eqns[i+nc*i];
+                np = kp;
+                do {  // do ligne( k ) = ligne( k ) - multiplier * ligne( i )
+                    p = kp - np;
+                    eqns[p+nc*j] = p <= i ? 0 : eqns[p+nc*j] - eqns[p+nc*i] * multiplier ;
+                } while (--np);
+            }
+        }
+    } while (--n);
+
+    // Get the solution
+    target.z = eqns[2*nc+3] / eqns[2*nc+2];
+    target.y = (eqns[1*nc+3] - eqns[1*nc+2]*target.z) / eqns[1*nc+1];
+    target.x = (eqns[0*nc+3] - eqns[0*nc+2]*target.z - eqns[0*nc+1]*target.y) / eqns[0*nc+0];
+
+    if(isNaN(target.x) || isNaN(target.y) || isNaN(target.z) || target.x===Infinity || target.y===Infinity || target.z===Infinity){
+        throw "Could not solve equation! Got x=["+target.toString()+"], b=["+b.toString()+"], A=["+this.toString()+"]";
+    }
+
+    return target;
+};
+
+/**
+ * Get an element in the matrix by index. Index starts at 0, not 1!!!
+ * @method e
+ * @param {Number} row
+ * @param {Number} column
+ * @param {Number} value Optional. If provided, the matrix element will be set to this value.
+ * @return {Number}
+ */
+Mat3.prototype.e = function( row , column ,value){
+    if(value===undefined){
+        return this.elements[column+3*row];
+    } else {
+        // Set value
+        this.elements[column+3*row] = value;
+    }
+};
+
+/**
+ * Copy another matrix into this matrix object.
+ * @method copy
+ * @param {Mat3} source
+ * @return {Mat3} this
+ */
+Mat3.prototype.copy = function(source){
+    for(var i=0; i < source.elements.length; i++){
+        this.elements[i] = source.elements[i];
+    }
+    return this;
+};
+
+/**
+ * Returns a string representation of the matrix.
+ * @method toString
+ * @return string
+ */
+Mat3.prototype.toString = function(){
+    var r = "";
+    var sep = ",";
+    for(var i=0; i<9; i++){
+        r += this.elements[i] + sep;
+    }
+    return r;
+};
+
+/**
+ * reverse the matrix
+ * @method reverse
+ * @param {Mat3} target Optional. Target matrix to save in.
+ * @return {Mat3} The solution x
+ */
+Mat3.prototype.reverse = function(target){
+
+    target = target || new Mat3();
+
+    // Construct equations
+    var nr = 3; // num rows
+    var nc = 6; // num cols
+    var eqns = [];
+    for(var i=0; i<nr*nc; i++){
+        eqns.push(0);
+    }
+    var i,j;
+    for(i=0; i<3; i++){
+        for(j=0; j<3; j++){
+            eqns[i+nc*j] = this.elements[i+3*j];
+        }
+    }
+    eqns[3+6*0] = 1;
+    eqns[3+6*1] = 0;
+    eqns[3+6*2] = 0;
+    eqns[4+6*0] = 0;
+    eqns[4+6*1] = 1;
+    eqns[4+6*2] = 0;
+    eqns[5+6*0] = 0;
+    eqns[5+6*1] = 0;
+    eqns[5+6*2] = 1;
+
+    // Compute right upper triangular version of the matrix - Gauss elimination
+    var n = 3, k = n, np;
+    var kp = nc; // num rows
+    var p;
+    do {
+        i = k - n;
+        if (eqns[i+nc*i] === 0) {
+            // the pivot is null, swap lines
+            for (j = i + 1; j < k; j++) {
+                if (eqns[i+nc*j] !== 0) {
+                    np = kp;
+                    do { // do line( i ) = line( i ) + line( k )
+                        p = kp - np;
+                        eqns[p+nc*i] += eqns[p+nc*j];
+                    } while (--np);
+                    break;
+                }
+            }
+        }
+        if (eqns[i+nc*i] !== 0) {
+            for (j = i + 1; j < k; j++) {
+                var multiplier = eqns[i+nc*j] / eqns[i+nc*i];
+                np = kp;
+                do { // do line( k ) = line( k ) - multiplier * line( i )
+                    p = kp - np;
+                    eqns[p+nc*j] = p <= i ? 0 : eqns[p+nc*j] - eqns[p+nc*i] * multiplier ;
+                } while (--np);
+            }
+        }
+    } while (--n);
+
+    // eliminate the upper left triangle of the matrix
+    i = 2;
+    do {
+        j = i-1;
+        do {
+            var multiplier = eqns[i+nc*j] / eqns[i+nc*i];
+            np = nc;
+            do {
+                p = nc - np;
+                eqns[p+nc*j] =  eqns[p+nc*j] - eqns[p+nc*i] * multiplier ;
+            } while (--np);
+        } while (j--);
+    } while (--i);
+
+    // operations on the diagonal
+    i = 2;
+    do {
+        var multiplier = 1 / eqns[i+nc*i];
+        np = nc;
+        do {
+            p = nc - np;
+            eqns[p+nc*i] = eqns[p+nc*i] * multiplier ;
+        } while (--np);
+    } while (i--);
+
+    i = 2;
+    do {
+        j = 2;
+        do {
+            p = eqns[nr+j+nc*i];
+            if( isNaN( p ) || p ===Infinity ){
+                throw "Could not reverse! A=["+this.toString()+"]";
+            }
+            target.e( i , j , p );
+        } while (j--);
+    } while (i--);
+
+    return target;
+};
+
+/**
+ * Set the matrix from a quaterion
+ * @method setRotationFromQuaternion
+ * @param {Quaternion} q
+ */
+Mat3.prototype.setRotationFromQuaternion = function( q ) {
+    var x = q.x, y = q.y, z = q.z, w = q.w,
+        x2 = x + x, y2 = y + y, z2 = z + z,
+        xx = x * x2, xy = x * y2, xz = x * z2,
+        yy = y * y2, yz = y * z2, zz = z * z2,
+        wx = w * x2, wy = w * y2, wz = w * z2,
+        e = this.elements;
+
+    e[3*0 + 0] = 1 - ( yy + zz );
+    e[3*0 + 1] = xy - wz;
+    e[3*0 + 2] = xz + wy;
+
+    e[3*1 + 0] = xy + wz;
+    e[3*1 + 1] = 1 - ( xx + zz );
+    e[3*1 + 2] = yz - wx;
+
+    e[3*2 + 0] = xz - wy;
+    e[3*2 + 1] = yz + wx;
+    e[3*2 + 2] = 1 - ( xx + yy );
+
+    return this;
+};
+
+/**
+ * Transpose the matrix
+ * @method transpose
+ * @param  {Mat3} target Where to store the result.
+ * @return {Mat3} The target Mat3, or a new Mat3 if target was omitted.
+ */
+Mat3.prototype.transpose = function( target ) {
+    target = target || new Mat3();
+
+    var Mt = target.elements,
+        M = this.elements;
+
+    for(var i=0; i!==3; i++){
+        for(var j=0; j!==3; j++){
+            Mt[3*i + j] = M[3*j + i];
+        }
+    }
+
+    return target;
+};
+
+},{"./Vec3":46}],44:[function(require,module,exports){
+module.exports = Quaternion;
+
+var Vec3 = require('./Vec3');
+
+/**
+ * A Quaternion describes a rotation in 3D space. The Quaternion is mathematically defined as Q = x*i + y*j + z*k + w, where (i,j,k) are imaginary basis vectors. (x,y,z) can be seen as a vector related to the axis of rotation, while the real multiplier, w, is related to the amount of rotation.
+ * @class Quaternion
+ * @constructor
+ * @param {Number} x Multiplier of the imaginary basis vector i.
+ * @param {Number} y Multiplier of the imaginary basis vector j.
+ * @param {Number} z Multiplier of the imaginary basis vector k.
+ * @param {Number} w Multiplier of the real part.
+ * @see http://en.wikipedia.org/wiki/Quaternion
+ */
+function Quaternion(x,y,z,w){
+    /**
+     * @property {Number} x
+     */
+    this.x = x!==undefined ? x : 0;
+
+    /**
+     * @property {Number} y
+     */
+    this.y = y!==undefined ? y : 0;
+
+    /**
+     * @property {Number} z
+     */
+    this.z = z!==undefined ? z : 0;
+
+    /**
+     * The multiplier of the real quaternion basis vector.
+     * @property {Number} w
+     */
+    this.w = w!==undefined ? w : 1;
+}
+
+/**
+ * Set the value of the quaternion.
+ * @method set
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @param {Number} w
+ */
+Quaternion.prototype.set = function(x,y,z,w){
+    this.x = x;
+    this.y = y;
+    this.z = z;
+    this.w = w;
+    return this;
+};
+
+/**
+ * Convert to a readable format
+ * @method toString
+ * @return string
+ */
+Quaternion.prototype.toString = function(){
+    return this.x+","+this.y+","+this.z+","+this.w;
+};
+
+/**
+ * Convert to an Array
+ * @method toArray
+ * @return Array
+ */
+Quaternion.prototype.toArray = function(){
+    return [this.x, this.y, this.z, this.w];
+};
+
+/**
+ * Set the quaternion components given an axis and an angle.
+ * @method setFromAxisAngle
+ * @param {Vec3} axis
+ * @param {Number} angle in radians
+ */
+Quaternion.prototype.setFromAxisAngle = function(axis,angle){
+    var s = Math.sin(angle*0.5);
+    this.x = axis.x * s;
+    this.y = axis.y * s;
+    this.z = axis.z * s;
+    this.w = Math.cos(angle*0.5);
+    return this;
+};
+
+/**
+ * Converts the quaternion to axis/angle representation.
+ * @method toAxisAngle
+ * @param {Vec3} [targetAxis] A vector object to reuse for storing the axis.
+ * @return {Array} An array, first elemnt is the axis and the second is the angle in radians.
+ */
+Quaternion.prototype.toAxisAngle = function(targetAxis){
+    targetAxis = targetAxis || new Vec3();
+    this.normalize(); // if w>1 acos and sqrt will produce errors, this cant happen if quaternion is normalised
+    var angle = 2 * Math.acos(this.w);
+    var s = Math.sqrt(1-this.w*this.w); // assuming quaternion normalised then w is less than 1, so term always positive.
+    if (s < 0.001) { // test to avoid divide by zero, s is always positive due to sqrt
+        // if s close to zero then direction of axis not important
+        targetAxis.x = this.x; // if it is important that axis is normalised then replace with x=1; y=z=0;
+        targetAxis.y = this.y;
+        targetAxis.z = this.z;
+    } else {
+        targetAxis.x = this.x / s; // normalise axis
+        targetAxis.y = this.y / s;
+        targetAxis.z = this.z / s;
+    }
+    return [targetAxis,angle];
+};
+
+var sfv_t1 = new Vec3(),
+    sfv_t2 = new Vec3();
+
+/**
+ * Set the quaternion value given two vectors. The resulting rotation will be the needed rotation to rotate u to v.
+ * @method setFromVectors
+ * @param {Vec3} u
+ * @param {Vec3} v
+ */
+Quaternion.prototype.setFromVectors = function(u,v){
+    if(u.isAntiparallelTo(v)){
+        var t1 = sfv_t1;
+        var t2 = sfv_t2;
+
+        u.tangents(t1,t2);
+        this.setFromAxisAngle(t1,Math.PI);
+    } else {
+        var a = u.cross(v);
+        this.x = a.x;
+        this.y = a.y;
+        this.z = a.z;
+        this.w = Math.sqrt(Math.pow(u.norm(),2) * Math.pow(v.norm(),2)) + u.dot(v);
+        this.normalize();
+    }
+    return this;
+};
+
+/**
+ * Quaternion multiplication
+ * @method mult
+ * @param {Quaternion} q
+ * @param {Quaternion} target Optional.
+ * @return {Quaternion}
+ */
+var Quaternion_mult_va = new Vec3();
+var Quaternion_mult_vb = new Vec3();
+var Quaternion_mult_vaxvb = new Vec3();
+Quaternion.prototype.mult = function(q,target){
+    target = target || new Quaternion();
+
+    var ax = this.x, ay = this.y, az = this.z, aw = this.w,
+        bx = q.x, by = q.y, bz = q.z, bw = q.w;
+
+    target.x = ax * bw + aw * bx + ay * bz - az * by;
+    target.y = ay * bw + aw * by + az * bx - ax * bz;
+    target.z = az * bw + aw * bz + ax * by - ay * bx;
+    target.w = aw * bw - ax * bx - ay * by - az * bz;
+
+    return target;
+};
+
+/**
+ * Get the inverse quaternion rotation.
+ * @method inverse
+ * @param {Quaternion} target
+ * @return {Quaternion}
+ */
+Quaternion.prototype.inverse = function(target){
+    var x = this.x, y = this.y, z = this.z, w = this.w;
+    target = target || new Quaternion();
+
+    this.conjugate(target);
+    var inorm2 = 1/(x*x + y*y + z*z + w*w);
+    target.x *= inorm2;
+    target.y *= inorm2;
+    target.z *= inorm2;
+    target.w *= inorm2;
+
+    return target;
+};
+
+/**
+ * Get the quaternion conjugate
+ * @method conjugate
+ * @param {Quaternion} target
+ * @return {Quaternion}
+ */
+Quaternion.prototype.conjugate = function(target){
+    target = target || new Quaternion();
+
+    target.x = -this.x;
+    target.y = -this.y;
+    target.z = -this.z;
+    target.w = this.w;
+
+    return target;
+};
+
+/**
+ * Normalize the quaternion. Note that this changes the values of the quaternion.
+ * @method normalize
+ */
+Quaternion.prototype.normalize = function(){
+    var l = Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w);
+    if ( l === 0 ) {
+        this.x = 0;
+        this.y = 0;
+        this.z = 0;
+        this.w = 0;
+    } else {
+        l = 1 / l;
+        this.x *= l;
+        this.y *= l;
+        this.z *= l;
+        this.w *= l;
+    }
+    return this;
+};
+
+/**
+ * Approximation of quaternion normalization. Works best when quat is already almost-normalized.
+ * @method normalizeFast
+ * @see http://jsperf.com/fast-quaternion-normalization
+ * @author unphased, https://github.com/unphased
+ */
+Quaternion.prototype.normalizeFast = function () {
+    var f = (3.0-(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w))/2.0;
+    if ( f === 0 ) {
+        this.x = 0;
+        this.y = 0;
+        this.z = 0;
+        this.w = 0;
+    } else {
+        this.x *= f;
+        this.y *= f;
+        this.z *= f;
+        this.w *= f;
+    }
+    return this;
+};
+
+/**
+ * Multiply the quaternion by a vector
+ * @method vmult
+ * @param {Vec3} v
+ * @param {Vec3} target Optional
+ * @return {Vec3}
+ */
+Quaternion.prototype.vmult = function(v,target){
+    target = target || new Vec3();
+
+    var x = v.x,
+        y = v.y,
+        z = v.z;
+
+    var qx = this.x,
+        qy = this.y,
+        qz = this.z,
+        qw = this.w;
+
+    // q*v
+    var ix =  qw * x + qy * z - qz * y,
+    iy =  qw * y + qz * x - qx * z,
+    iz =  qw * z + qx * y - qy * x,
+    iw = -qx * x - qy * y - qz * z;
+
+    target.x = ix * qw + iw * -qx + iy * -qz - iz * -qy;
+    target.y = iy * qw + iw * -qy + iz * -qx - ix * -qz;
+    target.z = iz * qw + iw * -qz + ix * -qy - iy * -qx;
+
+    return target;
+};
+
+/**
+ * Copies value of source to this quaternion.
+ * @method copy
+ * @param {Quaternion} source
+ * @return {Quaternion} this
+ */
+Quaternion.prototype.copy = function(source){
+    this.x = source.x;
+    this.y = source.y;
+    this.z = source.z;
+    this.w = source.w;
+    return this;
+};
+
+/**
+ * Convert the quaternion to euler angle representation. Order: YZX, as this page describes: http://www.euclideanspace.com/maths/standards/index.htm
+ * @method toEuler
+ * @param {Vec3} target
+ * @param string order Three-character string e.g. "YZX", which also is default.
+ */
+Quaternion.prototype.toEuler = function(target,order){
+    order = order || "YZX";
+
+    var heading, attitude, bank;
+    var x = this.x, y = this.y, z = this.z, w = this.w;
+
+    switch(order){
+    case "YZX":
+        var test = x*y + z*w;
+        if (test > 0.499) { // singularity at north pole
+            heading = 2 * Math.atan2(x,w);
+            attitude = Math.PI/2;
+            bank = 0;
+        }
+        if (test < -0.499) { // singularity at south pole
+            heading = -2 * Math.atan2(x,w);
+            attitude = - Math.PI/2;
+            bank = 0;
+        }
+        if(isNaN(heading)){
+            var sqx = x*x;
+            var sqy = y*y;
+            var sqz = z*z;
+            heading = Math.atan2(2*y*w - 2*x*z , 1 - 2*sqy - 2*sqz); // Heading
+            attitude = Math.asin(2*test); // attitude
+            bank = Math.atan2(2*x*w - 2*y*z , 1 - 2*sqx - 2*sqz); // bank
+        }
+        break;
+    default:
+        throw new Error("Euler order "+order+" not supported yet.");
+    }
+
+    target.y = heading;
+    target.z = attitude;
+    target.x = bank;
+};
+
+/**
+ * See http://www.mathworks.com/matlabcentral/fileexchange/20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/content/SpinCalc.m
+ * @method setFromEuler
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @param {String} order The order to apply angles: 'XYZ' or 'YXZ' or any other combination
+ */
+Quaternion.prototype.setFromEuler = function ( x, y, z, order ) {
+    order = order || "XYZ";
+
+    var c1 = Math.cos( x / 2 );
+    var c2 = Math.cos( y / 2 );
+    var c3 = Math.cos( z / 2 );
+    var s1 = Math.sin( x / 2 );
+    var s2 = Math.sin( y / 2 );
+    var s3 = Math.sin( z / 2 );
+
+    if ( order === 'XYZ' ) {
+
+        this.x = s1 * c2 * c3 + c1 * s2 * s3;
+        this.y = c1 * s2 * c3 - s1 * c2 * s3;
+        this.z = c1 * c2 * s3 + s1 * s2 * c3;
+        this.w = c1 * c2 * c3 - s1 * s2 * s3;
+
+    } else if ( order === 'YXZ' ) {
+
+        this.x = s1 * c2 * c3 + c1 * s2 * s3;
+        this.y = c1 * s2 * c3 - s1 * c2 * s3;
+        this.z = c1 * c2 * s3 - s1 * s2 * c3;
+        this.w = c1 * c2 * c3 + s1 * s2 * s3;
+
+    } else if ( order === 'ZXY' ) {
+
+        this.x = s1 * c2 * c3 - c1 * s2 * s3;
+        this.y = c1 * s2 * c3 + s1 * c2 * s3;
+        this.z = c1 * c2 * s3 + s1 * s2 * c3;
+        this.w = c1 * c2 * c3 - s1 * s2 * s3;
+
+    } else if ( order === 'ZYX' ) {
+
+        this.x = s1 * c2 * c3 - c1 * s2 * s3;
+        this.y = c1 * s2 * c3 + s1 * c2 * s3;
+        this.z = c1 * c2 * s3 - s1 * s2 * c3;
+        this.w = c1 * c2 * c3 + s1 * s2 * s3;
+
+    } else if ( order === 'YZX' ) {
+
+        this.x = s1 * c2 * c3 + c1 * s2 * s3;
+        this.y = c1 * s2 * c3 + s1 * c2 * s3;
+        this.z = c1 * c2 * s3 - s1 * s2 * c3;
+        this.w = c1 * c2 * c3 - s1 * s2 * s3;
+
+    } else if ( order === 'XZY' ) {
+
+        this.x = s1 * c2 * c3 - c1 * s2 * s3;
+        this.y = c1 * s2 * c3 - s1 * c2 * s3;
+        this.z = c1 * c2 * s3 + s1 * s2 * c3;
+        this.w = c1 * c2 * c3 + s1 * s2 * s3;
+
+    }
+
+    return this;
+};
+
+/**
+ * @method clone
+ * @return {Quaternion}
+ */
+Quaternion.prototype.clone = function(){
+    return new Quaternion(this.x, this.y, this.z, this.w);
+};
+
+/**
+ * Performs a spherical linear interpolation between two quat
+ *
+ * @method slerp
+ * @param {Quaternion} toQuat second operand
+ * @param {Number} t interpolation amount between the self quaternion and toQuat
+ * @param {Quaternion} [target] A quaternion to store the result in. If not provided, a new one will be created.
+ * @returns {Quaternion} The "target" object
+ */
+Quaternion.prototype.slerp = function (toQuat, t, target) {
+    target = target || new Quaternion();
+
+    var ax = this.x,
+        ay = this.y,
+        az = this.z,
+        aw = this.w,
+        bx = toQuat.x,
+        by = toQuat.y,
+        bz = toQuat.z,
+        bw = toQuat.w;
+
+    var omega, cosom, sinom, scale0, scale1;
+
+    // calc cosine
+    cosom = ax * bx + ay * by + az * bz + aw * bw;
+
+    // adjust signs (if necessary)
+    if ( cosom < 0.0 ) {
+        cosom = -cosom;
+        bx = - bx;
+        by = - by;
+        bz = - bz;
+        bw = - bw;
+    }
+
+    // calculate coefficients
+    if ( (1.0 - cosom) > 0.000001 ) {
+        // standard case (slerp)
+        omega  = Math.acos(cosom);
+        sinom  = Math.sin(omega);
+        scale0 = Math.sin((1.0 - t) * omega) / sinom;
+        scale1 = Math.sin(t * omega) / sinom;
+    } else {
+        // "from" and "to" quaternions are very close
+        //  ... so we can do a linear interpolation
+        scale0 = 1.0 - t;
+        scale1 = t;
+    }
+
+    // calculate final values
+    target.x = scale0 * ax + scale1 * bx;
+    target.y = scale0 * ay + scale1 * by;
+    target.z = scale0 * az + scale1 * bz;
+    target.w = scale0 * aw + scale1 * bw;
+
+    return target;
+};
+
+/**
+ * Rotate an absolute orientation quaternion given an angular velocity and a time step.
+ * @param  {Vec3} angularVelocity
+ * @param  {number} dt
+ * @param  {Vec3} angularFactor
+ * @param  {Quaternion} target
+ * @return {Quaternion} The "target" object
+ */
+Quaternion.prototype.integrate = function(angularVelocity, dt, angularFactor, target){
+    target = target || new Quaternion();
+
+    var ax = angularVelocity.x * angularFactor.x,
+        ay = angularVelocity.y * angularFactor.y,
+        az = angularVelocity.z * angularFactor.z,
+        bx = this.x,
+        by = this.y,
+        bz = this.z,
+        bw = this.w;
+
+    var half_dt = dt * 0.5;
+
+    target.x += half_dt * (ax * bw + ay * bz - az * by);
+    target.y += half_dt * (ay * bw + az * bx - ax * bz);
+    target.z += half_dt * (az * bw + ax * by - ay * bx);
+    target.w += half_dt * (- ax * bx - ay * by - az * bz);
+
+    return target;
+};
+},{"./Vec3":46}],45:[function(require,module,exports){
+var Vec3 = require('./Vec3');
+var Quaternion = require('./Quaternion');
+
+module.exports = Transform;
+
+/**
+ * @class Transform
+ * @constructor
+ */
+function Transform(options) {
+    options = options || {};
+
+	/**
+	 * @property {Vec3} position
+	 */
+	this.position = new Vec3();
+    if(options.position){
+        this.position.copy(options.position);
+    }
+
+	/**
+	 * @property {Quaternion} quaternion
+	 */
+	this.quaternion = new Quaternion();
+    if(options.quaternion){
+        this.quaternion.copy(options.quaternion);
+    }
+}
+
+var tmpQuat = new Quaternion();
+
+/**
+ * @static
+ * @method pointToLocaFrame
+ * @param {Vec3} position
+ * @param {Quaternion} quaternion
+ * @param {Vec3} worldPoint
+ * @param {Vec3} result
+ */
+Transform.pointToLocalFrame = function(position, quaternion, worldPoint, result){
+    var result = result || new Vec3();
+    worldPoint.vsub(position, result);
+    quaternion.conjugate(tmpQuat);
+    tmpQuat.vmult(result, result);
+    return result;
+};
+
+/**
+ * Get a global point in local transform coordinates.
+ * @method pointToLocal
+ * @param  {Vec3} point
+ * @param  {Vec3} result
+ * @return {Vec3} The "result" vector object
+ */
+Transform.prototype.pointToLocal = function(worldPoint, result){
+    return Transform.pointToLocalFrame(this.position, this.quaternion, worldPoint, result);
+};
+
+/**
+ * @static
+ * @method pointToWorldFrame
+ * @param {Vec3} position
+ * @param {Vec3} quaternion
+ * @param {Vec3} localPoint
+ * @param {Vec3} result
+ */
+Transform.pointToWorldFrame = function(position, quaternion, localPoint, result){
+    var result = result || new Vec3();
+    quaternion.vmult(localPoint, result);
+    result.vadd(position, result);
+    return result;
+};
+
+/**
+ * Get a local point in global transform coordinates.
+ * @method pointToWorld
+ * @param  {Vec3} point
+ * @param  {Vec3} result
+ * @return {Vec3} The "result" vector object
+ */
+Transform.prototype.pointToWorld = function(localPoint, result){
+    return Transform.pointToWorldFrame(this.position, this.quaternion, localPoint, result);
+};
+
+
+Transform.prototype.vectorToWorldFrame = function(localVector, result){
+    var result = result || new Vec3();
+    this.quaternion.vmult(localVector, result);
+    return result;
+};
+
+Transform.vectorToWorldFrame = function(quaternion, localVector, result){
+    quaternion.vmult(localVector, result);
+    return result;
+};
+
+Transform.vectorToLocalFrame = function(position, quaternion, worldVector, result){
+    var result = result || new Vec3();
+    quaternion.w *= -1;
+    quaternion.vmult(worldVector, result);
+    quaternion.w *= -1;
+    return result;
+};
+
+},{"./Quaternion":44,"./Vec3":46}],46:[function(require,module,exports){
+module.exports = Vec3;
+
+var Mat3 = require('./Mat3');
+
+/**
+ * 3-dimensional vector
+ * @class Vec3
+ * @constructor
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @author schteppe
+ * @example
+ *     var v = new Vec3(1, 2, 3);
+ *     console.log('x=' + v.x); // x=1
+ */
+function Vec3(x,y,z){
+    /**
+     * @property x
+     * @type {Number}
+     */
+    this.x = x||0.0;
+
+    /**
+     * @property y
+     * @type {Number}
+     */
+    this.y = y||0.0;
+
+    /**
+     * @property z
+     * @type {Number}
+     */
+    this.z = z||0.0;
+}
+
+/**
+ * @static
+ * @property {Vec3} ZERO
+ */
+Vec3.ZERO = new Vec3(0, 0, 0);
+
+/**
+ * @static
+ * @property {Vec3} UNIT_X
+ */
+Vec3.UNIT_X = new Vec3(1, 0, 0);
+
+/**
+ * @static
+ * @property {Vec3} UNIT_Y
+ */
+Vec3.UNIT_Y = new Vec3(0, 1, 0);
+
+/**
+ * @static
+ * @property {Vec3} UNIT_Z
+ */
+Vec3.UNIT_Z = new Vec3(0, 0, 1);
+
+/**
+ * Vector cross product
+ * @method cross
+ * @param {Vec3} v
+ * @param {Vec3} target Optional. Target to save in.
+ * @return {Vec3}
+ */
+Vec3.prototype.cross = function(v,target){
+    var vx=v.x, vy=v.y, vz=v.z, x=this.x, y=this.y, z=this.z;
+    target = target || new Vec3();
+
+    target.x = (y * vz) - (z * vy);
+    target.y = (z * vx) - (x * vz);
+    target.z = (x * vy) - (y * vx);
+
+    return target;
+};
+
+/**
+ * Set the vectors' 3 elements
+ * @method set
+ * @param {Number} x
+ * @param {Number} y
+ * @param {Number} z
+ * @return Vec3
+ */
+Vec3.prototype.set = function(x,y,z){
+    this.x = x;
+    this.y = y;
+    this.z = z;
+    return this;
+};
+
+/**
+ * Set all components of the vector to zero.
+ * @method setZero
+ */
+Vec3.prototype.setZero = function(){
+    this.x = this.y = this.z = 0;
+};
+
+/**
+ * Vector addition
+ * @method vadd
+ * @param {Vec3} v
+ * @param {Vec3} target Optional.
+ * @return {Vec3}
+ */
+Vec3.prototype.vadd = function(v,target){
+    if(target){
+        target.x = v.x + this.x;
+        target.y = v.y + this.y;
+        target.z = v.z + this.z;
+    } else {
+        return new Vec3(this.x + v.x,
+                               this.y + v.y,
+                               this.z + v.z);
+    }
+};
+
+/**
+ * Vector subtraction
+ * @method vsub
+ * @param {Vec3} v
+ * @param {Vec3} target Optional. Target to save in.
+ * @return {Vec3}
+ */
+Vec3.prototype.vsub = function(v,target){
+    if(target){
+        target.x = this.x - v.x;
+        target.y = this.y - v.y;
+        target.z = this.z - v.z;
+    } else {
+        return new Vec3(this.x-v.x,
+                               this.y-v.y,
+                               this.z-v.z);
+    }
+};
+
+/**
+ * Get the cross product matrix a_cross from a vector, such that a x b = a_cross * b = c
+ * @method crossmat
+ * @see http://www8.cs.umu.se/kurser/TDBD24/VT06/lectures/Lecture6.pdf
+ * @return {Mat3}
+ */
+Vec3.prototype.crossmat = function(){
+    return new Mat3([     0,  -this.z,   this.y,
+                            this.z,        0,  -this.x,
+                           -this.y,   this.x,        0]);
+};
+
+/**
+ * Normalize the vector. Note that this changes the values in the vector.
+ * @method normalize
+ * @return {Number} Returns the norm of the vector
+ */
+Vec3.prototype.normalize = function(){
+    var x=this.x, y=this.y, z=this.z;
+    var n = Math.sqrt(x*x + y*y + z*z);
+    if(n>0.0){
+        var invN = 1/n;
+        this.x *= invN;
+        this.y *= invN;
+        this.z *= invN;
+    } else {
+        // Make something up
+        this.x = 0;
+        this.y = 0;
+        this.z = 0;
+    }
+    return n;
+};
+
+/**
+ * Get the version of this vector that is of length 1.
+ * @method unit
+ * @param {Vec3} target Optional target to save in
+ * @return {Vec3} Returns the unit vector
+ */
+Vec3.prototype.unit = function(target){
+    target = target || new Vec3();
+    var x=this.x, y=this.y, z=this.z;
+    var ninv = Math.sqrt(x*x + y*y + z*z);
+    if(ninv>0.0){
+        ninv = 1.0/ninv;
+        target.x = x * ninv;
+        target.y = y * ninv;
+        target.z = z * ninv;
+    } else {
+        target.x = 1;
+        target.y = 0;
+        target.z = 0;
+    }
+    return target;
+};
+
+/**
+ * Get the length of the vector
+ * @method norm
+ * @return {Number}
+ * @deprecated Use .length() instead
+ */
+Vec3.prototype.norm = function(){
+    var x=this.x, y=this.y, z=this.z;
+    return Math.sqrt(x*x + y*y + z*z);
+};
+
+/**
+ * Get the length of the vector
+ * @method length
+ * @return {Number}
+ */
+Vec3.prototype.length = Vec3.prototype.norm;
+
+/**
+ * Get the squared length of the vector
+ * @method norm2
+ * @return {Number}
+ * @deprecated Use .lengthSquared() instead.
+ */
+Vec3.prototype.norm2 = function(){
+    return this.dot(this);
+};
+
+/**
+ * Get the squared length of the vector.
+ * @method lengthSquared
+ * @return {Number}
+ */
+Vec3.prototype.lengthSquared = Vec3.prototype.norm2;
+
+/**
+ * Get distance from this point to another point
+ * @method distanceTo
+ * @param  {Vec3} p
+ * @return {Number}
+ */
+Vec3.prototype.distanceTo = function(p){
+    var x=this.x, y=this.y, z=this.z;
+    var px=p.x, py=p.y, pz=p.z;
+    return Math.sqrt((px-x)*(px-x)+
+                     (py-y)*(py-y)+
+                     (pz-z)*(pz-z));
+};
+
+/**
+ * Get squared distance from this point to another point
+ * @method distanceSquared
+ * @param  {Vec3} p
+ * @return {Number}
+ */
+Vec3.prototype.distanceSquared = function(p){
+    var x=this.x, y=this.y, z=this.z;
+    var px=p.x, py=p.y, pz=p.z;
+    return (px-x)*(px-x) + (py-y)*(py-y) + (pz-z)*(pz-z);
+};
+
+/**
+ * Multiply all the components of the vector with a scalar.
+ * @deprecated Use .scale instead
+ * @method mult
+ * @param {Number} scalar
+ * @param {Vec3} target The vector to save the result in.
+ * @return {Vec3}
+ * @deprecated Use .scale() instead
+ */
+Vec3.prototype.mult = function(scalar,target){
+    target = target || new Vec3();
+    var x = this.x,
+        y = this.y,
+        z = this.z;
+    target.x = scalar * x;
+    target.y = scalar * y;
+    target.z = scalar * z;
+    return target;
+};
+
+/**
+ * Multiply the vector with an other vector, component-wise.
+ * @method mult
+ * @param {Number} vector
+ * @param {Vec3} target The vector to save the result in.
+ * @return {Vec3}
+ */
+Vec3.prototype.vmul = function(vector, target){
+    target = target || new Vec3();
+    target.x = vector.x * this.x;
+    target.y = vector.y * this.y;
+    target.z = vector.z * this.z;
+    return target;
+};
+
+/**
+ * Multiply the vector with a scalar.
+ * @method scale
+ * @param {Number} scalar
+ * @param {Vec3} target
+ * @return {Vec3}
+ */
+Vec3.prototype.scale = Vec3.prototype.mult;
+
+/**
+ * Scale a vector and add it to this vector. Save the result in "target". (target = this + vector * scalar)
+ * @method addScaledVector
+ * @param {Number} scalar
+ * @param {Vec3} vector
+ * @param {Vec3} target The vector to save the result in.
+ * @return {Vec3}
+ */
+Vec3.prototype.addScaledVector = function(scalar, vector, target){
+    target = target || new Vec3();
+    target.x = this.x + scalar * vector.x;
+    target.y = this.y + scalar * vector.y;
+    target.z = this.z + scalar * vector.z;
+    return target;
+};
+
+/**
+ * Calculate dot product
+ * @method dot
+ * @param {Vec3} v
+ * @return {Number}
+ */
+Vec3.prototype.dot = function(v){
+    return this.x * v.x + this.y * v.y + this.z * v.z;
+};
+
+/**
+ * @method isZero
+ * @return bool
+ */
+Vec3.prototype.isZero = function(){
+    return this.x===0 && this.y===0 && this.z===0;
+};
+
+/**
+ * Make the vector point in the opposite direction.
+ * @method negate
+ * @param {Vec3} target Optional target to save in
+ * @return {Vec3}
+ */
+Vec3.prototype.negate = function(target){
+    target = target || new Vec3();
+    target.x = -this.x;
+    target.y = -this.y;
+    target.z = -this.z;
+    return target;
+};
+
+/**
+ * Compute two artificial tangents to the vector
+ * @method tangents
+ * @param {Vec3} t1 Vector object to save the first tangent in
+ * @param {Vec3} t2 Vector object to save the second tangent in
+ */
+var Vec3_tangents_n = new Vec3();
+var Vec3_tangents_randVec = new Vec3();
+Vec3.prototype.tangents = function(t1,t2){
+    var norm = this.norm();
+    if(norm>0.0){
+        var n = Vec3_tangents_n;
+        var inorm = 1/norm;
+        n.set(this.x*inorm,this.y*inorm,this.z*inorm);
+        var randVec = Vec3_tangents_randVec;
+        if(Math.abs(n.x) < 0.9){
+            randVec.set(1,0,0);
+            n.cross(randVec,t1);
+        } else {
+            randVec.set(0,1,0);
+            n.cross(randVec,t1);
+        }
+        n.cross(t1,t2);
+    } else {
+        // The normal length is zero, make something up
+        t1.set(1, 0, 0);
+        t2.set(0, 1, 0);
+    }
+};
+
+/**
+ * Converts to a more readable format
+ * @method toString
+ * @return string
+ */
+Vec3.prototype.toString = function(){
+    return this.x+","+this.y+","+this.z;
+};
+
+/**
+ * Converts to an array
+ * @method toArray
+ * @return Array
+ */
+Vec3.prototype.toArray = function(){
+    return [this.x, this.y, this.z];
+};
+
+/**
+ * Copies value of source to this vector.
+ * @method copy
+ * @param {Vec3} source
+ * @return {Vec3} this
+ */
+Vec3.prototype.copy = function(source){
+    this.x = source.x;
+    this.y = source.y;
+    this.z = source.z;
+    return this;
+};
+
+
+/**
+ * Do a linear interpolation between two vectors
+ * @method lerp
+ * @param {Vec3} v
+ * @param {Number} t A number between 0 and 1. 0 will make this function return u, and 1 will make it return v. Numbers in between will generate a vector in between them.
+ * @param {Vec3} target
+ */
+Vec3.prototype.lerp = function(v,t,target){
+    var x=this.x, y=this.y, z=this.z;
+    target.x = x + (v.x-x)*t;
+    target.y = y + (v.y-y)*t;
+    target.z = z + (v.z-z)*t;
+};
+
+/**
+ * Check if a vector equals is almost equal to another one.
+ * @method almostEquals
+ * @param {Vec3} v
+ * @param {Number} precision
+ * @return bool
+ */
+Vec3.prototype.almostEquals = function(v,precision){
+    if(precision===undefined){
+        precision = 1e-6;
+    }
+    if( Math.abs(this.x-v.x)>precision ||
+        Math.abs(this.y-v.y)>precision ||
+        Math.abs(this.z-v.z)>precision){
+        return false;
+    }
+    return true;
+};
+
+/**
+ * Check if a vector is almost zero
+ * @method almostZero
+ * @param {Number} precision
+ */
+Vec3.prototype.almostZero = function(precision){
+    if(precision===undefined){
+        precision = 1e-6;
+    }
+    if( Math.abs(this.x)>precision ||
+        Math.abs(this.y)>precision ||
+        Math.abs(this.z)>precision){
+        return false;
+    }
+    return true;
+};
+
+var antip_neg = new Vec3();
+
+/**
+ * Check if the vector is anti-parallel to another vector.
+ * @method isAntiparallelTo
+ * @param  {Vec3}  v
+ * @param  {Number}  precision Set to zero for exact comparisons
+ * @return {Boolean}
+ */
+Vec3.prototype.isAntiparallelTo = function(v,precision){
+    this.negate(antip_neg);
+    return antip_neg.almostEquals(v,precision);
+};
+
+/**
+ * Clone the vector
+ * @method clone
+ * @return {Vec3}
+ */
+Vec3.prototype.clone = function(){
+    return new Vec3(this.x, this.y, this.z);
+};
+},{"./Mat3":43}],47:[function(require,module,exports){
+module.exports = Body;
+
+var EventTarget = require('../utils/EventTarget');
+var Shape = require('../shapes/Shape');
+var Vec3 = require('../math/Vec3');
+var Mat3 = require('../math/Mat3');
+var Quaternion = require('../math/Quaternion');
+var Material = require('../material/Material');
+var AABB = require('../collision/AABB');
+var Box = require('../shapes/Box');
+
+/**
+ * Base class for all body types.
+ * @class Body
+ * @constructor
+ * @extends EventTarget
+ * @param {object} [options]
+ * @param {Vec3} [options.position]
+ * @param {Vec3} [options.velocity]
+ * @param {Vec3} [options.angularVelocity]
+ * @param {Quaternion} [options.quaternion]
+ * @param {number} [options.mass]
+ * @param {Material} [options.material]
+ * @param {number} [options.type]
+ * @param {number} [options.linearDamping=0.01]
+ * @param {number} [options.angularDamping=0.01]
+ * @param {boolean} [options.allowSleep=true]
+ * @param {number} [options.sleepSpeedLimit=0.1]
+ * @param {number} [options.sleepTimeLimit=1]
+ * @param {number} [options.collisionFilterGroup=1]
+ * @param {number} [options.collisionFilterMask=1]
+ * @param {boolean} [options.fixedRotation=false]
+ * @param {Vec3} [options.linearFactor]
+ * @param {Vec3} [options.angularFactor]
+ * @param {Shape} [options.shape]
+ * @example
+ *     var body = new Body({
+ *         mass: 1
+ *     });
+ *     var shape = new Sphere(1);
+ *     body.addShape(shape);
+ *     world.addBody(body);
+ */
+function Body(options){
+    options = options || {};
+
+    EventTarget.apply(this);
+
+    this.id = Body.idCounter++;
+
+    /**
+     * Reference to the world the body is living in
+     * @property world
+     * @type {World}
+     */
+    this.world = null;
+
+    /**
+     * Callback function that is used BEFORE stepping the system. Use it to apply forces, for example. Inside the function, "this" will refer to this Body object.
+     * @property preStep
+     * @type {Function}
+     * @deprecated Use World events instead
+     */
+    this.preStep = null;
+
+    /**
+     * Callback function that is used AFTER stepping the system. Inside the function, "this" will refer to this Body object.
+     * @property postStep
+     * @type {Function}
+     * @deprecated Use World events instead
+     */
+    this.postStep = null;
+
+    this.vlambda = new Vec3();
+
+    /**
+     * @property {Number} collisionFilterGroup
+     */
+    this.collisionFilterGroup = typeof(options.collisionFilterGroup) === 'number' ? options.collisionFilterGroup : 1;
+
+    /**
+     * @property {Number} collisionFilterMask
+     */
+    this.collisionFilterMask = typeof(options.collisionFilterMask) === 'number' ? options.collisionFilterMask : 1;
+
+    /**
+     * Whether to produce contact forces when in contact with other bodies. Note that contacts will be generated, but they will be disabled.
+     * @property {Number} collisionResponse
+     */
+	this.collisionResponse = true;
+
+    /**
+     * @property position
+     * @type {Vec3}
+     */
+    this.position = new Vec3();
+
+    /**
+     * @property {Vec3} previousPosition
+     */
+    this.previousPosition = new Vec3();
+
+    /**
+     * Interpolated position of the body.
+     * @property {Vec3} interpolatedPosition
+     */
+    this.interpolatedPosition = new Vec3();
+
+    /**
+     * Initial position of the body
+     * @property initPosition
+     * @type {Vec3}
+     */
+    this.initPosition = new Vec3();
+
+    if(options.position){
+        this.position.copy(options.position);
+        this.previousPosition.copy(options.position);
+        this.interpolatedPosition.copy(options.position);
+        this.initPosition.copy(options.position);
+    }
+
+    /**
+     * @property velocity
+     * @type {Vec3}
+     */
+    this.velocity = new Vec3();
+
+    if(options.velocity){
+        this.velocity.copy(options.velocity);
+    }
+
+    /**
+     * @property initVelocity
+     * @type {Vec3}
+     */
+    this.initVelocity = new Vec3();
+
+    /**
+     * Linear force on the body
+     * @property force
+     * @type {Vec3}
+     */
+    this.force = new Vec3();
+
+    var mass = typeof(options.mass) === 'number' ? options.mass : 0;
+
+    /**
+     * @property mass
+     * @type {Number}
+     * @default 0
+     */
+    this.mass = mass;
+
+    /**
+     * @property invMass
+     * @type {Number}
+     */
+    this.invMass = mass > 0 ? 1.0 / mass : 0;
+
+    /**
+     * @property material
+     * @type {Material}
+     */
+    this.material = options.material || null;
+
+    /**
+     * @property linearDamping
+     * @type {Number}
+     */
+    this.linearDamping = typeof(options.linearDamping) === 'number' ? options.linearDamping : 0.01;
+
+    /**
+     * One of: Body.DYNAMIC, Body.STATIC and Body.KINEMATIC.
+     * @property type
+     * @type {Number}
+     */
+    this.type = (mass <= 0.0 ? Body.STATIC : Body.DYNAMIC);
+    if(typeof(options.type) === typeof(Body.STATIC)){
+        this.type = options.type;
+    }
+
+    /**
+     * If true, the body will automatically fall to sleep.
+     * @property allowSleep
+     * @type {Boolean}
+     * @default true
+     */
+    this.allowSleep = typeof(options.allowSleep) !== 'undefined' ? options.allowSleep : true;
+
+    /**
+     * Current sleep state.
+     * @property sleepState
+     * @type {Number}
+     */
+    this.sleepState = 0;
+
+    /**
+     * If the speed (the norm of the velocity) is smaller than this value, the body is considered sleepy.
+     * @property sleepSpeedLimit
+     * @type {Number}
+     * @default 0.1
+     */
+    this.sleepSpeedLimit = typeof(options.sleepSpeedLimit) !== 'undefined' ? options.sleepSpeedLimit : 0.1;
+
+    /**
+     * If the body has been sleepy for this sleepTimeLimit seconds, it is considered sleeping.
+     * @property sleepTimeLimit
+     * @type {Number}
+     * @default 1
+     */
+    this.sleepTimeLimit = typeof(options.sleepTimeLimit) !== 'undefined' ? options.sleepTimeLimit : 1;
+
+    this.timeLastSleepy = 0;
+
+    this._wakeUpAfterNarrowphase = false;
+
+
+    /**
+     * Rotational force on the body, around center of mass
+     * @property {Vec3} torque
+     */
+    this.torque = new Vec3();
+
+    /**
+     * Orientation of the body
+     * @property quaternion
+     * @type {Quaternion}
+     */
+    this.quaternion = new Quaternion();
+
+    /**
+     * @property initQuaternion
+     * @type {Quaternion}
+     */
+    this.initQuaternion = new Quaternion();
+
+    /**
+     * @property {Quaternion} previousQuaternion
+     */
+    this.previousQuaternion = new Quaternion();
+
+    /**
+     * Interpolated orientation of the body.
+     * @property {Quaternion} interpolatedQuaternion
+     */
+    this.interpolatedQuaternion = new Quaternion();
+
+    if(options.quaternion){
+        this.quaternion.copy(options.quaternion);
+        this.initQuaternion.copy(options.quaternion);
+        this.previousQuaternion.copy(options.quaternion);
+        this.interpolatedQuaternion.copy(options.quaternion);
+    }
+
+    /**
+     * @property angularVelocity
+     * @type {Vec3}
+     */
+    this.angularVelocity = new Vec3();
+
+    if(options.angularVelocity){
+        this.angularVelocity.copy(options.angularVelocity);
+    }
+
+    /**
+     * @property initAngularVelocity
+     * @type {Vec3}
+     */
+    this.initAngularVelocity = new Vec3();
+
+    /**
+     * @property shapes
+     * @type {array}
+     */
+    this.shapes = [];
+
+    /**
+     * @property shapeOffsets
+     * @type {array}
+     */
+    this.shapeOffsets = [];
+
+    /**
+     * @property shapeOrientations
+     * @type {array}
+     */
+    this.shapeOrientations = [];
+
+    /**
+     * @property inertia
+     * @type {Vec3}
+     */
+    this.inertia = new Vec3();
+
+    /**
+     * @property {Vec3} invInertia
+     */
+    this.invInertia = new Vec3();
+
+    /**
+     * @property {Mat3} invInertiaWorld
+     */
+    this.invInertiaWorld = new Mat3();
+
+    this.invMassSolve = 0;
+
+    /**
+     * @property {Vec3} invInertiaSolve
+     */
+    this.invInertiaSolve = new Vec3();
+
+    /**
+     * @property {Mat3} invInertiaWorldSolve
+     */
+    this.invInertiaWorldSolve = new Mat3();
+
+    /**
+     * Set to true if you don't want the body to rotate. Make sure to run .updateMassProperties() after changing this.
+     * @property {Boolean} fixedRotation
+     * @default false
+     */
+    this.fixedRotation = typeof(options.fixedRotation) !== "undefined" ? options.fixedRotation : false;
+
+    /**
+     * @property {Number} angularDamping
+     */
+    this.angularDamping = typeof(options.angularDamping) !== 'undefined' ? options.angularDamping : 0.01;
+
+    /**
+     * @property {Vec3} linearFactor
+     */
+    this.linearFactor = new Vec3(1,1,1);
+    if(options.linearFactor){
+        this.linearFactor.copy(options.linearFactor);
+    }
+
+    /**
+     * @property {Vec3} angularFactor
+     */
+    this.angularFactor = new Vec3(1,1,1);
+    if(options.angularFactor){
+        this.angularFactor.copy(options.angularFactor);
+    }
+
+    /**
+     * @property aabb
+     * @type {AABB}
+     */
+    this.aabb = new AABB();
+
+    /**
+     * Indicates if the AABB needs to be updated before use.
+     * @property aabbNeedsUpdate
+     * @type {Boolean}
+     */
+    this.aabbNeedsUpdate = true;
+
+    this.wlambda = new Vec3();
+
+    if(options.shape){
+        this.addShape(options.shape);
+    }
+
+    this.updateMassProperties();
+}
+Body.prototype = new EventTarget();
+Body.prototype.constructor = Body;
+
+/**
+ * Dispatched after two bodies collide. This event is dispatched on each
+ * of the two bodies involved in the collision.
+ * @event collide
+ * @param {Body} body The body that was involved in the collision.
+ * @param {ContactEquation} contact The details of the collision.
+ */
+Body.COLLIDE_EVENT_NAME = "collide";
+
+/**
+ * A dynamic body is fully simulated. Can be moved manually by the user, but normally they move according to forces. A dynamic body can collide with all body types. A dynamic body always has finite, non-zero mass.
+ * @static
+ * @property DYNAMIC
+ * @type {Number}
+ */
+Body.DYNAMIC = 1;
+
+/**
+ * A static body does not move during simulation and behaves as if it has infinite mass. Static bodies can be moved manually by setting the position of the body. The velocity of a static body is always zero. Static bodies do not collide with other static or kinematic bodies.
+ * @static
+ * @property STATIC
+ * @type {Number}
+ */
+Body.STATIC = 2;
+
+/**
+ * A kinematic body moves under simulation according to its velocity. They do not respond to forces. They can be moved manually, but normally a kinematic body is moved by setting its velocity. A kinematic body behaves as if it has infinite mass. Kinematic bodies do not collide with other static or kinematic bodies.
+ * @static
+ * @property KINEMATIC
+ * @type {Number}
+ */
+Body.KINEMATIC = 4;
+
+
+
+/**
+ * @static
+ * @property AWAKE
+ * @type {number}
+ */
+Body.AWAKE = 0;
+
+/**
+ * @static
+ * @property SLEEPY
+ * @type {number}
+ */
+Body.SLEEPY = 1;
+
+/**
+ * @static
+ * @property SLEEPING
+ * @type {number}
+ */
+Body.SLEEPING = 2;
+
+Body.idCounter = 0;
+
+/**
+ * Dispatched after a sleeping body has woken up.
+ * @event wakeup
+ */
+Body.wakeupEvent = {
+    type: "wakeup"
+};
+
+/**
+ * Wake the body up.
+ * @method wakeUp
+ */
+Body.prototype.wakeUp = function(){
+    var s = this.sleepState;
+    this.sleepState = 0;
+    this._wakeUpAfterNarrowphase = false;
+    if(s === Body.SLEEPING){
+        this.dispatchEvent(Body.wakeupEvent);
+    }
+};
+
+/**
+ * Force body sleep
+ * @method sleep
+ */
+Body.prototype.sleep = function(){
+    this.sleepState = Body.SLEEPING;
+    this.velocity.set(0,0,0);
+    this.angularVelocity.set(0,0,0);
+    this._wakeUpAfterNarrowphase = false;
+};
+
+/**
+ * Dispatched after a body has gone in to the sleepy state.
+ * @event sleepy
+ */
+Body.sleepyEvent = {
+    type: "sleepy"
+};
+
+/**
+ * Dispatched after a body has fallen asleep.
+ * @event sleep
+ */
+Body.sleepEvent = {
+    type: "sleep"
+};
+
+/**
+ * Called every timestep to update internal sleep timer and change sleep state if needed.
+ * @method sleepTick
+ * @param {Number} time The world time in seconds
+ */
+Body.prototype.sleepTick = function(time){
+    if(this.allowSleep){
+        var sleepState = this.sleepState;
+        var speedSquared = this.velocity.norm2() + this.angularVelocity.norm2();
+        var speedLimitSquared = Math.pow(this.sleepSpeedLimit,2);
+        if(sleepState===Body.AWAKE && speedSquared < speedLimitSquared){
+            this.sleepState = Body.SLEEPY; // Sleepy
+            this.timeLastSleepy = time;
+            this.dispatchEvent(Body.sleepyEvent);
+        } else if(sleepState===Body.SLEEPY && speedSquared > speedLimitSquared){
+            this.wakeUp(); // Wake up
+        } else if(sleepState===Body.SLEEPY && (time - this.timeLastSleepy ) > this.sleepTimeLimit){
+            this.sleep(); // Sleeping
+            this.dispatchEvent(Body.sleepEvent);
+        }
+    }
+};
+
+/**
+ * If the body is sleeping, it should be immovable / have infinite mass during solve. We solve it by having a separate "solve mass".
+ * @method updateSolveMassProperties
+ */
+Body.prototype.updateSolveMassProperties = function(){
+    if(this.sleepState === Body.SLEEPING || this.type === Body.KINEMATIC){
+        this.invMassSolve = 0;
+        this.invInertiaSolve.setZero();
+        this.invInertiaWorldSolve.setZero();
+    } else {
+        this.invMassSolve = this.invMass;
+        this.invInertiaSolve.copy(this.invInertia);
+        this.invInertiaWorldSolve.copy(this.invInertiaWorld);
+    }
+};
+
+/**
+ * Convert a world point to local body frame.
+ * @method pointToLocalFrame
+ * @param  {Vec3} worldPoint
+ * @param  {Vec3} result
+ * @return {Vec3}
+ */
+Body.prototype.pointToLocalFrame = function(worldPoint,result){
+    var result = result || new Vec3();
+    worldPoint.vsub(this.position,result);
+    this.quaternion.conjugate().vmult(result,result);
+    return result;
+};
+
+/**
+ * Convert a world vector to local body frame.
+ * @method vectorToLocalFrame
+ * @param  {Vec3} worldPoint
+ * @param  {Vec3} result
+ * @return {Vec3}
+ */
+Body.prototype.vectorToLocalFrame = function(worldVector, result){
+    var result = result || new Vec3();
+    this.quaternion.conjugate().vmult(worldVector,result);
+    return result;
+};
+
+/**
+ * Convert a local body point to world frame.
+ * @method pointToWorldFrame
+ * @param  {Vec3} localPoint
+ * @param  {Vec3} result
+ * @return {Vec3}
+ */
+Body.prototype.pointToWorldFrame = function(localPoint,result){
+    var result = result || new Vec3();
+    this.quaternion.vmult(localPoint,result);
+    result.vadd(this.position,result);
+    return result;
+};
+
+/**
+ * Convert a local body point to world frame.
+ * @method vectorToWorldFrame
+ * @param  {Vec3} localVector
+ * @param  {Vec3} result
+ * @return {Vec3}
+ */
+Body.prototype.vectorToWorldFrame = function(localVector, result){
+    var result = result || new Vec3();
+    this.quaternion.vmult(localVector, result);
+    return result;
+};
+
+var tmpVec = new Vec3();
+var tmpQuat = new Quaternion();
+
+/**
+ * Add a shape to the body with a local offset and orientation.
+ * @method addShape
+ * @param {Shape} shape
+ * @param {Vec3} [_offset]
+ * @param {Quaternion} [_orientation]
+ * @return {Body} The body object, for chainability.
+ */
+Body.prototype.addShape = function(shape, _offset, _orientation){
+    var offset = new Vec3();
+    var orientation = new Quaternion();
+
+    if(_offset){
+        offset.copy(_offset);
+    }
+    if(_orientation){
+        orientation.copy(_orientation);
+    }
+
+    this.shapes.push(shape);
+    this.shapeOffsets.push(offset);
+    this.shapeOrientations.push(orientation);
+    this.updateMassProperties();
+    this.updateBoundingRadius();
+
+    this.aabbNeedsUpdate = true;
+
+    shape.body = this;
+
+    return this;
+};
+
+/**
+ * Update the bounding radius of the body. Should be done if any of the shapes are changed.
+ * @method updateBoundingRadius
+ */
+Body.prototype.updateBoundingRadius = function(){
+    var shapes = this.shapes,
+        shapeOffsets = this.shapeOffsets,
+        N = shapes.length,
+        radius = 0;
+
+    for(var i=0; i!==N; i++){
+        var shape = shapes[i];
+        shape.updateBoundingSphereRadius();
+        var offset = shapeOffsets[i].norm(),
+            r = shape.boundingSphereRadius;
+        if(offset + r > radius){
+            radius = offset + r;
+        }
+    }
+
+    this.boundingRadius = radius;
+};
+
+var computeAABB_shapeAABB = new AABB();
+
+/**
+ * Updates the .aabb
+ * @method computeAABB
+ * @todo rename to updateAABB()
+ */
+Body.prototype.computeAABB = function(){
+    var shapes = this.shapes,
+        shapeOffsets = this.shapeOffsets,
+        shapeOrientations = this.shapeOrientations,
+        N = shapes.length,
+        offset = tmpVec,
+        orientation = tmpQuat,
+        bodyQuat = this.quaternion,
+        aabb = this.aabb,
+        shapeAABB = computeAABB_shapeAABB;
+
+    for(var i=0; i!==N; i++){
+        var shape = shapes[i];
+
+        // Get shape world position
+        bodyQuat.vmult(shapeOffsets[i], offset);
+        offset.vadd(this.position, offset);
+
+        // Get shape world quaternion
+        shapeOrientations[i].mult(bodyQuat, orientation);
+
+        // Get shape AABB
+        shape.calculateWorldAABB(offset, orientation, shapeAABB.lowerBound, shapeAABB.upperBound);
+
+        if(i === 0){
+            aabb.copy(shapeAABB);
+        } else {
+            aabb.extend(shapeAABB);
+        }
+    }
+
+    this.aabbNeedsUpdate = false;
+};
+
+var uiw_m1 = new Mat3(),
+    uiw_m2 = new Mat3(),
+    uiw_m3 = new Mat3();
+
+/**
+ * Update .inertiaWorld and .invInertiaWorld
+ * @method updateInertiaWorld
+ */
+Body.prototype.updateInertiaWorld = function(force){
+    var I = this.invInertia;
+    if (I.x === I.y && I.y === I.z && !force) {
+        // If inertia M = s*I, where I is identity and s a scalar, then
+        //    R*M*R' = R*(s*I)*R' = s*R*I*R' = s*R*R' = s*I = M
+        // where R is the rotation matrix.
+        // In other words, we don't have to transform the inertia if all
+        // inertia diagonal entries are equal.
+    } else {
+        var m1 = uiw_m1,
+            m2 = uiw_m2,
+            m3 = uiw_m3;
+        m1.setRotationFromQuaternion(this.quaternion);
+        m1.transpose(m2);
+        m1.scale(I,m1);
+        m1.mmult(m2,this.invInertiaWorld);
+    }
+};
+
+/**
+ * Apply force to a world point. This could for example be a point on the Body surface. Applying force this way will add to Body.force and Body.torque.
+ * @method applyForce
+ * @param  {Vec3} force The amount of force to add.
+ * @param  {Vec3} relativePoint A point relative to the center of mass to apply the force on.
+ */
+var Body_applyForce_r = new Vec3();
+var Body_applyForce_rotForce = new Vec3();
+Body.prototype.applyForce = function(force,relativePoint){
+    if(this.type !== Body.DYNAMIC){ // Needed?
+        return;
+    }
+
+    // Compute produced rotational force
+    var rotForce = Body_applyForce_rotForce;
+    relativePoint.cross(force,rotForce);
+
+    // Add linear force
+    this.force.vadd(force,this.force);
+
+    // Add rotational force
+    this.torque.vadd(rotForce,this.torque);
+};
+
+/**
+ * Apply force to a local point in the body.
+ * @method applyLocalForce
+ * @param  {Vec3} force The force vector to apply, defined locally in the body frame.
+ * @param  {Vec3} localPoint A local point in the body to apply the force on.
+ */
+var Body_applyLocalForce_worldForce = new Vec3();
+var Body_applyLocalForce_relativePointWorld = new Vec3();
+Body.prototype.applyLocalForce = function(localForce, localPoint){
+    if(this.type !== Body.DYNAMIC){
+        return;
+    }
+
+    var worldForce = Body_applyLocalForce_worldForce;
+    var relativePointWorld = Body_applyLocalForce_relativePointWorld;
+
+    // Transform the force vector to world space
+    this.vectorToWorldFrame(localForce, worldForce);
+    this.vectorToWorldFrame(localPoint, relativePointWorld);
+
+    this.applyForce(worldForce, relativePointWorld);
+};
+
+/**
+ * Apply impulse to a world point. This could for example be a point on the Body surface. An impulse is a force added to a body during a short period of time (impulse = force * time). Impulses will be added to Body.velocity and Body.angularVelocity.
+ * @method applyImpulse
+ * @param  {Vec3} impulse The amount of impulse to add.
+ * @param  {Vec3} relativePoint A point relative to the center of mass to apply the force on.
+ */
+var Body_applyImpulse_r = new Vec3();
+var Body_applyImpulse_velo = new Vec3();
+var Body_applyImpulse_rotVelo = new Vec3();
+Body.prototype.applyImpulse = function(impulse, relativePoint){
+    if(this.type !== Body.DYNAMIC){
+        return;
+    }
+
+    // Compute point position relative to the body center
+    var r = relativePoint;
+
+    // Compute produced central impulse velocity
+    var velo = Body_applyImpulse_velo;
+    velo.copy(impulse);
+    velo.mult(this.invMass,velo);
+
+    // Add linear impulse
+    this.velocity.vadd(velo, this.velocity);
+
+    // Compute produced rotational impulse velocity
+    var rotVelo = Body_applyImpulse_rotVelo;
+    r.cross(impulse,rotVelo);
+
+    /*
+    rotVelo.x *= this.invInertia.x;
+    rotVelo.y *= this.invInertia.y;
+    rotVelo.z *= this.invInertia.z;
+    */
+    this.invInertiaWorld.vmult(rotVelo,rotVelo);
+
+    // Add rotational Impulse
+    this.angularVelocity.vadd(rotVelo, this.angularVelocity);
+};
+
+/**
+ * Apply locally-defined impulse to a local point in the body.
+ * @method applyLocalImpulse
+ * @param  {Vec3} force The force vector to apply, defined locally in the body frame.
+ * @param  {Vec3} localPoint A local point in the body to apply the force on.
+ */
+var Body_applyLocalImpulse_worldImpulse = new Vec3();
+var Body_applyLocalImpulse_relativePoint = new Vec3();
+Body.prototype.applyLocalImpulse = function(localImpulse, localPoint){
+    if(this.type !== Body.DYNAMIC){
+        return;
+    }
+
+    var worldImpulse = Body_applyLocalImpulse_worldImpulse;
+    var relativePointWorld = Body_applyLocalImpulse_relativePoint;
+
+    // Transform the force vector to world space
+    this.vectorToWorldFrame(localImpulse, worldImpulse);
+    this.vectorToWorldFrame(localPoint, relativePointWorld);
+
+    this.applyImpulse(worldImpulse, relativePointWorld);
+};
+
+var Body_updateMassProperties_halfExtents = new Vec3();
+
+/**
+ * Should be called whenever you change the body shape or mass.
+ * @method updateMassProperties
+ */
+Body.prototype.updateMassProperties = function(){
+    var halfExtents = Body_updateMassProperties_halfExtents;
+
+    this.invMass = this.mass > 0 ? 1.0 / this.mass : 0;
+    var I = this.inertia;
+    var fixed = this.fixedRotation;
+
+    // Approximate with AABB box
+    this.computeAABB();
+    halfExtents.set(
+        (this.aabb.upperBound.x-this.aabb.lowerBound.x) / 2,
+        (this.aabb.upperBound.y-this.aabb.lowerBound.y) / 2,
+        (this.aabb.upperBound.z-this.aabb.lowerBound.z) / 2
+    );
+    Box.calculateInertia(halfExtents, this.mass, I);
+
+    this.invInertia.set(
+        I.x > 0 && !fixed ? 1.0 / I.x : 0,
+        I.y > 0 && !fixed ? 1.0 / I.y : 0,
+        I.z > 0 && !fixed ? 1.0 / I.z : 0
+    );
+    this.updateInertiaWorld(true);
+};
+
+/**
+ * Get world velocity of a point in the body.
+ * @method getVelocityAtWorldPoint
+ * @param  {Vec3} worldPoint
+ * @param  {Vec3} result
+ * @return {Vec3} The result vector.
+ */
+Body.prototype.getVelocityAtWorldPoint = function(worldPoint, result){
+    var r = new Vec3();
+    worldPoint.vsub(this.position, r);
+    this.angularVelocity.cross(r, result);
+    this.velocity.vadd(result, result);
+    return result;
+};
+
+var torque = new Vec3();
+var invI_tau_dt = new Vec3();
+var w = new Quaternion();
+var wq = new Quaternion();
+
+/**
+ * Move the body forward in time.
+ * @param {number} dt Time step
+ * @param {boolean} quatNormalize Set to true to normalize the body quaternion
+ * @param {boolean} quatNormalizeFast If the quaternion should be normalized using "fast" quaternion normalization
+ */
+Body.prototype.integrate = function(dt, quatNormalize, quatNormalizeFast){
+
+    // Save previous position
+    this.previousPosition.copy(this.position);
+    this.previousQuaternion.copy(this.quaternion);
+
+    if(!(this.type === Body.DYNAMIC || this.type === Body.KINEMATIC) || this.sleepState === Body.SLEEPING){ // Only for dynamic
+        return;
+    }
+
+    var velo = this.velocity,
+        angularVelo = this.angularVelocity,
+        pos = this.position,
+        force = this.force,
+        torque = this.torque,
+        quat = this.quaternion,
+        invMass = this.invMass,
+        invInertia = this.invInertiaWorld,
+        linearFactor = this.linearFactor;
+
+    var iMdt = invMass * dt;
+    velo.x += force.x * iMdt * linearFactor.x;
+    velo.y += force.y * iMdt * linearFactor.y;
+    velo.z += force.z * iMdt * linearFactor.z;
+
+    var e = invInertia.elements;
+    var angularFactor = this.angularFactor;
+    var tx = torque.x * angularFactor.x;
+    var ty = torque.y * angularFactor.y;
+    var tz = torque.z * angularFactor.z;
+    angularVelo.x += dt * (e[0] * tx + e[1] * ty + e[2] * tz);
+    angularVelo.y += dt * (e[3] * tx + e[4] * ty + e[5] * tz);
+    angularVelo.z += dt * (e[6] * tx + e[7] * ty + e[8] * tz);
+
+    // Use new velocity  - leap frog
+    pos.x += velo.x * dt;
+    pos.y += velo.y * dt;
+    pos.z += velo.z * dt;
+
+    quat.integrate(this.angularVelocity, dt, this.angularFactor, quat);
+
+    if(quatNormalize){
+        if(quatNormalizeFast){
+            quat.normalizeFast();
+        } else {
+            quat.normalize();
+        }
+    }
+
+    this.aabbNeedsUpdate = true;
+
+    // Update world inertia
+    this.updateInertiaWorld();
+};
+
+},{"../collision/AABB":18,"../material/Material":41,"../math/Mat3":43,"../math/Quaternion":44,"../math/Vec3":46,"../shapes/Box":53,"../shapes/Shape":59,"../utils/EventTarget":65}],48:[function(require,module,exports){
+var Body = require('./Body');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var RaycastResult = require('../collision/RaycastResult');
+var Ray = require('../collision/Ray');
+var WheelInfo = require('../objects/WheelInfo');
+
+module.exports = RaycastVehicle;
+
+/**
+ * Vehicle helper class that casts rays from the wheel positions towards the ground and applies forces.
+ * @class RaycastVehicle
+ * @constructor
+ * @param {object} [options]
+ * @param {Body} [options.chassisBody] The car chassis body.
+ * @param {integer} [options.indexRightAxis] Axis to use for right. x=0, y=1, z=2
+ * @param {integer} [options.indexLeftAxis]
+ * @param {integer} [options.indexUpAxis]
+ */
+function RaycastVehicle(options){
+
+    /**
+     * @property {Body} chassisBody
+     */
+    this.chassisBody = options.chassisBody;
+
+    /**
+     * An array of WheelInfo objects.
+     * @property {array} wheelInfos
+     */
+    this.wheelInfos = [];
+
+    /**
+     * Will be set to true if the car is sliding.
+     * @property {boolean} sliding
+     */
+    this.sliding = false;
+
+    /**
+     * @property {World} world
+     */
+    this.world = null;
+
+    /**
+     * Index of the right axis, 0=x, 1=y, 2=z
+     * @property {integer} indexRightAxis
+     * @default 1
+     */
+    this.indexRightAxis = typeof(options.indexRightAxis) !== 'undefined' ? options.indexRightAxis : 1;
+
+    /**
+     * Index of the forward axis, 0=x, 1=y, 2=z
+     * @property {integer} indexForwardAxis
+     * @default 0
+     */
+    this.indexForwardAxis = typeof(options.indexForwardAxis) !== 'undefined' ? options.indexForwardAxis : 0;
+
+    /**
+     * Index of the up axis, 0=x, 1=y, 2=z
+     * @property {integer} indexUpAxis
+     * @default 2
+     */
+    this.indexUpAxis = typeof(options.indexUpAxis) !== 'undefined' ? options.indexUpAxis : 2;
+}
+
+var tmpVec1 = new Vec3();
+var tmpVec2 = new Vec3();
+var tmpVec3 = new Vec3();
+var tmpVec4 = new Vec3();
+var tmpVec5 = new Vec3();
+var tmpVec6 = new Vec3();
+var tmpRay = new Ray();
+
+/**
+ * Add a wheel. For information about the options, see WheelInfo.
+ * @method addWheel
+ * @param {object} [options]
+ */
+RaycastVehicle.prototype.addWheel = function(options){
+    options = options || {};
+
+    var info = new WheelInfo(options);
+    var index = this.wheelInfos.length;
+    this.wheelInfos.push(info);
+
+    return index;
+};
+
+/**
+ * Set the steering value of a wheel.
+ * @method setSteeringValue
+ * @param {number} value
+ * @param {integer} wheelIndex
+ */
+RaycastVehicle.prototype.setSteeringValue = function(value, wheelIndex){
+    var wheel = this.wheelInfos[wheelIndex];
+    wheel.steering = value;
+};
+
+var torque = new Vec3();
+
+/**
+ * Set the wheel force to apply on one of the wheels each time step
+ * @method applyEngineForce
+ * @param  {number} value
+ * @param  {integer} wheelIndex
+ */
+RaycastVehicle.prototype.applyEngineForce = function(value, wheelIndex){
+    this.wheelInfos[wheelIndex].engineForce = value;
+};
+
+/**
+ * Set the braking force of a wheel
+ * @method setBrake
+ * @param {number} brake
+ * @param {integer} wheelIndex
+ */
+RaycastVehicle.prototype.setBrake = function(brake, wheelIndex){
+    this.wheelInfos[wheelIndex].brake = brake;
+};
+
+/**
+ * Add the vehicle including its constraints to the world.
+ * @method addToWorld
+ * @param {World} world
+ */
+RaycastVehicle.prototype.addToWorld = function(world){
+    var constraints = this.constraints;
+    world.addBody(this.chassisBody);
+    var that = this;
+    this.preStepCallback = function(){
+        that.updateVehicle(world.dt);
+    };
+    world.addEventListener('preStep', this.preStepCallback);
+    this.world = world;
+};
+
+/**
+ * Get one of the wheel axles, world-oriented.
+ * @private
+ * @method getVehicleAxisWorld
+ * @param  {integer} axisIndex
+ * @param  {Vec3} result
+ */
+RaycastVehicle.prototype.getVehicleAxisWorld = function(axisIndex, result){
+    result.set(
+        axisIndex === 0 ? 1 : 0,
+        axisIndex === 1 ? 1 : 0,
+        axisIndex === 2 ? 1 : 0
+    );
+    this.chassisBody.vectorToWorldFrame(result, result);
+};
+
+RaycastVehicle.prototype.updateVehicle = function(timeStep){
+    var wheelInfos = this.wheelInfos;
+    var numWheels = wheelInfos.length;
+    var chassisBody = this.chassisBody;
+
+    for (var i = 0; i < numWheels; i++) {
+        this.updateWheelTransform(i);
+    }
+
+    this.currentVehicleSpeedKmHour = 3.6 * chassisBody.velocity.norm();
+
+    var forwardWorld = new Vec3();
+    this.getVehicleAxisWorld(this.indexForwardAxis, forwardWorld);
+
+    if (forwardWorld.dot(chassisBody.velocity) < 0){
+        this.currentVehicleSpeedKmHour *= -1;
+    }
+
+    // simulate suspension
+    for (var i = 0; i < numWheels; i++) {
+        this.castRay(wheelInfos[i]);
+    }
+
+    this.updateSuspension(timeStep);
+
+    var impulse = new Vec3();
+    var relpos = new Vec3();
+    for (var i = 0; i < numWheels; i++) {
+        //apply suspension force
+        var wheel = wheelInfos[i];
+        var suspensionForce = wheel.suspensionForce;
+        if (suspensionForce > wheel.maxSuspensionForce) {
+            suspensionForce = wheel.maxSuspensionForce;
+        }
+        wheel.raycastResult.hitNormalWorld.scale(suspensionForce * timeStep, impulse);
+
+        wheel.raycastResult.hitPointWorld.vsub(chassisBody.position, relpos);
+        chassisBody.applyImpulse(impulse, relpos);
+    }
+
+    this.updateFriction(timeStep);
+
+    var hitNormalWorldScaledWithProj = new Vec3();
+    var fwd  = new Vec3();
+    var vel = new Vec3();
+    for (i = 0; i < numWheels; i++) {
+        var wheel = wheelInfos[i];
+        //var relpos = new Vec3();
+        //wheel.chassisConnectionPointWorld.vsub(chassisBody.position, relpos);
+        chassisBody.getVelocityAtWorldPoint(wheel.chassisConnectionPointWorld, vel);
+
+        // Hack to get the rotation in the correct direction
+        var m = 1;
+        switch(this.indexUpAxis){
+        case 1:
+            m = -1;
+            break;
+        }
+
+        if (wheel.isInContact) {
+
+            this.getVehicleAxisWorld(this.indexForwardAxis, fwd);
+            var proj = fwd.dot(wheel.raycastResult.hitNormalWorld);
+            wheel.raycastResult.hitNormalWorld.scale(proj, hitNormalWorldScaledWithProj);
+
+            fwd.vsub(hitNormalWorldScaledWithProj, fwd);
+
+            var proj2 = fwd.dot(vel);
+            wheel.deltaRotation = m * proj2 * timeStep / wheel.radius;
+        }
+
+        if((wheel.sliding || !wheel.isInContact) && wheel.engineForce !== 0 && wheel.useCustomSlidingRotationalSpeed){
+            // Apply custom rotation when accelerating and sliding
+            wheel.deltaRotation = (wheel.engineForce > 0 ? 1 : -1) * wheel.customSlidingRotationalSpeed * timeStep;
+        }
+
+        // Lock wheels
+        if(Math.abs(wheel.brake) > Math.abs(wheel.engineForce)){
+            wheel.deltaRotation = 0;
+        }
+
+        wheel.rotation += wheel.deltaRotation; // Use the old value
+        wheel.deltaRotation *= 0.99; // damping of rotation when not in contact
+    }
+};
+
+RaycastVehicle.prototype.updateSuspension = function(deltaTime) {
+    var chassisBody = this.chassisBody;
+    var chassisMass = chassisBody.mass;
+    var wheelInfos = this.wheelInfos;
+    var numWheels = wheelInfos.length;
+
+    for (var w_it = 0; w_it < numWheels; w_it++){
+        var wheel = wheelInfos[w_it];
+
+        if (wheel.isInContact){
+            var force;
+
+            // Spring
+            var susp_length = wheel.suspensionRestLength;
+            var current_length = wheel.suspensionLength;
+            var length_diff = (susp_length - current_length);
+
+            force = wheel.suspensionStiffness * length_diff * wheel.clippedInvContactDotSuspension;
+
+            // Damper
+            var projected_rel_vel = wheel.suspensionRelativeVelocity;
+            var susp_damping;
+            if (projected_rel_vel < 0) {
+                susp_damping = wheel.dampingCompression;
+            } else {
+                susp_damping = wheel.dampingRelaxation;
+            }
+            force -= susp_damping * projected_rel_vel;
+
+            wheel.suspensionForce = force * chassisMass;
+            if (wheel.suspensionForce < 0) {
+                wheel.suspensionForce = 0;
+            }
+        } else {
+            wheel.suspensionForce = 0;
+        }
+    }
+};
+
+/**
+ * Remove the vehicle including its constraints from the world.
+ * @method removeFromWorld
+ * @param {World} world
+ */
+RaycastVehicle.prototype.removeFromWorld = function(world){
+    var constraints = this.constraints;
+    world.remove(this.chassisBody);
+    world.removeEventListener('preStep', this.preStepCallback);
+    this.world = null;
+};
+
+var castRay_rayvector = new Vec3();
+var castRay_target = new Vec3();
+RaycastVehicle.prototype.castRay = function(wheel) {
+    var rayvector = castRay_rayvector;
+    var target = castRay_target;
+
+    this.updateWheelTransformWorld(wheel);
+    var chassisBody = this.chassisBody;
+
+    var depth = -1;
+
+    var raylen = wheel.suspensionRestLength + wheel.radius;
+
+    wheel.directionWorld.scale(raylen, rayvector);
+    var source = wheel.chassisConnectionPointWorld;
+    source.vadd(rayvector, target);
+    var raycastResult = wheel.raycastResult;
+
+    var param = 0;
+
+    raycastResult.reset();
+    // Turn off ray collision with the chassis temporarily
+    var oldState = chassisBody.collisionResponse;
+    chassisBody.collisionResponse = false;
+
+    // Cast ray against world
+    this.world.rayTest(source, target, raycastResult);
+    chassisBody.collisionResponse = oldState;
+
+    var object = raycastResult.body;
+
+    wheel.raycastResult.groundObject = 0;
+
+    if (object) {
+        depth = raycastResult.distance;
+        wheel.raycastResult.hitNormalWorld  = raycastResult.hitNormalWorld;
+        wheel.isInContact = true;
+
+        var hitDistance = raycastResult.distance;
+        wheel.suspensionLength = hitDistance - wheel.radius;
+
+        // clamp on max suspension travel
+        var minSuspensionLength = wheel.suspensionRestLength - wheel.maxSuspensionTravel;
+        var maxSuspensionLength = wheel.suspensionRestLength + wheel.maxSuspensionTravel;
+        if (wheel.suspensionLength < minSuspensionLength) {
+            wheel.suspensionLength = minSuspensionLength;
+        }
+        if (wheel.suspensionLength > maxSuspensionLength) {
+            wheel.suspensionLength = maxSuspensionLength;
+            wheel.raycastResult.reset();
+        }
+
+        var denominator = wheel.raycastResult.hitNormalWorld.dot(wheel.directionWorld);
+
+        var chassis_velocity_at_contactPoint = new Vec3();
+        chassisBody.getVelocityAtWorldPoint(wheel.raycastResult.hitPointWorld, chassis_velocity_at_contactPoint);
+
+        var projVel = wheel.raycastResult.hitNormalWorld.dot( chassis_velocity_at_contactPoint );
+
+        if (denominator >= -0.1) {
+            wheel.suspensionRelativeVelocity = 0;
+            wheel.clippedInvContactDotSuspension = 1 / 0.1;
+        } else {
+            var inv = -1 / denominator;
+            wheel.suspensionRelativeVelocity = projVel * inv;
+            wheel.clippedInvContactDotSuspension = inv;
+        }
+
+    } else {
+
+        //put wheel info as in rest position
+        wheel.suspensionLength = wheel.suspensionRestLength + 0 * wheel.maxSuspensionTravel;
+        wheel.suspensionRelativeVelocity = 0.0;
+        wheel.directionWorld.scale(-1, wheel.raycastResult.hitNormalWorld);
+        wheel.clippedInvContactDotSuspension = 1.0;
+    }
+
+    return depth;
+};
+
+RaycastVehicle.prototype.updateWheelTransformWorld = function(wheel){
+    wheel.isInContact = false;
+    var chassisBody = this.chassisBody;
+    chassisBody.pointToWorldFrame(wheel.chassisConnectionPointLocal, wheel.chassisConnectionPointWorld);
+    chassisBody.vectorToWorldFrame(wheel.directionLocal, wheel.directionWorld);
+    chassisBody.vectorToWorldFrame(wheel.axleLocal, wheel.axleWorld);
+};
+
+
+/**
+ * Update one of the wheel transform.
+ * Note when rendering wheels: during each step, wheel transforms are updated BEFORE the chassis; ie. their position becomes invalid after the step. Thus when you render wheels, you must update wheel transforms before rendering them. See raycastVehicle demo for an example.
+ * @method updateWheelTransform
+ * @param {integer} wheelIndex The wheel index to update.
+ */
+RaycastVehicle.prototype.updateWheelTransform = function(wheelIndex){
+    var up = tmpVec4;
+    var right = tmpVec5;
+    var fwd = tmpVec6;
+
+    var wheel = this.wheelInfos[wheelIndex];
+    this.updateWheelTransformWorld(wheel);
+
+    wheel.directionLocal.scale(-1, up);
+    right.copy(wheel.axleLocal);
+    up.cross(right, fwd);
+    fwd.normalize();
+    right.normalize();
+
+    // Rotate around steering over the wheelAxle
+    var steering = wheel.steering;
+    var steeringOrn = new Quaternion();
+    steeringOrn.setFromAxisAngle(up, steering);
+
+    var rotatingOrn = new Quaternion();
+    rotatingOrn.setFromAxisAngle(right, wheel.rotation);
+
+    // World rotation of the wheel
+    var q = wheel.worldTransform.quaternion;
+    this.chassisBody.quaternion.mult(steeringOrn, q);
+    q.mult(rotatingOrn, q);
+
+    q.normalize();
+
+    // world position of the wheel
+    var p = wheel.worldTransform.position;
+    p.copy(wheel.directionWorld);
+    p.scale(wheel.suspensionLength, p);
+    p.vadd(wheel.chassisConnectionPointWorld, p);
+};
+
+var directions = [
+    new Vec3(1, 0, 0),
+    new Vec3(0, 1, 0),
+    new Vec3(0, 0, 1)
+];
+
+/**
+ * Get the world transform of one of the wheels
+ * @method getWheelTransformWorld
+ * @param  {integer} wheelIndex
+ * @return {Transform}
+ */
+RaycastVehicle.prototype.getWheelTransformWorld = function(wheelIndex) {
+    return this.wheelInfos[wheelIndex].worldTransform;
+};
+
+
+var updateFriction_surfNormalWS_scaled_proj = new Vec3();
+var updateFriction_axle = [];
+var updateFriction_forwardWS = [];
+var sideFrictionStiffness2 = 1;
+RaycastVehicle.prototype.updateFriction = function(timeStep) {
+    var surfNormalWS_scaled_proj = updateFriction_surfNormalWS_scaled_proj;
+
+    //calculate the impulse, so that the wheels don't move sidewards
+    var wheelInfos = this.wheelInfos;
+    var numWheels = wheelInfos.length;
+    var chassisBody = this.chassisBody;
+    var forwardWS = updateFriction_forwardWS;
+    var axle = updateFriction_axle;
+
+    var numWheelsOnGround = 0;
+
+    for (var i = 0; i < numWheels; i++) {
+        var wheel = wheelInfos[i];
+
+        var groundObject = wheel.raycastResult.body;
+        if (groundObject){
+            numWheelsOnGround++;
+        }
+
+        wheel.sideImpulse = 0;
+        wheel.forwardImpulse = 0;
+        if(!forwardWS[i]){
+            forwardWS[i] = new Vec3();
+        }
+        if(!axle[i]){
+            axle[i] = new Vec3();
+        }
+    }
+
+    for (var i = 0; i < numWheels; i++){
+        var wheel = wheelInfos[i];
+
+        var groundObject = wheel.raycastResult.body;
+
+        if (groundObject) {
+            var axlei = axle[i];
+            var wheelTrans = this.getWheelTransformWorld(i);
+
+            // Get world axle
+            wheelTrans.vectorToWorldFrame(directions[this.indexRightAxis], axlei);
+
+            var surfNormalWS = wheel.raycastResult.hitNormalWorld;
+            var proj = axlei.dot(surfNormalWS);
+            surfNormalWS.scale(proj, surfNormalWS_scaled_proj);
+            axlei.vsub(surfNormalWS_scaled_proj, axlei);
+            axlei.normalize();
+
+            surfNormalWS.cross(axlei, forwardWS[i]);
+            forwardWS[i].normalize();
+
+            wheel.sideImpulse = resolveSingleBilateral(
+                chassisBody,
+                wheel.raycastResult.hitPointWorld,
+                groundObject,
+                wheel.raycastResult.hitPointWorld,
+                axlei
+            );
+
+            wheel.sideImpulse *= sideFrictionStiffness2;
+        }
+    }
+
+    var sideFactor = 1;
+    var fwdFactor = 0.5;
+
+    this.sliding = false;
+    for (var i = 0; i < numWheels; i++) {
+        var wheel = wheelInfos[i];
+        var groundObject = wheel.raycastResult.body;
+
+        var rollingFriction = 0;
+
+        wheel.slipInfo = 1;
+        if (groundObject) {
+            var defaultRollingFrictionImpulse = 0;
+            var maxImpulse = wheel.brake ? wheel.brake : defaultRollingFrictionImpulse;
+
+            // btWheelContactPoint contactPt(chassisBody,groundObject,wheelInfraycastInfo.hitPointWorld,forwardWS[wheel],maxImpulse);
+            // rollingFriction = calcRollingFriction(contactPt);
+            rollingFriction = calcRollingFriction(chassisBody, groundObject, wheel.raycastResult.hitPointWorld, forwardWS[i], maxImpulse);
+
+            rollingFriction += wheel.engineForce * timeStep;
+
+            // rollingFriction = 0;
+            var factor = maxImpulse / rollingFriction;
+            wheel.slipInfo *= factor;
+        }
+
+        //switch between active rolling (throttle), braking and non-active rolling friction (nthrottle/break)
+
+        wheel.forwardImpulse = 0;
+        wheel.skidInfo = 1;
+
+        if (groundObject) {
+            wheel.skidInfo = 1;
+
+            var maximp = wheel.suspensionForce * timeStep * wheel.frictionSlip;
+            var maximpSide = maximp;
+
+            var maximpSquared = maximp * maximpSide;
+
+            wheel.forwardImpulse = rollingFriction;//wheelInfo.engineForce* timeStep;
+
+            var x = wheel.forwardImpulse * fwdFactor;
+            var y = wheel.sideImpulse * sideFactor;
+
+            var impulseSquared = x * x + y * y;
+
+            wheel.sliding = false;
+            if (impulseSquared > maximpSquared) {
+                this.sliding = true;
+                wheel.sliding = true;
+
+                var factor = maximp / Math.sqrt(impulseSquared);
+
+                wheel.skidInfo *= factor;
+            }
+        }
+    }
+
+    if (this.sliding) {
+        for (var i = 0; i < numWheels; i++) {
+            var wheel = wheelInfos[i];
+            if (wheel.sideImpulse !== 0) {
+                if (wheel.skidInfo < 1){
+                    wheel.forwardImpulse *= wheel.skidInfo;
+                    wheel.sideImpulse *= wheel.skidInfo;
+                }
+            }
+        }
+    }
+
+    // apply the impulses
+    for (var i = 0; i < numWheels; i++) {
+        var wheel = wheelInfos[i];
+
+        var rel_pos = new Vec3();
+        wheel.raycastResult.hitPointWorld.vsub(chassisBody.position, rel_pos);
+        // cannons applyimpulse is using world coord for the position
+        //rel_pos.copy(wheel.raycastResult.hitPointWorld);
+
+        if (wheel.forwardImpulse !== 0) {
+            var impulse = new Vec3();
+            forwardWS[i].scale(wheel.forwardImpulse, impulse);
+            chassisBody.applyImpulse(impulse, rel_pos);
+        }
+
+        if (wheel.sideImpulse !== 0){
+            var groundObject = wheel.raycastResult.body;
+
+            var rel_pos2 = new Vec3();
+            wheel.raycastResult.hitPointWorld.vsub(groundObject.position, rel_pos2);
+            //rel_pos2.copy(wheel.raycastResult.hitPointWorld);
+            var sideImp = new Vec3();
+            axle[i].scale(wheel.sideImpulse, sideImp);
+
+            // Scale the relative position in the up direction with rollInfluence.
+            // If rollInfluence is 1, the impulse will be applied on the hitPoint (easy to roll over), if it is zero it will be applied in the same plane as the center of mass (not easy to roll over).
+            chassisBody.vectorToLocalFrame(rel_pos, rel_pos);
+            rel_pos['xyz'[this.indexUpAxis]] *= wheel.rollInfluence;
+            chassisBody.vectorToWorldFrame(rel_pos, rel_pos);
+            chassisBody.applyImpulse(sideImp, rel_pos);
+
+            //apply friction impulse on the ground
+            sideImp.scale(-1, sideImp);
+            groundObject.applyImpulse(sideImp, rel_pos2);
+        }
+    }
+};
+
+var calcRollingFriction_vel1 = new Vec3();
+var calcRollingFriction_vel2 = new Vec3();
+var calcRollingFriction_vel = new Vec3();
+
+function calcRollingFriction(body0, body1, frictionPosWorld, frictionDirectionWorld, maxImpulse) {
+    var j1 = 0;
+    var contactPosWorld = frictionPosWorld;
+
+    // var rel_pos1 = new Vec3();
+    // var rel_pos2 = new Vec3();
+    var vel1 = calcRollingFriction_vel1;
+    var vel2 = calcRollingFriction_vel2;
+    var vel = calcRollingFriction_vel;
+    // contactPosWorld.vsub(body0.position, rel_pos1);
+    // contactPosWorld.vsub(body1.position, rel_pos2);
+
+    body0.getVelocityAtWorldPoint(contactPosWorld, vel1);
+    body1.getVelocityAtWorldPoint(contactPosWorld, vel2);
+    vel1.vsub(vel2, vel);
+
+    var vrel = frictionDirectionWorld.dot(vel);
+
+    var denom0 = computeImpulseDenominator(body0, frictionPosWorld, frictionDirectionWorld);
+    var denom1 = computeImpulseDenominator(body1, frictionPosWorld, frictionDirectionWorld);
+    var relaxation = 1;
+    var jacDiagABInv = relaxation / (denom0 + denom1);
+
+    // calculate j that moves us to zero relative velocity
+    j1 = -vrel * jacDiagABInv;
+
+    if (maxImpulse < j1) {
+        j1 = maxImpulse;
+    }
+    if (j1 < -maxImpulse) {
+        j1 = -maxImpulse;
+    }
+
+    return j1;
+}
+
+var computeImpulseDenominator_r0 = new Vec3();
+var computeImpulseDenominator_c0 = new Vec3();
+var computeImpulseDenominator_vec = new Vec3();
+var computeImpulseDenominator_m = new Vec3();
+function computeImpulseDenominator(body, pos, normal) {
+    var r0 = computeImpulseDenominator_r0;
+    var c0 = computeImpulseDenominator_c0;
+    var vec = computeImpulseDenominator_vec;
+    var m = computeImpulseDenominator_m;
+
+    pos.vsub(body.position, r0);
+    r0.cross(normal, c0);
+    body.invInertiaWorld.vmult(c0, m);
+    m.cross(r0, vec);
+
+    return body.invMass + normal.dot(vec);
+}
+
+
+var resolveSingleBilateral_vel1 = new Vec3();
+var resolveSingleBilateral_vel2 = new Vec3();
+var resolveSingleBilateral_vel = new Vec3();
+
+//bilateral constraint between two dynamic objects
+function resolveSingleBilateral(body1, pos1, body2, pos2, normal, impulse){
+    var normalLenSqr = normal.norm2();
+    if (normalLenSqr > 1.1){
+        return 0; // no impulse
+    }
+    // var rel_pos1 = new Vec3();
+    // var rel_pos2 = new Vec3();
+    // pos1.vsub(body1.position, rel_pos1);
+    // pos2.vsub(body2.position, rel_pos2);
+
+    var vel1 = resolveSingleBilateral_vel1;
+    var vel2 = resolveSingleBilateral_vel2;
+    var vel = resolveSingleBilateral_vel;
+    body1.getVelocityAtWorldPoint(pos1, vel1);
+    body2.getVelocityAtWorldPoint(pos2, vel2);
+
+    vel1.vsub(vel2, vel);
+
+    var rel_vel = normal.dot(vel);
+
+    var contactDamping = 0.2;
+    var massTerm = 1 / (body1.invMass + body2.invMass);
+    var impulse = - contactDamping * rel_vel * massTerm;
+
+    return impulse;
+}
+},{"../collision/Ray":25,"../collision/RaycastResult":26,"../math/Quaternion":44,"../math/Vec3":46,"../objects/WheelInfo":52,"./Body":47}],49:[function(require,module,exports){
+var Body = require('./Body');
+var Sphere = require('../shapes/Sphere');
+var Box = require('../shapes/Box');
+var Vec3 = require('../math/Vec3');
+var HingeConstraint = require('../constraints/HingeConstraint');
+
+module.exports = RigidVehicle;
+
+/**
+ * Simple vehicle helper class with spherical rigid body wheels.
+ * @class RigidVehicle
+ * @constructor
+ * @param {Body} [options.chassisBody]
+ */
+function RigidVehicle(options){
+    this.wheelBodies = [];
+
+    /**
+     * @property coordinateSystem
+     * @type {Vec3}
+     */
+    this.coordinateSystem = typeof(options.coordinateSystem)==='undefined' ? new Vec3(1, 2, 3) : options.coordinateSystem.clone();
+
+    /**
+     * @property {Body} chassisBody
+     */
+    this.chassisBody = options.chassisBody;
+
+    if(!this.chassisBody){
+        // No chassis body given. Create it!
+        var chassisShape = new Box(new Vec3(5, 2, 0.5));
+        this.chassisBody = new Body(1, chassisShape);
+    }
+
+    /**
+     * @property constraints
+     * @type {Array}
+     */
+    this.constraints = [];
+
+    this.wheelAxes = [];
+    this.wheelForces = [];
+}
+
+/**
+ * Add a wheel
+ * @method addWheel
+ * @param {object} options
+ * @param {boolean} [options.isFrontWheel]
+ * @param {Vec3} [options.position] Position of the wheel, locally in the chassis body.
+ * @param {Vec3} [options.direction] Slide direction of the wheel along the suspension.
+ * @param {Vec3} [options.axis] Axis of rotation of the wheel, locally defined in the chassis.
+ * @param {Body} [options.body] The wheel body.
+ */
+RigidVehicle.prototype.addWheel = function(options){
+    options = options || {};
+    var wheelBody = options.body;
+    if(!wheelBody){
+        wheelBody =  new Body(1, new Sphere(1.2));
+    }
+    this.wheelBodies.push(wheelBody);
+    this.wheelForces.push(0);
+
+    // Position constrain wheels
+    var zero = new Vec3();
+    var position = typeof(options.position) !== 'undefined' ? options.position.clone() : new Vec3();
+
+    // Set position locally to the chassis
+    var worldPosition = new Vec3();
+    this.chassisBody.pointToWorldFrame(position, worldPosition);
+    wheelBody.position.set(worldPosition.x, worldPosition.y, worldPosition.z);
+
+    // Constrain wheel
+    var axis = typeof(options.axis) !== 'undefined' ? options.axis.clone() : new Vec3(0, 1, 0);
+    this.wheelAxes.push(axis);
+
+    var hingeConstraint = new HingeConstraint(this.chassisBody, wheelBody, {
+        pivotA: position,
+        axisA: axis,
+        pivotB: Vec3.ZERO,
+        axisB: axis,
+        collideConnected: false
+    });
+    this.constraints.push(hingeConstraint);
+
+    return this.wheelBodies.length - 1;
+};
+
+/**
+ * Set the steering value of a wheel.
+ * @method setSteeringValue
+ * @param {number} value
+ * @param {integer} wheelIndex
+ * @todo check coordinateSystem
+ */
+RigidVehicle.prototype.setSteeringValue = function(value, wheelIndex){
+    // Set angle of the hinge axis
+    var axis = this.wheelAxes[wheelIndex];
+
+    var c = Math.cos(value),
+        s = Math.sin(value),
+        x = axis.x,
+        y = axis.y;
+    this.constraints[wheelIndex].axisA.set(
+        c*x -s*y,
+        s*x +c*y,
+        0
+    );
+};
+
+/**
+ * Set the target rotational speed of the hinge constraint.
+ * @method setMotorSpeed
+ * @param {number} value
+ * @param {integer} wheelIndex
+ */
+RigidVehicle.prototype.setMotorSpeed = function(value, wheelIndex){
+    var hingeConstraint = this.constraints[wheelIndex];
+    hingeConstraint.enableMotor();
+    hingeConstraint.motorTargetVelocity = value;
+};
+
+/**
+ * Set the target rotational speed of the hinge constraint.
+ * @method disableMotor
+ * @param {number} value
+ * @param {integer} wheelIndex
+ */
+RigidVehicle.prototype.disableMotor = function(wheelIndex){
+    var hingeConstraint = this.constraints[wheelIndex];
+    hingeConstraint.disableMotor();
+};
+
+var torque = new Vec3();
+
+/**
+ * Set the wheel force to apply on one of the wheels each time step
+ * @method setWheelForce
+ * @param  {number} value
+ * @param  {integer} wheelIndex
+ */
+RigidVehicle.prototype.setWheelForce = function(value, wheelIndex){
+    this.wheelForces[wheelIndex] = value;
+};
+
+/**
+ * Apply a torque on one of the wheels.
+ * @method applyWheelForce
+ * @param  {number} value
+ * @param  {integer} wheelIndex
+ */
+RigidVehicle.prototype.applyWheelForce = function(value, wheelIndex){
+    var axis = this.wheelAxes[wheelIndex];
+    var wheelBody = this.wheelBodies[wheelIndex];
+    var bodyTorque = wheelBody.torque;
+
+    axis.scale(value, torque);
+    wheelBody.vectorToWorldFrame(torque, torque);
+    bodyTorque.vadd(torque, bodyTorque);
+};
+
+/**
+ * Add the vehicle including its constraints to the world.
+ * @method addToWorld
+ * @param {World} world
+ */
+RigidVehicle.prototype.addToWorld = function(world){
+    var constraints = this.constraints;
+    var bodies = this.wheelBodies.concat([this.chassisBody]);
+
+    for (var i = 0; i < bodies.length; i++) {
+        world.addBody(bodies[i]);
+    }
+
+    for (var i = 0; i < constraints.length; i++) {
+        world.addConstraint(constraints[i]);
+    }
+
+    world.addEventListener('preStep', this._update.bind(this));
+};
+
+RigidVehicle.prototype._update = function(){
+    var wheelForces = this.wheelForces;
+    for (var i = 0; i < wheelForces.length; i++) {
+        this.applyWheelForce(wheelForces[i], i);
+    }
+};
+
+/**
+ * Remove the vehicle including its constraints from the world.
+ * @method removeFromWorld
+ * @param {World} world
+ */
+RigidVehicle.prototype.removeFromWorld = function(world){
+    var constraints = this.constraints;
+    var bodies = this.wheelBodies.concat([this.chassisBody]);
+
+    for (var i = 0; i < bodies.length; i++) {
+        world.remove(bodies[i]);
+    }
+
+    for (var i = 0; i < constraints.length; i++) {
+        world.removeConstraint(constraints[i]);
+    }
+};
+
+var worldAxis = new Vec3();
+
+/**
+ * Get current rotational velocity of a wheel
+ * @method getWheelSpeed
+ * @param {integer} wheelIndex
+ */
+RigidVehicle.prototype.getWheelSpeed = function(wheelIndex){
+    var axis = this.wheelAxes[wheelIndex];
+    var wheelBody = this.wheelBodies[wheelIndex];
+    var w = wheelBody.angularVelocity;
+    this.chassisBody.vectorToWorldFrame(axis, worldAxis);
+    return w.dot(worldAxis);
+};
+
+},{"../constraints/HingeConstraint":31,"../math/Vec3":46,"../shapes/Box":53,"../shapes/Sphere":60,"./Body":47}],50:[function(require,module,exports){
+module.exports = SPHSystem;
+
+var Shape = require('../shapes/Shape');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Particle = require('../shapes/Particle');
+var Body = require('../objects/Body');
+var Material = require('../material/Material');
+
+/**
+ * Smoothed-particle hydrodynamics system
+ * @class SPHSystem
+ * @constructor
+ */
+function SPHSystem(){
+    this.particles = [];
+	
+    /**
+     * Density of the system (kg/m3).
+     * @property {number} density
+     */
+    this.density = 1;
+	
+    /**
+     * Distance below which two particles are considered to be neighbors.
+     * It should be adjusted so there are about 15-20 neighbor particles within this radius.
+     * @property {number} smoothingRadius
+     */
+    this.smoothingRadius = 1;
+    this.speedOfSound = 1;
+	
+    /**
+     * Viscosity of the system.
+     * @property {number} viscosity
+     */
+    this.viscosity = 0.01;
+    this.eps = 0.000001;
+
+    // Stuff Computed per particle
+    this.pressures = [];
+    this.densities = [];
+    this.neighbors = [];
+}
+
+/**
+ * Add a particle to the system.
+ * @method add
+ * @param {Body} particle
+ */
+SPHSystem.prototype.add = function(particle){
+    this.particles.push(particle);
+    if(this.neighbors.length < this.particles.length){
+        this.neighbors.push([]);
+    }
+};
+
+/**
+ * Remove a particle from the system.
+ * @method remove
+ * @param {Body} particle
+ */
+SPHSystem.prototype.remove = function(particle){
+    var idx = this.particles.indexOf(particle);
+    if(idx !== -1){
+        this.particles.splice(idx,1);
+        if(this.neighbors.length > this.particles.length){
+            this.neighbors.pop();
+        }
+    }
+};
+
+/**
+ * Get neighbors within smoothing volume, save in the array neighbors
+ * @method getNeighbors
+ * @param {Body} particle
+ * @param {Array} neighbors
+ */
+var SPHSystem_getNeighbors_dist = new Vec3();
+SPHSystem.prototype.getNeighbors = function(particle,neighbors){
+    var N = this.particles.length,
+        id = particle.id,
+        R2 = this.smoothingRadius * this.smoothingRadius,
+        dist = SPHSystem_getNeighbors_dist;
+    for(var i=0; i!==N; i++){
+        var p = this.particles[i];
+        p.position.vsub(particle.position,dist);
+        if(id!==p.id && dist.norm2() < R2){
+            neighbors.push(p);
+        }
+    }
+};
+
+// Temp vectors for calculation
+var SPHSystem_update_dist = new Vec3(),
+    SPHSystem_update_a_pressure = new Vec3(),
+    SPHSystem_update_a_visc = new Vec3(),
+    SPHSystem_update_gradW = new Vec3(),
+    SPHSystem_update_r_vec = new Vec3(),
+    SPHSystem_update_u = new Vec3(); // Relative velocity
+SPHSystem.prototype.update = function(){
+    var N = this.particles.length,
+        dist = SPHSystem_update_dist,
+        cs = this.speedOfSound,
+        eps = this.eps;
+
+    for(var i=0; i!==N; i++){
+        var p = this.particles[i]; // Current particle
+        var neighbors = this.neighbors[i];
+
+        // Get neighbors
+        neighbors.length = 0;
+        this.getNeighbors(p,neighbors);
+        neighbors.push(this.particles[i]); // Add current too
+        var numNeighbors = neighbors.length;
+
+        // Accumulate density for the particle
+        var sum = 0.0;
+        for(var j=0; j!==numNeighbors; j++){
+
+            //printf("Current particle has position %f %f %f\n",objects[id].pos.x(),objects[id].pos.y(),objects[id].pos.z());
+            p.position.vsub(neighbors[j].position, dist);
+            var len = dist.norm();
+
+            var weight = this.w(len);
+            sum += neighbors[j].mass * weight;
+        }
+
+        // Save
+        this.densities[i] = sum;
+        this.pressures[i] = cs * cs * (this.densities[i] - this.density);
+    }
+
+    // Add forces
+
+    // Sum to these accelerations
+    var a_pressure= SPHSystem_update_a_pressure;
+    var a_visc =    SPHSystem_update_a_visc;
+    var gradW =     SPHSystem_update_gradW;
+    var r_vec =     SPHSystem_update_r_vec;
+    var u =         SPHSystem_update_u;
+
+    for(var i=0; i!==N; i++){
+
+        var particle = this.particles[i];
+
+        a_pressure.set(0,0,0);
+        a_visc.set(0,0,0);
+
+        // Init vars
+        var Pij;
+        var nabla;
+        var Vij;
+
+        // Sum up for all other neighbors
+        var neighbors = this.neighbors[i];
+        var numNeighbors = neighbors.length;
+
+        //printf("Neighbors: ");
+        for(var j=0; j!==numNeighbors; j++){
+
+            var neighbor = neighbors[j];
+            //printf("%d ",nj);
+
+            // Get r once for all..
+            particle.position.vsub(neighbor.position,r_vec);
+            var r = r_vec.norm();
+
+            // Pressure contribution
+            Pij = -neighbor.mass * (this.pressures[i] / (this.densities[i]*this.densities[i] + eps) + this.pressures[j] / (this.densities[j]*this.densities[j] + eps));
+            this.gradw(r_vec, gradW);
+            // Add to pressure acceleration
+            gradW.mult(Pij , gradW);
+            a_pressure.vadd(gradW, a_pressure);
+
+            // Viscosity contribution
+            neighbor.velocity.vsub(particle.velocity, u);
+            u.mult( 1.0 / (0.0001+this.densities[i] * this.densities[j]) * this.viscosity * neighbor.mass , u );
+            nabla = this.nablaw(r);
+            u.mult(nabla,u);
+            // Add to viscosity acceleration
+            a_visc.vadd( u, a_visc );
+        }
+
+        // Calculate force
+        a_visc.mult(particle.mass, a_visc);
+        a_pressure.mult(particle.mass, a_pressure);
+
+        // Add force to particles
+        particle.force.vadd(a_visc, particle.force);
+        particle.force.vadd(a_pressure, particle.force);
+    }
+};
+
+// Calculate the weight using the W(r) weightfunction
+SPHSystem.prototype.w = function(r){
+    // 315
+    var h = this.smoothingRadius;
+    return 315.0/(64.0*Math.PI*Math.pow(h,9)) * Math.pow(h*h-r*r,3);
+};
+
+// calculate gradient of the weight function
+SPHSystem.prototype.gradw = function(rVec,resultVec){
+    var r = rVec.norm(),
+        h = this.smoothingRadius;
+    rVec.mult(945.0/(32.0*Math.PI*Math.pow(h,9)) * Math.pow((h*h-r*r),2) , resultVec);
+};
+
+// Calculate nabla(W)
+SPHSystem.prototype.nablaw = function(r){
+    var h = this.smoothingRadius;
+    var nabla = 945.0/(32.0*Math.PI*Math.pow(h,9)) * (h*h-r*r)*(7*r*r - 3*h*h);
+    return nabla;
+};
+
+},{"../material/Material":41,"../math/Quaternion":44,"../math/Vec3":46,"../objects/Body":47,"../shapes/Particle":57,"../shapes/Shape":59}],51:[function(require,module,exports){
+var Vec3 = require('../math/Vec3');
+
+module.exports = Spring;
+
+/**
+ * A spring, connecting two bodies.
+ *
+ * @class Spring
+ * @constructor
+ * @param {Body} bodyA
+ * @param {Body} bodyB
+ * @param {Object} [options]
+ * @param {number} [options.restLength]   A number > 0. Default: 1
+ * @param {number} [options.stiffness]    A number >= 0. Default: 100
+ * @param {number} [options.damping]      A number >= 0. Default: 1
+ * @param {Vec3}  [options.worldAnchorA] Where to hook the spring to body A, in world coordinates.
+ * @param {Vec3}  [options.worldAnchorB]
+ * @param {Vec3}  [options.localAnchorA] Where to hook the spring to body A, in local body coordinates.
+ * @param {Vec3}  [options.localAnchorB]
+ */
+function Spring(bodyA,bodyB,options){
+    options = options || {};
+
+    /**
+     * Rest length of the spring.
+     * @property restLength
+     * @type {number}
+     */
+    this.restLength = typeof(options.restLength) === "number" ? options.restLength : 1;
+
+    /**
+     * Stiffness of the spring.
+     * @property stiffness
+     * @type {number}
+     */
+    this.stiffness = options.stiffness || 100;
+
+    /**
+     * Damping of the spring.
+     * @property damping
+     * @type {number}
+     */
+    this.damping = options.damping || 1;
+
+    /**
+     * First connected body.
+     * @property bodyA
+     * @type {Body}
+     */
+    this.bodyA = bodyA;
+
+    /**
+     * Second connected body.
+     * @property bodyB
+     * @type {Body}
+     */
+    this.bodyB = bodyB;
+
+    /**
+     * Anchor for bodyA in local bodyA coordinates.
+     * @property localAnchorA
+     * @type {Vec3}
+     */
+    this.localAnchorA = new Vec3();
+
+    /**
+     * Anchor for bodyB in local bodyB coordinates.
+     * @property localAnchorB
+     * @type {Vec3}
+     */
+    this.localAnchorB = new Vec3();
+
+    if(options.localAnchorA){
+        this.localAnchorA.copy(options.localAnchorA);
+    }
+    if(options.localAnchorB){
+        this.localAnchorB.copy(options.localAnchorB);
+    }
+    if(options.worldAnchorA){
+        this.setWorldAnchorA(options.worldAnchorA);
+    }
+    if(options.worldAnchorB){
+        this.setWorldAnchorB(options.worldAnchorB);
+    }
+}
+
+/**
+ * Set the anchor point on body A, using world coordinates.
+ * @method setWorldAnchorA
+ * @param {Vec3} worldAnchorA
+ */
+Spring.prototype.setWorldAnchorA = function(worldAnchorA){
+    this.bodyA.pointToLocalFrame(worldAnchorA,this.localAnchorA);
+};
+
+/**
+ * Set the anchor point on body B, using world coordinates.
+ * @method setWorldAnchorB
+ * @param {Vec3} worldAnchorB
+ */
+Spring.prototype.setWorldAnchorB = function(worldAnchorB){
+    this.bodyB.pointToLocalFrame(worldAnchorB,this.localAnchorB);
+};
+
+/**
+ * Get the anchor point on body A, in world coordinates.
+ * @method getWorldAnchorA
+ * @param {Vec3} result The vector to store the result in.
+ */
+Spring.prototype.getWorldAnchorA = function(result){
+    this.bodyA.pointToWorldFrame(this.localAnchorA,result);
+};
+
+/**
+ * Get the anchor point on body B, in world coordinates.
+ * @method getWorldAnchorB
+ * @param {Vec3} result The vector to store the result in.
+ */
+Spring.prototype.getWorldAnchorB = function(result){
+    this.bodyB.pointToWorldFrame(this.localAnchorB,result);
+};
+
+var applyForce_r =              new Vec3(),
+    applyForce_r_unit =         new Vec3(),
+    applyForce_u =              new Vec3(),
+    applyForce_f =              new Vec3(),
+    applyForce_worldAnchorA =   new Vec3(),
+    applyForce_worldAnchorB =   new Vec3(),
+    applyForce_ri =             new Vec3(),
+    applyForce_rj =             new Vec3(),
+    applyForce_ri_x_f =         new Vec3(),
+    applyForce_rj_x_f =         new Vec3(),
+    applyForce_tmp =            new Vec3();
+
+/**
+ * Apply the spring force to the connected bodies.
+ * @method applyForce
+ */
+Spring.prototype.applyForce = function(){
+    var k = this.stiffness,
+        d = this.damping,
+        l = this.restLength,
+        bodyA = this.bodyA,
+        bodyB = this.bodyB,
+        r = applyForce_r,
+        r_unit = applyForce_r_unit,
+        u = applyForce_u,
+        f = applyForce_f,
+        tmp = applyForce_tmp;
+
+    var worldAnchorA = applyForce_worldAnchorA,
+        worldAnchorB = applyForce_worldAnchorB,
+        ri = applyForce_ri,
+        rj = applyForce_rj,
+        ri_x_f = applyForce_ri_x_f,
+        rj_x_f = applyForce_rj_x_f;
+
+    // Get world anchors
+    this.getWorldAnchorA(worldAnchorA);
+    this.getWorldAnchorB(worldAnchorB);
+
+    // Get offset points
+    worldAnchorA.vsub(bodyA.position,ri);
+    worldAnchorB.vsub(bodyB.position,rj);
+
+    // Compute distance vector between world anchor points
+    worldAnchorB.vsub(worldAnchorA,r);
+    var rlen = r.norm();
+    r_unit.copy(r);
+    r_unit.normalize();
+
+    // Compute relative velocity of the anchor points, u
+    bodyB.velocity.vsub(bodyA.velocity,u);
+    // Add rotational velocity
+
+    bodyB.angularVelocity.cross(rj,tmp);
+    u.vadd(tmp,u);
+    bodyA.angularVelocity.cross(ri,tmp);
+    u.vsub(tmp,u);
+
+    // F = - k * ( x - L ) - D * ( u )
+    r_unit.mult(-k*(rlen-l) - d*u.dot(r_unit), f);
+
+    // Add forces to bodies
+    bodyA.force.vsub(f,bodyA.force);
+    bodyB.force.vadd(f,bodyB.force);
+
+    // Angular force
+    ri.cross(f,ri_x_f);
+    rj.cross(f,rj_x_f);
+    bodyA.torque.vsub(ri_x_f,bodyA.torque);
+    bodyB.torque.vadd(rj_x_f,bodyB.torque);
+};
+
+},{"../math/Vec3":46}],52:[function(require,module,exports){
+var Vec3 = require('../math/Vec3');
+var Transform = require('../math/Transform');
+var RaycastResult = require('../collision/RaycastResult');
+var Utils = require('../utils/Utils');
+
+module.exports = WheelInfo;
+
+/**
+ * @class WheelInfo
+ * @constructor
+ * @param {Object} [options]
+ *
+ * @param {Vec3} [options.chassisConnectionPointLocal]
+ * @param {Vec3} [options.chassisConnectionPointWorld]
+ * @param {Vec3} [options.directionLocal]
+ * @param {Vec3} [options.directionWorld]
+ * @param {Vec3} [options.axleLocal]
+ * @param {Vec3} [options.axleWorld]
+ * @param {number} [options.suspensionRestLength=1]
+ * @param {number} [options.suspensionMaxLength=2]
+ * @param {number} [options.radius=1]
+ * @param {number} [options.suspensionStiffness=100]
+ * @param {number} [options.dampingCompression=10]
+ * @param {number} [options.dampingRelaxation=10]
+ * @param {number} [options.frictionSlip=10000]
+ * @param {number} [options.steering=0]
+ * @param {number} [options.rotation=0]
+ * @param {number} [options.deltaRotation=0]
+ * @param {number} [options.rollInfluence=0.01]
+ * @param {number} [options.maxSuspensionForce]
+ * @param {boolean} [options.isFrontWheel=true]
+ * @param {number} [options.clippedInvContactDotSuspension=1]
+ * @param {number} [options.suspensionRelativeVelocity=0]
+ * @param {number} [options.suspensionForce=0]
+ * @param {number} [options.skidInfo=0]
+ * @param {number} [options.suspensionLength=0]
+ * @param {number} [options.maxSuspensionTravel=1]
+ * @param {boolean} [options.useCustomSlidingRotationalSpeed=false]
+ * @param {number} [options.customSlidingRotationalSpeed=-0.1]
+ */
+function WheelInfo(options){
+    options = Utils.defaults(options, {
+        chassisConnectionPointLocal: new Vec3(),
+        chassisConnectionPointWorld: new Vec3(),
+        directionLocal: new Vec3(),
+        directionWorld: new Vec3(),
+        axleLocal: new Vec3(),
+        axleWorld: new Vec3(),
+        suspensionRestLength: 1,
+        suspensionMaxLength: 2,
+        radius: 1,
+        suspensionStiffness: 100,
+        dampingCompression: 10,
+        dampingRelaxation: 10,
+        frictionSlip: 10000,
+        steering: 0,
+        rotation: 0,
+        deltaRotation: 0,
+        rollInfluence: 0.01,
+        maxSuspensionForce: Number.MAX_VALUE,
+        isFrontWheel: true,
+        clippedInvContactDotSuspension: 1,
+        suspensionRelativeVelocity: 0,
+        suspensionForce: 0,
+        skidInfo: 0,
+        suspensionLength: 0,
+        maxSuspensionTravel: 1,
+        useCustomSlidingRotationalSpeed: false,
+        customSlidingRotationalSpeed: -0.1
+    });
+
+    /**
+     * Max travel distance of the suspension, in meters.
+     * @property {number} maxSuspensionTravel
+     */
+    this.maxSuspensionTravel = options.maxSuspensionTravel;
+
+    /**
+     * Speed to apply to the wheel rotation when the wheel is sliding.
+     * @property {number} customSlidingRotationalSpeed
+     */
+    this.customSlidingRotationalSpeed = options.customSlidingRotationalSpeed;
+
+    /**
+     * If the customSlidingRotationalSpeed should be used.
+     * @property {Boolean} useCustomSlidingRotationalSpeed
+     */
+    this.useCustomSlidingRotationalSpeed = options.useCustomSlidingRotationalSpeed;
+
+    /**
+     * @property {Boolean} sliding
+     */
+    this.sliding = false;
+
+    /**
+     * Connection point, defined locally in the chassis body frame.
+     * @property {Vec3} chassisConnectionPointLocal
+     */
+    this.chassisConnectionPointLocal = options.chassisConnectionPointLocal.clone();
+
+    /**
+     * @property {Vec3} chassisConnectionPointWorld
+     */
+    this.chassisConnectionPointWorld = options.chassisConnectionPointWorld.clone();
+
+    /**
+     * @property {Vec3} directionLocal
+     */
+    this.directionLocal = options.directionLocal.clone();
+
+    /**
+     * @property {Vec3} directionWorld
+     */
+    this.directionWorld = options.directionWorld.clone();
+
+    /**
+     * @property {Vec3} axleLocal
+     */
+    this.axleLocal = options.axleLocal.clone();
+
+    /**
+     * @property {Vec3} axleWorld
+     */
+    this.axleWorld = options.axleWorld.clone();
+
+    /**
+     * @property {number} suspensionRestLength
+     */
+    this.suspensionRestLength = options.suspensionRestLength;
+
+    /**
+     * @property {number} suspensionMaxLength
+     */
+    this.suspensionMaxLength = options.suspensionMaxLength;
+
+    /**
+     * @property {number} radius
+     */
+    this.radius = options.radius;
+
+    /**
+     * @property {number} suspensionStiffness
+     */
+    this.suspensionStiffness = options.suspensionStiffness;
+
+    /**
+     * @property {number} dampingCompression
+     */
+    this.dampingCompression = options.dampingCompression;
+
+    /**
+     * @property {number} dampingRelaxation
+     */
+    this.dampingRelaxation = options.dampingRelaxation;
+
+    /**
+     * @property {number} frictionSlip
+     */
+    this.frictionSlip = options.frictionSlip;
+
+    /**
+     * @property {number} steering
+     */
+    this.steering = 0;
+
+    /**
+     * Rotation value, in radians.
+     * @property {number} rotation
+     */
+    this.rotation = 0;
+
+    /**
+     * @property {number} deltaRotation
+     */
+    this.deltaRotation = 0;
+
+    /**
+     * @property {number} rollInfluence
+     */
+    this.rollInfluence = options.rollInfluence;
+
+    /**
+     * @property {number} maxSuspensionForce
+     */
+    this.maxSuspensionForce = options.maxSuspensionForce;
+
+    /**
+     * @property {number} engineForce
+     */
+    this.engineForce = 0;
+
+    /**
+     * @property {number} brake
+     */
+    this.brake = 0;
+
+    /**
+     * @property {number} isFrontWheel
+     */
+    this.isFrontWheel = options.isFrontWheel;
+
+    /**
+     * @property {number} clippedInvContactDotSuspension
+     */
+    this.clippedInvContactDotSuspension = 1;
+
+    /**
+     * @property {number} suspensionRelativeVelocity
+     */
+    this.suspensionRelativeVelocity = 0;
+
+    /**
+     * @property {number} suspensionForce
+     */
+    this.suspensionForce = 0;
+
+    /**
+     * @property {number} skidInfo
+     */
+    this.skidInfo = 0;
+
+    /**
+     * @property {number} suspensionLength
+     */
+    this.suspensionLength = 0;
+
+    /**
+     * @property {number} sideImpulse
+     */
+    this.sideImpulse = 0;
+
+    /**
+     * @property {number} forwardImpulse
+     */
+    this.forwardImpulse = 0;
+
+    /**
+     * The result from raycasting
+     * @property {RaycastResult} raycastResult
+     */
+    this.raycastResult = new RaycastResult();
+
+    /**
+     * Wheel world transform
+     * @property {Transform} worldTransform
+     */
+    this.worldTransform = new Transform();
+
+    /**
+     * @property {boolean} isInContact
+     */
+    this.isInContact = false;
+}
+
+var chassis_velocity_at_contactPoint = new Vec3();
+var relpos = new Vec3();
+var chassis_velocity_at_contactPoint = new Vec3();
+WheelInfo.prototype.updateWheel = function(chassis){
+    var raycastResult = this.raycastResult;
+
+    if (this.isInContact){
+        var project= raycastResult.hitNormalWorld.dot(raycastResult.directionWorld);
+        raycastResult.hitPointWorld.vsub(chassis.position, relpos);
+        chassis.getVelocityAtWorldPoint(relpos, chassis_velocity_at_contactPoint);
+        var projVel = raycastResult.hitNormalWorld.dot( chassis_velocity_at_contactPoint );
+        if (project >= -0.1) {
+            this.suspensionRelativeVelocity = 0.0;
+            this.clippedInvContactDotSuspension = 1.0 / 0.1;
+        } else {
+            var inv = -1 / project;
+            this.suspensionRelativeVelocity = projVel * inv;
+            this.clippedInvContactDotSuspension = inv;
+        }
+
+    } else {
+        // Not in contact : position wheel in a nice (rest length) position
+        raycastResult.suspensionLength = this.suspensionRestLength;
+        this.suspensionRelativeVelocity = 0.0;
+        raycastResult.directionWorld.scale(-1, raycastResult.hitNormalWorld);
+        this.clippedInvContactDotSuspension = 1.0;
+    }
+};
+},{"../collision/RaycastResult":26,"../math/Transform":45,"../math/Vec3":46,"../utils/Utils":69}],53:[function(require,module,exports){
+module.exports = Box;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+var ConvexPolyhedron = require('./ConvexPolyhedron');
+
+/**
+ * A 3d box shape.
+ * @class Box
+ * @constructor
+ * @param {Vec3} halfExtents
+ * @author schteppe
+ * @extends Shape
+ */
+function Box(halfExtents){
+    Shape.call(this);
+
+    this.type = Shape.types.BOX;
+
+    /**
+     * @property halfExtents
+     * @type {Vec3}
+     */
+    this.halfExtents = halfExtents;
+
+    /**
+     * Used by the contact generator to make contacts with other convex polyhedra for example
+     * @property convexPolyhedronRepresentation
+     * @type {ConvexPolyhedron}
+     */
+    this.convexPolyhedronRepresentation = null;
+
+    this.updateConvexPolyhedronRepresentation();
+    this.updateBoundingSphereRadius();
+}
+Box.prototype = new Shape();
+Box.prototype.constructor = Box;
+
+/**
+ * Updates the local convex polyhedron representation used for some collisions.
+ * @method updateConvexPolyhedronRepresentation
+ */
+Box.prototype.updateConvexPolyhedronRepresentation = function(){
+    var sx = this.halfExtents.x;
+    var sy = this.halfExtents.y;
+    var sz = this.halfExtents.z;
+    var V = Vec3;
+
+    var vertices = [
+        new V(-sx,-sy,-sz),
+        new V( sx,-sy,-sz),
+        new V( sx, sy,-sz),
+        new V(-sx, sy,-sz),
+        new V(-sx,-sy, sz),
+        new V( sx,-sy, sz),
+        new V( sx, sy, sz),
+        new V(-sx, sy, sz)
+    ];
+
+    var indices = [
+        [3,2,1,0], // -z
+        [4,5,6,7], // +z
+        [5,4,0,1], // -y
+        [2,3,7,6], // +y
+        [0,4,7,3], // -x
+        [1,2,6,5], // +x
+    ];
+
+    var axes = [
+        new V(0, 0, 1),
+        new V(0, 1, 0),
+        new V(1, 0, 0)
+    ];
+
+    var h = new ConvexPolyhedron(vertices, indices);
+    this.convexPolyhedronRepresentation = h;
+    h.material = this.material;
+};
+
+/**
+ * @method calculateLocalInertia
+ * @param  {Number} mass
+ * @param  {Vec3} target
+ * @return {Vec3}
+ */
+Box.prototype.calculateLocalInertia = function(mass,target){
+    target = target || new Vec3();
+    Box.calculateInertia(this.halfExtents, mass, target);
+    return target;
+};
+
+Box.calculateInertia = function(halfExtents,mass,target){
+    var e = halfExtents;
+    target.x = 1.0 / 12.0 * mass * (   2*e.y*2*e.y + 2*e.z*2*e.z );
+    target.y = 1.0 / 12.0 * mass * (   2*e.x*2*e.x + 2*e.z*2*e.z );
+    target.z = 1.0 / 12.0 * mass * (   2*e.y*2*e.y + 2*e.x*2*e.x );
+};
+
+/**
+ * Get the box 6 side normals
+ * @method getSideNormals
+ * @param {array}      sixTargetVectors An array of 6 vectors, to store the resulting side normals in.
+ * @param {Quaternion} quat             Orientation to apply to the normal vectors. If not provided, the vectors will be in respect to the local frame.
+ * @return {array}
+ */
+Box.prototype.getSideNormals = function(sixTargetVectors,quat){
+    var sides = sixTargetVectors;
+    var ex = this.halfExtents;
+    sides[0].set(  ex.x,     0,     0);
+    sides[1].set(     0,  ex.y,     0);
+    sides[2].set(     0,     0,  ex.z);
+    sides[3].set( -ex.x,     0,     0);
+    sides[4].set(     0, -ex.y,     0);
+    sides[5].set(     0,     0, -ex.z);
+
+    if(quat!==undefined){
+        for(var i=0; i!==sides.length; i++){
+            quat.vmult(sides[i],sides[i]);
+        }
+    }
+
+    return sides;
+};
+
+Box.prototype.volume = function(){
+    return 8.0 * this.halfExtents.x * this.halfExtents.y * this.halfExtents.z;
+};
+
+Box.prototype.updateBoundingSphereRadius = function(){
+    this.boundingSphereRadius = this.halfExtents.norm();
+};
+
+var worldCornerTempPos = new Vec3();
+var worldCornerTempNeg = new Vec3();
+Box.prototype.forEachWorldCorner = function(pos,quat,callback){
+
+    var e = this.halfExtents;
+    var corners = [[  e.x,  e.y,  e.z],
+                   [ -e.x,  e.y,  e.z],
+                   [ -e.x, -e.y,  e.z],
+                   [ -e.x, -e.y, -e.z],
+                   [  e.x, -e.y, -e.z],
+                   [  e.x,  e.y, -e.z],
+                   [ -e.x,  e.y, -e.z],
+                   [  e.x, -e.y,  e.z]];
+    for(var i=0; i<corners.length; i++){
+        worldCornerTempPos.set(corners[i][0],corners[i][1],corners[i][2]);
+        quat.vmult(worldCornerTempPos,worldCornerTempPos);
+        pos.vadd(worldCornerTempPos,worldCornerTempPos);
+        callback(worldCornerTempPos.x,
+                 worldCornerTempPos.y,
+                 worldCornerTempPos.z);
+    }
+};
+
+var worldCornersTemp = [
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3(),
+    new Vec3()
+];
+Box.prototype.calculateWorldAABB = function(pos,quat,min,max){
+
+    var e = this.halfExtents;
+    worldCornersTemp[0].set(e.x, e.y, e.z);
+    worldCornersTemp[1].set(-e.x,  e.y, e.z);
+    worldCornersTemp[2].set(-e.x, -e.y, e.z);
+    worldCornersTemp[3].set(-e.x, -e.y, -e.z);
+    worldCornersTemp[4].set(e.x, -e.y, -e.z);
+    worldCornersTemp[5].set(e.x,  e.y, -e.z);
+    worldCornersTemp[6].set(-e.x,  e.y, -e.z);
+    worldCornersTemp[7].set(e.x, -e.y,  e.z);
+
+    var wc = worldCornersTemp[0];
+    quat.vmult(wc, wc);
+    pos.vadd(wc, wc);
+    max.copy(wc);
+    min.copy(wc);
+    for(var i=1; i<8; i++){
+        var wc = worldCornersTemp[i];
+        quat.vmult(wc, wc);
+        pos.vadd(wc, wc);
+        var x = wc.x;
+        var y = wc.y;
+        var z = wc.z;
+        if(x > max.x){
+            max.x = x;
+        }
+        if(y > max.y){
+            max.y = y;
+        }
+        if(z > max.z){
+            max.z = z;
+        }
+
+        if(x < min.x){
+            min.x = x;
+        }
+        if(y < min.y){
+            min.y = y;
+        }
+        if(z < min.z){
+            min.z = z;
+        }
+    }
+
+    // Get each axis max
+    // min.set(Infinity,Infinity,Infinity);
+    // max.set(-Infinity,-Infinity,-Infinity);
+    // this.forEachWorldCorner(pos,quat,function(x,y,z){
+    //     if(x > max.x){
+    //         max.x = x;
+    //     }
+    //     if(y > max.y){
+    //         max.y = y;
+    //     }
+    //     if(z > max.z){
+    //         max.z = z;
+    //     }
+
+    //     if(x < min.x){
+    //         min.x = x;
+    //     }
+    //     if(y < min.y){
+    //         min.y = y;
+    //     }
+    //     if(z < min.z){
+    //         min.z = z;
+    //     }
+    // });
+};
+
+},{"../math/Vec3":46,"./ConvexPolyhedron":54,"./Shape":59}],54:[function(require,module,exports){
+module.exports = ConvexPolyhedron;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Transform = require('../math/Transform');
+
+/**
+ * A set of polygons describing a convex shape.
+ * @class ConvexPolyhedron
+ * @constructor
+ * @extends Shape
+ * @description The shape MUST be convex for the code to work properly. No polygons may be coplanar (contained
+ * in the same 3D plane), instead these should be merged into one polygon.
+ *
+ * @param {array} points An array of Vec3's
+ * @param {array} faces Array of integer arrays, describing which vertices that is included in each face.
+ *
+ * @author qiao / https://github.com/qiao (original author, see https://github.com/qiao/three.js/commit/85026f0c769e4000148a67d45a9e9b9c5108836f)
+ * @author schteppe / https://github.com/schteppe
+ * @see http://www.altdevblogaday.com/2011/05/13/contact-generation-between-3d-convex-meshes/
+ * @see http://bullet.googlecode.com/svn/trunk/src/BulletCollision/NarrowPhaseCollision/btPolyhedralContactClipping.cpp
+ *
+ * @todo Move the clipping functions to ContactGenerator?
+ * @todo Automatically merge coplanar polygons in constructor.
+ */
+function ConvexPolyhedron(points, faces, uniqueAxes) {
+    var that = this;
+    Shape.call(this);
+    this.type = Shape.types.CONVEXPOLYHEDRON;
+
+    /**
+     * Array of Vec3
+     * @property vertices
+     * @type {Array}
+     */
+    this.vertices = points||[];
+
+    this.worldVertices = []; // World transformed version of .vertices
+    this.worldVerticesNeedsUpdate = true;
+
+    /**
+     * Array of integer arrays, indicating which vertices each face consists of
+     * @property faces
+     * @type {Array}
+     */
+    this.faces = faces||[];
+
+    /**
+     * Array of Vec3
+     * @property faceNormals
+     * @type {Array}
+     */
+    this.faceNormals = [];
+    this.computeNormals();
+
+    this.worldFaceNormalsNeedsUpdate = true;
+    this.worldFaceNormals = []; // World transformed version of .faceNormals
+
+    /**
+     * Array of Vec3
+     * @property uniqueEdges
+     * @type {Array}
+     */
+    this.uniqueEdges = [];
+
+    /**
+     * If given, these locally defined, normalized axes are the only ones being checked when doing separating axis check.
+     * @property {Array} uniqueAxes
+     */
+    this.uniqueAxes = uniqueAxes ? uniqueAxes.slice() : null;
+
+    this.computeEdges();
+    this.updateBoundingSphereRadius();
+}
+ConvexPolyhedron.prototype = new Shape();
+ConvexPolyhedron.prototype.constructor = ConvexPolyhedron;
+
+var computeEdges_tmpEdge = new Vec3();
+/**
+ * Computes uniqueEdges
+ * @method computeEdges
+ */
+ConvexPolyhedron.prototype.computeEdges = function(){
+    var faces = this.faces;
+    var vertices = this.vertices;
+    var nv = vertices.length;
+    var edges = this.uniqueEdges;
+
+    edges.length = 0;
+
+    var edge = computeEdges_tmpEdge;
+
+    for(var i=0; i !== faces.length; i++){
+        var face = faces[i];
+        var numVertices = face.length;
+        for(var j = 0; j !== numVertices; j++){
+            var k = ( j+1 ) % numVertices;
+            vertices[face[j]].vsub(vertices[face[k]], edge);
+            edge.normalize();
+            var found = false;
+            for(var p=0; p !== edges.length; p++){
+                if (edges[p].almostEquals(edge) || edges[p].almostEquals(edge)){
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found){
+                edges.push(edge.clone());
+            }
+        }
+    }
+};
+
+/**
+ * Compute the normals of the faces. Will reuse existing Vec3 objects in the .faceNormals array if they exist.
+ * @method computeNormals
+ */
+ConvexPolyhedron.prototype.computeNormals = function(){
+    this.faceNormals.length = this.faces.length;
+
+    // Generate normals
+    for(var i=0; i<this.faces.length; i++){
+
+        // Check so all vertices exists for this face
+        for(var j=0; j<this.faces[i].length; j++){
+            if(!this.vertices[this.faces[i][j]]){
+                throw new Error("Vertex "+this.faces[i][j]+" not found!");
+            }
+        }
+
+        var n = this.faceNormals[i] || new Vec3();
+        this.getFaceNormal(i,n);
+        n.negate(n);
+        this.faceNormals[i] = n;
+        var vertex = this.vertices[this.faces[i][0]];
+        if(n.dot(vertex) < 0){
+            console.error(".faceNormals[" + i + "] = Vec3("+n.toString()+") looks like it points into the shape? The vertices follow. Make sure they are ordered CCW around the normal, using the right hand rule.");
+            for(var j=0; j<this.faces[i].length; j++){
+                console.warn(".vertices["+this.faces[i][j]+"] = Vec3("+this.vertices[this.faces[i][j]].toString()+")");
+            }
+        }
+    }
+};
+
+/**
+ * Get face normal given 3 vertices
+ * @static
+ * @method getFaceNormal
+ * @param {Vec3} va
+ * @param {Vec3} vb
+ * @param {Vec3} vc
+ * @param {Vec3} target
+ */
+var cb = new Vec3();
+var ab = new Vec3();
+ConvexPolyhedron.computeNormal = function ( va, vb, vc, target ) {
+    vb.vsub(va,ab);
+    vc.vsub(vb,cb);
+    cb.cross(ab,target);
+    if ( !target.isZero() ) {
+        target.normalize();
+    }
+};
+
+/**
+ * Compute the normal of a face from its vertices
+ * @method getFaceNormal
+ * @param  {Number} i
+ * @param  {Vec3} target
+ */
+ConvexPolyhedron.prototype.getFaceNormal = function(i,target){
+    var f = this.faces[i];
+    var va = this.vertices[f[0]];
+    var vb = this.vertices[f[1]];
+    var vc = this.vertices[f[2]];
+    return ConvexPolyhedron.computeNormal(va,vb,vc,target);
+};
+
+/**
+ * @method clipAgainstHull
+ * @param {Vec3} posA
+ * @param {Quaternion} quatA
+ * @param {ConvexPolyhedron} hullB
+ * @param {Vec3} posB
+ * @param {Quaternion} quatB
+ * @param {Vec3} separatingNormal
+ * @param {Number} minDist Clamp distance
+ * @param {Number} maxDist
+ * @param {array} result The an array of contact point objects, see clipFaceAgainstHull
+ * @see http://bullet.googlecode.com/svn/trunk/src/BulletCollision/NarrowPhaseCollision/btPolyhedralContactClipping.cpp
+ */
+var cah_WorldNormal = new Vec3();
+ConvexPolyhedron.prototype.clipAgainstHull = function(posA,quatA,hullB,posB,quatB,separatingNormal,minDist,maxDist,result){
+    var WorldNormal = cah_WorldNormal;
+    var hullA = this;
+    var curMaxDist = maxDist;
+    var closestFaceB = -1;
+    var dmax = -Number.MAX_VALUE;
+    for(var face=0; face < hullB.faces.length; face++){
+        WorldNormal.copy(hullB.faceNormals[face]);
+        quatB.vmult(WorldNormal,WorldNormal);
+        //posB.vadd(WorldNormal,WorldNormal);
+        var d = WorldNormal.dot(separatingNormal);
+        if (d > dmax){
+            dmax = d;
+            closestFaceB = face;
+        }
+    }
+    var worldVertsB1 = [];
+    var polyB = hullB.faces[closestFaceB];
+    var numVertices = polyB.length;
+    for(var e0=0; e0<numVertices; e0++){
+        var b = hullB.vertices[polyB[e0]];
+        var worldb = new Vec3();
+        worldb.copy(b);
+        quatB.vmult(worldb,worldb);
+        posB.vadd(worldb,worldb);
+        worldVertsB1.push(worldb);
+    }
+
+    if (closestFaceB>=0){
+        this.clipFaceAgainstHull(separatingNormal,
+                                 posA,
+                                 quatA,
+                                 worldVertsB1,
+                                 minDist,
+                                 maxDist,
+                                 result);
+    }
+};
+
+/**
+ * Find the separating axis between this hull and another
+ * @method findSeparatingAxis
+ * @param {ConvexPolyhedron} hullB
+ * @param {Vec3} posA
+ * @param {Quaternion} quatA
+ * @param {Vec3} posB
+ * @param {Quaternion} quatB
+ * @param {Vec3} target The target vector to save the axis in
+ * @return {bool} Returns false if a separation is found, else true
+ */
+var fsa_faceANormalWS3 = new Vec3(),
+    fsa_Worldnormal1 = new Vec3(),
+    fsa_deltaC = new Vec3(),
+    fsa_worldEdge0 = new Vec3(),
+    fsa_worldEdge1 = new Vec3(),
+    fsa_Cross = new Vec3();
+ConvexPolyhedron.prototype.findSeparatingAxis = function(hullB,posA,quatA,posB,quatB,target, faceListA, faceListB){
+    var faceANormalWS3 = fsa_faceANormalWS3,
+        Worldnormal1 = fsa_Worldnormal1,
+        deltaC = fsa_deltaC,
+        worldEdge0 = fsa_worldEdge0,
+        worldEdge1 = fsa_worldEdge1,
+        Cross = fsa_Cross;
+
+    var dmin = Number.MAX_VALUE;
+    var hullA = this;
+    var curPlaneTests=0;
+
+    if(!hullA.uniqueAxes){
+
+        var numFacesA = faceListA ? faceListA.length : hullA.faces.length;
+
+        // Test face normals from hullA
+        for(var i=0; i<numFacesA; i++){
+            var fi = faceListA ? faceListA[i] : i;
+
+            // Get world face normal
+            faceANormalWS3.copy(hullA.faceNormals[fi]);
+            quatA.vmult(faceANormalWS3,faceANormalWS3);
+
+            var d = hullA.testSepAxis(faceANormalWS3, hullB, posA, quatA, posB, quatB);
+            if(d===false){
+                return false;
+            }
+
+            if(d<dmin){
+                dmin = d;
+                target.copy(faceANormalWS3);
+            }
+        }
+
+    } else {
+
+        // Test unique axes
+        for(var i = 0; i !== hullA.uniqueAxes.length; i++){
+
+            // Get world axis
+            quatA.vmult(hullA.uniqueAxes[i],faceANormalWS3);
+
+            var d = hullA.testSepAxis(faceANormalWS3, hullB, posA, quatA, posB, quatB);
+            if(d===false){
+                return false;
+            }
+
+            if(d<dmin){
+                dmin = d;
+                target.copy(faceANormalWS3);
+            }
+        }
+    }
+
+    if(!hullB.uniqueAxes){
+
+        // Test face normals from hullB
+        var numFacesB = faceListB ? faceListB.length : hullB.faces.length;
+        for(var i=0;i<numFacesB;i++){
+
+            var fi = faceListB ? faceListB[i] : i;
+
+            Worldnormal1.copy(hullB.faceNormals[fi]);
+            quatB.vmult(Worldnormal1,Worldnormal1);
+            curPlaneTests++;
+            var d = hullA.testSepAxis(Worldnormal1, hullB,posA,quatA,posB,quatB);
+            if(d===false){
+                return false;
+            }
+
+            if(d<dmin){
+                dmin = d;
+                target.copy(Worldnormal1);
+            }
+        }
+    } else {
+
+        // Test unique axes in B
+        for(var i = 0; i !== hullB.uniqueAxes.length; i++){
+            quatB.vmult(hullB.uniqueAxes[i],Worldnormal1);
+
+            curPlaneTests++;
+            var d = hullA.testSepAxis(Worldnormal1, hullB,posA,quatA,posB,quatB);
+            if(d===false){
+                return false;
+            }
+
+            if(d<dmin){
+                dmin = d;
+                target.copy(Worldnormal1);
+            }
+        }
+    }
+
+    // Test edges
+    for(var e0=0; e0 !== hullA.uniqueEdges.length; e0++){
+
+        // Get world edge
+        quatA.vmult(hullA.uniqueEdges[e0],worldEdge0);
+
+        for(var e1=0; e1 !== hullB.uniqueEdges.length; e1++){
+
+            // Get world edge 2
+            quatB.vmult(hullB.uniqueEdges[e1], worldEdge1);
+            worldEdge0.cross(worldEdge1,Cross);
+
+            if(!Cross.almostZero()){
+                Cross.normalize();
+                var dist = hullA.testSepAxis(Cross, hullB, posA, quatA, posB, quatB);
+                if(dist === false){
+                    return false;
+                }
+                if(dist < dmin){
+                    dmin = dist;
+                    target.copy(Cross);
+                }
+            }
+        }
+    }
+
+    posB.vsub(posA,deltaC);
+    if((deltaC.dot(target))>0.0){
+        target.negate(target);
+    }
+
+    return true;
+};
+
+var maxminA=[], maxminB=[];
+
+/**
+ * Test separating axis against two hulls. Both hulls are projected onto the axis and the overlap size is returned if there is one.
+ * @method testSepAxis
+ * @param {Vec3} axis
+ * @param {ConvexPolyhedron} hullB
+ * @param {Vec3} posA
+ * @param {Quaternion} quatA
+ * @param {Vec3} posB
+ * @param {Quaternion} quatB
+ * @return {number} The overlap depth, or FALSE if no penetration.
+ */
+ConvexPolyhedron.prototype.testSepAxis = function(axis, hullB, posA, quatA, posB, quatB){
+    var hullA=this;
+    ConvexPolyhedron.project(hullA, axis, posA, quatA, maxminA);
+    ConvexPolyhedron.project(hullB, axis, posB, quatB, maxminB);
+    var maxA = maxminA[0];
+    var minA = maxminA[1];
+    var maxB = maxminB[0];
+    var minB = maxminB[1];
+    if(maxA<minB || maxB<minA){
+        return false; // Separated
+    }
+    var d0 = maxA - minB;
+    var d1 = maxB - minA;
+    var depth = d0<d1 ? d0:d1;
+    return depth;
+};
+
+var cli_aabbmin = new Vec3(),
+    cli_aabbmax = new Vec3();
+
+/**
+ * @method calculateLocalInertia
+ * @param  {Number} mass
+ * @param  {Vec3} target
+ */
+ConvexPolyhedron.prototype.calculateLocalInertia = function(mass,target){
+    // Approximate with box inertia
+    // Exact inertia calculation is overkill, but see http://geometrictools.com/Documentation/PolyhedralMassProperties.pdf for the correct way to do it
+    this.computeLocalAABB(cli_aabbmin,cli_aabbmax);
+    var x = cli_aabbmax.x - cli_aabbmin.x,
+        y = cli_aabbmax.y - cli_aabbmin.y,
+        z = cli_aabbmax.z - cli_aabbmin.z;
+    target.x = 1.0 / 12.0 * mass * ( 2*y*2*y + 2*z*2*z );
+    target.y = 1.0 / 12.0 * mass * ( 2*x*2*x + 2*z*2*z );
+    target.z = 1.0 / 12.0 * mass * ( 2*y*2*y + 2*x*2*x );
+};
+
+/**
+ * @method getPlaneConstantOfFace
+ * @param  {Number} face_i Index of the face
+ * @return {Number}
+ */
+ConvexPolyhedron.prototype.getPlaneConstantOfFace = function(face_i){
+    var f = this.faces[face_i];
+    var n = this.faceNormals[face_i];
+    var v = this.vertices[f[0]];
+    var c = -n.dot(v);
+    return c;
+};
+
+/**
+ * Clip a face against a hull.
+ * @method clipFaceAgainstHull
+ * @param {Vec3} separatingNormal
+ * @param {Vec3} posA
+ * @param {Quaternion} quatA
+ * @param {Array} worldVertsB1 An array of Vec3 with vertices in the world frame.
+ * @param {Number} minDist Distance clamping
+ * @param {Number} maxDist
+ * @param Array result Array to store resulting contact points in. Will be objects with properties: point, depth, normal. These are represented in world coordinates.
+ */
+var cfah_faceANormalWS = new Vec3(),
+    cfah_edge0 = new Vec3(),
+    cfah_WorldEdge0 = new Vec3(),
+    cfah_worldPlaneAnormal1 = new Vec3(),
+    cfah_planeNormalWS1 = new Vec3(),
+    cfah_worldA1 = new Vec3(),
+    cfah_localPlaneNormal = new Vec3(),
+    cfah_planeNormalWS = new Vec3();
+ConvexPolyhedron.prototype.clipFaceAgainstHull = function(separatingNormal, posA, quatA, worldVertsB1, minDist, maxDist,result){
+    var faceANormalWS = cfah_faceANormalWS,
+        edge0 = cfah_edge0,
+        WorldEdge0 = cfah_WorldEdge0,
+        worldPlaneAnormal1 = cfah_worldPlaneAnormal1,
+        planeNormalWS1 = cfah_planeNormalWS1,
+        worldA1 = cfah_worldA1,
+        localPlaneNormal = cfah_localPlaneNormal,
+        planeNormalWS = cfah_planeNormalWS;
+
+    var hullA = this;
+    var worldVertsB2 = [];
+    var pVtxIn = worldVertsB1;
+    var pVtxOut = worldVertsB2;
+    // Find the face with normal closest to the separating axis
+    var closestFaceA = -1;
+    var dmin = Number.MAX_VALUE;
+    for(var face=0; face<hullA.faces.length; face++){
+        faceANormalWS.copy(hullA.faceNormals[face]);
+        quatA.vmult(faceANormalWS,faceANormalWS);
+        //posA.vadd(faceANormalWS,faceANormalWS);
+        var d = faceANormalWS.dot(separatingNormal);
+        if (d < dmin){
+            dmin = d;
+            closestFaceA = face;
+        }
+    }
+    if (closestFaceA < 0){
+        // console.log("--- did not find any closest face... ---");
+        return;
+    }
+    //console.log("closest A: ",closestFaceA);
+    // Get the face and construct connected faces
+    var polyA = hullA.faces[closestFaceA];
+    polyA.connectedFaces = [];
+    for(var i=0; i<hullA.faces.length; i++){
+        for(var j=0; j<hullA.faces[i].length; j++){
+            if(polyA.indexOf(hullA.faces[i][j])!==-1 /* Sharing a vertex*/ && i!==closestFaceA /* Not the one we are looking for connections from */ && polyA.connectedFaces.indexOf(i)===-1 /* Not already added */ ){
+                polyA.connectedFaces.push(i);
+            }
+        }
+    }
+    // Clip the polygon to the back of the planes of all faces of hull A, that are adjacent to the witness face
+    var numContacts = pVtxIn.length;
+    var numVerticesA = polyA.length;
+    var res = [];
+    for(var e0=0; e0<numVerticesA; e0++){
+        var a = hullA.vertices[polyA[e0]];
+        var b = hullA.vertices[polyA[(e0+1)%numVerticesA]];
+        a.vsub(b,edge0);
+        WorldEdge0.copy(edge0);
+        quatA.vmult(WorldEdge0,WorldEdge0);
+        posA.vadd(WorldEdge0,WorldEdge0);
+        worldPlaneAnormal1.copy(this.faceNormals[closestFaceA]);//transA.getBasis()* btVector3(polyA.m_plane[0],polyA.m_plane[1],polyA.m_plane[2]);
+        quatA.vmult(worldPlaneAnormal1,worldPlaneAnormal1);
+        posA.vadd(worldPlaneAnormal1,worldPlaneAnormal1);
+        WorldEdge0.cross(worldPlaneAnormal1,planeNormalWS1);
+        planeNormalWS1.negate(planeNormalWS1);
+        worldA1.copy(a);
+        quatA.vmult(worldA1,worldA1);
+        posA.vadd(worldA1,worldA1);
+        var planeEqWS1 = -worldA1.dot(planeNormalWS1);
+        var planeEqWS;
+        if(true){
+            var otherFace = polyA.connectedFaces[e0];
+            localPlaneNormal.copy(this.faceNormals[otherFace]);
+            var localPlaneEq = this.getPlaneConstantOfFace(otherFace);
+
+            planeNormalWS.copy(localPlaneNormal);
+            quatA.vmult(planeNormalWS,planeNormalWS);
+            //posA.vadd(planeNormalWS,planeNormalWS);
+            var planeEqWS = localPlaneEq - planeNormalWS.dot(posA);
+        } else  {
+            planeNormalWS.copy(planeNormalWS1);
+            planeEqWS = planeEqWS1;
+        }
+
+        // Clip face against our constructed plane
+        this.clipFaceAgainstPlane(pVtxIn, pVtxOut, planeNormalWS, planeEqWS);
+
+        // Throw away all clipped points, but save the reamining until next clip
+        while(pVtxIn.length){
+            pVtxIn.shift();
+        }
+        while(pVtxOut.length){
+            pVtxIn.push(pVtxOut.shift());
+        }
+    }
+
+    //console.log("Resulting points after clip:",pVtxIn);
+
+    // only keep contact points that are behind the witness face
+    localPlaneNormal.copy(this.faceNormals[closestFaceA]);
+
+    var localPlaneEq = this.getPlaneConstantOfFace(closestFaceA);
+    planeNormalWS.copy(localPlaneNormal);
+    quatA.vmult(planeNormalWS,planeNormalWS);
+
+    var planeEqWS = localPlaneEq - planeNormalWS.dot(posA);
+    for (var i=0; i<pVtxIn.length; i++){
+        var depth = planeNormalWS.dot(pVtxIn[i]) + planeEqWS; //???
+        /*console.log("depth calc from normal=",planeNormalWS.toString()," and constant "+planeEqWS+" and vertex ",pVtxIn[i].toString()," gives "+depth);*/
+        if (depth <=minDist){
+            console.log("clamped: depth="+depth+" to minDist="+(minDist+""));
+            depth = minDist;
+        }
+
+        if (depth <=maxDist){
+            var point = pVtxIn[i];
+            if(depth<=0){
+                /*console.log("Got contact point ",point.toString(),
+                  ", depth=",depth,
+                  "contact normal=",separatingNormal.toString(),
+                  "plane",planeNormalWS.toString(),
+                  "planeConstant",planeEqWS);*/
+                var p = {
+                    point:point,
+                    normal:planeNormalWS,
+                    depth: depth,
+                };
+                result.push(p);
+            }
+        }
+    }
+};
+
+/**
+ * Clip a face in a hull against the back of a plane.
+ * @method clipFaceAgainstPlane
+ * @param {Array} inVertices
+ * @param {Array} outVertices
+ * @param {Vec3} planeNormal
+ * @param {Number} planeConstant The constant in the mathematical plane equation
+ */
+ConvexPolyhedron.prototype.clipFaceAgainstPlane = function(inVertices,outVertices, planeNormal, planeConstant){
+    var n_dot_first, n_dot_last;
+    var numVerts = inVertices.length;
+
+    if(numVerts < 2){
+        return outVertices;
+    }
+
+    var firstVertex = inVertices[inVertices.length-1],
+        lastVertex =   inVertices[0];
+
+    n_dot_first = planeNormal.dot(firstVertex) + planeConstant;
+
+    for(var vi = 0; vi < numVerts; vi++){
+        lastVertex = inVertices[vi];
+        n_dot_last = planeNormal.dot(lastVertex) + planeConstant;
+        if(n_dot_first < 0){
+            if(n_dot_last < 0){
+                // Start < 0, end < 0, so output lastVertex
+                var newv = new Vec3();
+                newv.copy(lastVertex);
+                outVertices.push(newv);
+            } else {
+                // Start < 0, end >= 0, so output intersection
+                var newv = new Vec3();
+                firstVertex.lerp(lastVertex,
+                                 n_dot_first / (n_dot_first - n_dot_last),
+                                 newv);
+                outVertices.push(newv);
+            }
+        } else {
+            if(n_dot_last<0){
+                // Start >= 0, end < 0 so output intersection and end
+                var newv = new Vec3();
+                firstVertex.lerp(lastVertex,
+                                 n_dot_first / (n_dot_first - n_dot_last),
+                                 newv);
+                outVertices.push(newv);
+                outVertices.push(lastVertex);
+            }
+        }
+        firstVertex = lastVertex;
+        n_dot_first = n_dot_last;
+    }
+    return outVertices;
+};
+
+// Updates .worldVertices and sets .worldVerticesNeedsUpdate to false.
+ConvexPolyhedron.prototype.computeWorldVertices = function(position,quat){
+    var N = this.vertices.length;
+    while(this.worldVertices.length < N){
+        this.worldVertices.push( new Vec3() );
+    }
+
+    var verts = this.vertices,
+        worldVerts = this.worldVertices;
+    for(var i=0; i!==N; i++){
+        quat.vmult( verts[i] , worldVerts[i] );
+        position.vadd( worldVerts[i] , worldVerts[i] );
+    }
+
+    this.worldVerticesNeedsUpdate = false;
+};
+
+var computeLocalAABB_worldVert = new Vec3();
+ConvexPolyhedron.prototype.computeLocalAABB = function(aabbmin,aabbmax){
+    var n = this.vertices.length,
+        vertices = this.vertices,
+        worldVert = computeLocalAABB_worldVert;
+
+    aabbmin.set(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
+    aabbmax.set(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
+
+    for(var i=0; i<n; i++){
+        var v = vertices[i];
+        if     (v.x < aabbmin.x){
+            aabbmin.x = v.x;
+        } else if(v.x > aabbmax.x){
+            aabbmax.x = v.x;
+        }
+        if     (v.y < aabbmin.y){
+            aabbmin.y = v.y;
+        } else if(v.y > aabbmax.y){
+            aabbmax.y = v.y;
+        }
+        if     (v.z < aabbmin.z){
+            aabbmin.z = v.z;
+        } else if(v.z > aabbmax.z){
+            aabbmax.z = v.z;
+        }
+    }
+};
+
+/**
+ * Updates .worldVertices and sets .worldVerticesNeedsUpdate to false.
+ * @method computeWorldFaceNormals
+ * @param  {Quaternion} quat
+ */
+ConvexPolyhedron.prototype.computeWorldFaceNormals = function(quat){
+    var N = this.faceNormals.length;
+    while(this.worldFaceNormals.length < N){
+        this.worldFaceNormals.push( new Vec3() );
+    }
+
+    var normals = this.faceNormals,
+        worldNormals = this.worldFaceNormals;
+    for(var i=0; i!==N; i++){
+        quat.vmult( normals[i] , worldNormals[i] );
+    }
+
+    this.worldFaceNormalsNeedsUpdate = false;
+};
+
+/**
+ * @method updateBoundingSphereRadius
+ */
+ConvexPolyhedron.prototype.updateBoundingSphereRadius = function(){
+    // Assume points are distributed with local (0,0,0) as center
+    var max2 = 0;
+    var verts = this.vertices;
+    for(var i=0, N=verts.length; i!==N; i++) {
+        var norm2 = verts[i].norm2();
+        if(norm2 > max2){
+            max2 = norm2;
+        }
+    }
+    this.boundingSphereRadius = Math.sqrt(max2);
+};
+
+var tempWorldVertex = new Vec3();
+
+/**
+ * @method calculateWorldAABB
+ * @param {Vec3}        pos
+ * @param {Quaternion}  quat
+ * @param {Vec3}        min
+ * @param {Vec3}        max
+ */
+ConvexPolyhedron.prototype.calculateWorldAABB = function(pos,quat,min,max){
+    var n = this.vertices.length, verts = this.vertices;
+    var minx,miny,minz,maxx,maxy,maxz;
+    for(var i=0; i<n; i++){
+        tempWorldVertex.copy(verts[i]);
+        quat.vmult(tempWorldVertex,tempWorldVertex);
+        pos.vadd(tempWorldVertex,tempWorldVertex);
+        var v = tempWorldVertex;
+        if     (v.x < minx || minx===undefined){
+            minx = v.x;
+        } else if(v.x > maxx || maxx===undefined){
+            maxx = v.x;
+        }
+
+        if     (v.y < miny || miny===undefined){
+            miny = v.y;
+        } else if(v.y > maxy || maxy===undefined){
+            maxy = v.y;
+        }
+
+        if     (v.z < minz || minz===undefined){
+            minz = v.z;
+        } else if(v.z > maxz || maxz===undefined){
+            maxz = v.z;
+        }
+    }
+    min.set(minx,miny,minz);
+    max.set(maxx,maxy,maxz);
+};
+
+/**
+ * Get approximate convex volume
+ * @method volume
+ * @return {Number}
+ */
+ConvexPolyhedron.prototype.volume = function(){
+    return 4.0 * Math.PI * this.boundingSphereRadius / 3.0;
+};
+
+/**
+ * Get an average of all the vertices positions
+ * @method getAveragePointLocal
+ * @param  {Vec3} target
+ * @return {Vec3}
+ */
+ConvexPolyhedron.prototype.getAveragePointLocal = function(target){
+    target = target || new Vec3();
+    var n = this.vertices.length,
+        verts = this.vertices;
+    for(var i=0; i<n; i++){
+        target.vadd(verts[i],target);
+    }
+    target.mult(1/n,target);
+    return target;
+};
+
+/**
+ * Transform all local points. Will change the .vertices
+ * @method transformAllPoints
+ * @param  {Vec3} offset
+ * @param  {Quaternion} quat
+ */
+ConvexPolyhedron.prototype.transformAllPoints = function(offset,quat){
+    var n = this.vertices.length,
+        verts = this.vertices;
+
+    // Apply rotation
+    if(quat){
+        // Rotate vertices
+        for(var i=0; i<n; i++){
+            var v = verts[i];
+            quat.vmult(v,v);
+        }
+        // Rotate face normals
+        for(var i=0; i<this.faceNormals.length; i++){
+            var v = this.faceNormals[i];
+            quat.vmult(v,v);
+        }
+        /*
+        // Rotate edges
+        for(var i=0; i<this.uniqueEdges.length; i++){
+            var v = this.uniqueEdges[i];
+            quat.vmult(v,v);
+        }*/
+    }
+
+    // Apply offset
+    if(offset){
+        for(var i=0; i<n; i++){
+            var v = verts[i];
+            v.vadd(offset,v);
+        }
+    }
+};
+
+/**
+ * Checks whether p is inside the polyhedra. Must be in local coords. The point lies outside of the convex hull of the other points if and only if the direction of all the vectors from it to those other points are on less than one half of a sphere around it.
+ * @method pointIsInside
+ * @param  {Vec3} p      A point given in local coordinates
+ * @return {Boolean}
+ */
+var ConvexPolyhedron_pointIsInside = new Vec3();
+var ConvexPolyhedron_vToP = new Vec3();
+var ConvexPolyhedron_vToPointInside = new Vec3();
+ConvexPolyhedron.prototype.pointIsInside = function(p){
+    var n = this.vertices.length,
+        verts = this.vertices,
+        faces = this.faces,
+        normals = this.faceNormals;
+    var positiveResult = null;
+    var N = this.faces.length;
+    var pointInside = ConvexPolyhedron_pointIsInside;
+    this.getAveragePointLocal(pointInside);
+    for(var i=0; i<N; i++){
+        var numVertices = this.faces[i].length;
+        var n = normals[i];
+        var v = verts[faces[i][0]]; // We only need one point in the face
+
+        // This dot product determines which side of the edge the point is
+        var vToP = ConvexPolyhedron_vToP;
+        p.vsub(v,vToP);
+        var r1 = n.dot(vToP);
+
+        var vToPointInside = ConvexPolyhedron_vToPointInside;
+        pointInside.vsub(v,vToPointInside);
+        var r2 = n.dot(vToPointInside);
+
+        if((r1<0 && r2>0) || (r1>0 && r2<0)){
+            return false; // Encountered some other sign. Exit.
+        } else {
+        }
+    }
+
+    // If we got here, all dot products were of the same sign.
+    return positiveResult ? 1 : -1;
+};
+
+/**
+ * Get max and min dot product of a convex hull at position (pos,quat) projected onto an axis. Results are saved in the array maxmin.
+ * @static
+ * @method project
+ * @param {ConvexPolyhedron} hull
+ * @param {Vec3} axis
+ * @param {Vec3} pos
+ * @param {Quaternion} quat
+ * @param {array} result result[0] and result[1] will be set to maximum and minimum, respectively.
+ */
+var project_worldVertex = new Vec3();
+var project_localAxis = new Vec3();
+var project_localOrigin = new Vec3();
+ConvexPolyhedron.project = function(hull, axis, pos, quat, result){
+    var n = hull.vertices.length,
+        worldVertex = project_worldVertex,
+        localAxis = project_localAxis,
+        max = 0,
+        min = 0,
+        localOrigin = project_localOrigin,
+        vs = hull.vertices;
+
+    localOrigin.setZero();
+
+    // Transform the axis to local
+    Transform.vectorToLocalFrame(pos, quat, axis, localAxis);
+    Transform.pointToLocalFrame(pos, quat, localOrigin, localOrigin);
+    var add = localOrigin.dot(localAxis);
+
+    min = max = vs[0].dot(localAxis);
+
+    for(var i = 1; i < n; i++){
+        var val = vs[i].dot(localAxis);
+
+        if(val > max){
+            max = val;
+        }
+
+        if(val < min){
+            min = val;
+        }
+    }
+
+    min -= add;
+    max -= add;
+
+    if(min > max){
+        // Inconsistent - swap
+        var temp = min;
+        min = max;
+        max = temp;
+    }
+    // Output
+    result[0] = max;
+    result[1] = min;
+};
+
+},{"../math/Quaternion":44,"../math/Transform":45,"../math/Vec3":46,"./Shape":59}],55:[function(require,module,exports){
+module.exports = Cylinder;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var ConvexPolyhedron = require('./ConvexPolyhedron');
+
+/**
+ * @class Cylinder
+ * @constructor
+ * @extends ConvexPolyhedron
+ * @author schteppe / https://github.com/schteppe
+ * @param {Number} radiusTop
+ * @param {Number} radiusBottom
+ * @param {Number} height
+ * @param {Number} numSegments The number of segments to build the cylinder out of
+ */
+function Cylinder( radiusTop, radiusBottom, height , numSegments ) {
+    var N = numSegments,
+        verts = [],
+        axes = [],
+        faces = [],
+        bottomface = [],
+        topface = [],
+        cos = Math.cos,
+        sin = Math.sin;
+
+    // First bottom point
+    verts.push(new Vec3(radiusBottom*cos(0),
+                               radiusBottom*sin(0),
+                               -height*0.5));
+    bottomface.push(0);
+
+    // First top point
+    verts.push(new Vec3(radiusTop*cos(0),
+                               radiusTop*sin(0),
+                               height*0.5));
+    topface.push(1);
+
+    for(var i=0; i<N; i++){
+        var theta = 2*Math.PI/N * (i+1);
+        var thetaN = 2*Math.PI/N * (i+0.5);
+        if(i<N-1){
+            // Bottom
+            verts.push(new Vec3(radiusBottom*cos(theta),
+                                       radiusBottom*sin(theta),
+                                       -height*0.5));
+            bottomface.push(2*i+2);
+            // Top
+            verts.push(new Vec3(radiusTop*cos(theta),
+                                       radiusTop*sin(theta),
+                                       height*0.5));
+            topface.push(2*i+3);
+
+            // Face
+            faces.push([2*i+2, 2*i+3, 2*i+1,2*i]);
+        } else {
+            faces.push([0,1, 2*i+1, 2*i]); // Connect
+        }
+
+        // Axis: we can cut off half of them if we have even number of segments
+        if(N % 2 === 1 || i < N / 2){
+            axes.push(new Vec3(cos(thetaN), sin(thetaN), 0));
+        }
+    }
+    faces.push(topface);
+    axes.push(new Vec3(0,0,1));
+
+    // Reorder bottom face
+    var temp = [];
+    for(var i=0; i<bottomface.length; i++){
+        temp.push(bottomface[bottomface.length - i - 1]);
+    }
+    faces.push(temp);
+
+    this.type = Shape.types.CONVEXPOLYHEDRON;
+    ConvexPolyhedron.call( this, verts, faces, axes );
+}
+
+Cylinder.prototype = new ConvexPolyhedron();
+
+},{"../math/Quaternion":44,"../math/Vec3":46,"./ConvexPolyhedron":54,"./Shape":59}],56:[function(require,module,exports){
+var Shape = require('./Shape');
+var ConvexPolyhedron = require('./ConvexPolyhedron');
+var Vec3 = require('../math/Vec3');
+var Utils = require('../utils/Utils');
+
+module.exports = Heightfield;
+
+/**
+ * Heightfield shape class. Height data is given as an array. These data points are spread out evenly with a given distance.
+ * @class Heightfield
+ * @extends Shape
+ * @constructor
+ * @param {Array} data An array of Y values that will be used to construct the terrain.
+ * @param {object} options
+ * @param {Number} [options.minValue] Minimum value of the data points in the data array. Will be computed automatically if not given.
+ * @param {Number} [options.maxValue] Maximum value.
+ * @param {Number} [options.elementSize=0.1] World spacing between the data points in X direction.
+ * @todo Should be possible to use along all axes, not just y
+ * @todo should be possible to scale along all axes
+ *
+ * @example
+ *     // Generate some height data (y-values).
+ *     var data = [];
+ *     for(var i = 0; i < 1000; i++){
+ *         var y = 0.5 * Math.cos(0.2 * i);
+ *         data.push(y);
+ *     }
+ *
+ *     // Create the heightfield shape
+ *     var heightfieldShape = new Heightfield(data, {
+ *         elementSize: 1 // Distance between the data points in X and Y directions
+ *     });
+ *     var heightfieldBody = new Body();
+ *     heightfieldBody.addShape(heightfieldShape);
+ *     world.addBody(heightfieldBody);
+ */
+function Heightfield(data, options){
+    options = Utils.defaults(options, {
+        maxValue : null,
+        minValue : null,
+        elementSize : 1
+    });
+
+    /**
+     * An array of numbers, or height values, that are spread out along the x axis.
+     * @property {array} data
+     */
+    this.data = data;
+
+    /**
+     * Max value of the data
+     * @property {number} maxValue
+     */
+    this.maxValue = options.maxValue;
+
+    /**
+     * Max value of the data
+     * @property {number} minValue
+     */
+    this.minValue = options.minValue;
+
+    /**
+     * The width of each element
+     * @property {number} elementSize
+     * @todo elementSizeX and Y
+     */
+    this.elementSize = options.elementSize;
+
+    if(options.minValue === null){
+        this.updateMinValue();
+    }
+    if(options.maxValue === null){
+        this.updateMaxValue();
+    }
+
+    this.cacheEnabled = true;
+
+    Shape.call(this);
+
+    this.pillarConvex = new ConvexPolyhedron();
+    this.pillarOffset = new Vec3();
+
+    this.type = Shape.types.HEIGHTFIELD;
+    this.updateBoundingSphereRadius();
+
+    // "i_j_isUpper" => { convex: ..., offset: ... }
+    // for example:
+    // _cachedPillars["0_2_1"]
+    this._cachedPillars = {};
+}
+Heightfield.prototype = new Shape();
+
+/**
+ * Call whenever you change the data array.
+ * @method update
+ */
+Heightfield.prototype.update = function(){
+    this._cachedPillars = {};
+};
+
+/**
+ * Update the .minValue property
+ * @method updateMinValue
+ */
+Heightfield.prototype.updateMinValue = function(){
+    var data = this.data;
+    var minValue = data[0][0];
+    for(var i=0; i !== data.length; i++){
+        for(var j=0; j !== data[i].length; j++){
+            var v = data[i][j];
+            if(v < minValue){
+                minValue = v;
+            }
+        }
+    }
+    this.minValue = minValue;
+};
+
+/**
+ * Update the .maxValue property
+ * @method updateMaxValue
+ */
+Heightfield.prototype.updateMaxValue = function(){
+    var data = this.data;
+    var maxValue = data[0][0];
+    for(var i=0; i !== data.length; i++){
+        for(var j=0; j !== data[i].length; j++){
+            var v = data[i][j];
+            if(v > maxValue){
+                maxValue = v;
+            }
+        }
+    }
+    this.maxValue = maxValue;
+};
+
+/**
+ * Set the height value at an index. Don't forget to update maxValue and minValue after you're done.
+ * @method setHeightValueAtIndex
+ * @param {integer} xi
+ * @param {integer} yi
+ * @param {number} value
+ */
+Heightfield.prototype.setHeightValueAtIndex = function(xi, yi, value){
+    var data = this.data;
+    data[xi][yi] = value;
+
+    // Invalidate cache
+    this.clearCachedConvexTrianglePillar(xi, yi, false);
+    if(xi > 0){
+        this.clearCachedConvexTrianglePillar(xi - 1, yi, true);
+        this.clearCachedConvexTrianglePillar(xi - 1, yi, false);
+    }
+    if(yi > 0){
+        this.clearCachedConvexTrianglePillar(xi, yi - 1, true);
+        this.clearCachedConvexTrianglePillar(xi, yi - 1, false);
+    }
+    if(yi > 0 && xi > 0){
+        this.clearCachedConvexTrianglePillar(xi - 1, yi - 1, true);
+    }
+};
+
+/**
+ * Get max/min in a rectangle in the matrix data
+ * @method getRectMinMax
+ * @param  {integer} iMinX
+ * @param  {integer} iMinY
+ * @param  {integer} iMaxX
+ * @param  {integer} iMaxY
+ * @param  {array} [result] An array to store the results in.
+ * @return {array} The result array, if it was passed in. Minimum will be at position 0 and max at 1.
+ */
+Heightfield.prototype.getRectMinMax = function (iMinX, iMinY, iMaxX, iMaxY, result) {
+    result = result || [];
+
+    // Get max and min of the data
+    var data = this.data,
+        max = this.minValue; // Set first value
+    for(var i = iMinX; i <= iMaxX; i++){
+        for(var j = iMinY; j <= iMaxY; j++){
+            var height = data[i][j];
+            if(height > max){
+                max = height;
+            }
+        }
+    }
+
+    result[0] = this.minValue;
+    result[1] = max;
+};
+
+
+
+/**
+ * Get the index of a local position on the heightfield. The indexes indicate the rectangles, so if your terrain is made of N x N height data points, you will have rectangle indexes ranging from 0 to N-1.
+ * @method getIndexOfPosition
+ * @param  {number} x
+ * @param  {number} y
+ * @param  {array} result Two-element array
+ * @param  {boolean} clamp If the position should be clamped to the heightfield edge.
+ * @return {boolean}
+ */
+Heightfield.prototype.getIndexOfPosition = function (x, y, result, clamp) {
+
+    // Get the index of the data points to test against
+    var w = this.elementSize;
+    var data = this.data;
+    var xi = Math.floor(x / w);
+    var yi = Math.floor(y / w);
+
+    result[0] = xi;
+    result[1] = yi;
+
+    if(clamp){
+        // Clamp index to edges
+        if(xi < 0){ xi = 0; }
+        if(yi < 0){ yi = 0; }
+        if(xi >= data.length - 1){ xi = data.length - 1; }
+        if(yi >= data[0].length - 1){ yi = data[0].length - 1; }
+    }
+
+    // Bail out if we are out of the terrain
+    if(xi < 0 || yi < 0 || xi >= data.length-1 || yi >= data[0].length-1){
+        return false;
+    }
+
+    return true;
+};
+
+
+var getHeightAt_idx = [];
+var getHeightAt_weights = new Vec3();
+var getHeightAt_a = new Vec3();
+var getHeightAt_b = new Vec3();
+var getHeightAt_c = new Vec3();
+
+Heightfield.prototype.getTriangleAt = function(x, y, edgeClamp, a, b, c){
+    var idx = getHeightAt_idx;
+    this.getIndexOfPosition(x, y, idx, edgeClamp);
+    var xi = idx[0];
+    var yi = idx[1];
+
+    var data = this.data;
+    if(edgeClamp){
+        xi = Math.min(data.length - 2, Math.max(0, xi));
+        yi = Math.min(data[0].length - 2, Math.max(0, yi));
+    }
+
+    var elementSize = this.elementSize;
+    var lowerDist2 = Math.pow(x / elementSize - xi, 2) + Math.pow(y / elementSize - yi, 2);
+    var upperDist2 = Math.pow(x / elementSize - (xi + 1), 2) + Math.pow(y / elementSize - (yi + 1), 2);
+    var upper = lowerDist2 > upperDist2;
+    this.getTriangle(xi, yi, upper, a, b, c);
+    return upper;
+};
+
+var getNormalAt_a = new Vec3();
+var getNormalAt_b = new Vec3();
+var getNormalAt_c = new Vec3();
+var getNormalAt_e0 = new Vec3();
+var getNormalAt_e1 = new Vec3();
+Heightfield.prototype.getNormalAt = function(x, y, edgeClamp, result){
+    var a = getNormalAt_a;
+    var b = getNormalAt_b;
+    var c = getNormalAt_c;
+    var e0 = getNormalAt_e0;
+    var e1 = getNormalAt_e1;
+    this.getTriangleAt(x, y, edgeClamp, a, b, c);
+    b.vsub(a, e0);
+    c.vsub(a, e1);
+    e0.cross(e1, result);
+    result.normalize();
+};
+
+
+/**
+ * Get an AABB of a square in the heightfield
+ * @param  {number} xi
+ * @param  {number} yi
+ * @param  {AABB} result
+ */
+Heightfield.prototype.getAabbAtIndex = function(xi, yi, result){
+    var data = this.data;
+    var elementSize = this.elementSize;
+
+    result.lowerBound.set(
+        xi * elementSize,
+        yi * elementSize,
+        data[xi][yi]
+    );
+    result.upperBound.set(
+        (xi + 1) * elementSize,
+        (yi + 1) * elementSize,
+        data[xi + 1][yi + 1]
+    );
+};
+
+
+/**
+ * Get the height in the heightfield at a given position
+ * @param  {number} x
+ * @param  {number} y
+ * @param  {boolean} edgeClamp
+ * @return {number}
+ */
+Heightfield.prototype.getHeightAt = function(x, y, edgeClamp){
+    var data = this.data;
+    var a = getHeightAt_a;
+    var b = getHeightAt_b;
+    var c = getHeightAt_c;
+    var idx = getHeightAt_idx;
+
+    this.getIndexOfPosition(x, y, idx, edgeClamp);
+    var xi = idx[0];
+    var yi = idx[1];
+    if(edgeClamp){
+        xi = Math.min(data.length - 2, Math.max(0, xi));
+        yi = Math.min(data[0].length - 2, Math.max(0, yi));
+    }
+    var upper = this.getTriangleAt(x, y, edgeClamp, a, b, c);
+    barycentricWeights(x, y, a.x, a.y, b.x, b.y, c.x, c.y, getHeightAt_weights);
+
+    var w = getHeightAt_weights;
+
+    if(upper){
+
+        // Top triangle verts
+        return data[xi + 1][yi + 1] * w.x + data[xi][yi + 1] * w.y + data[xi + 1][yi] * w.z;
+
+    } else {
+
+        // Top triangle verts
+        return data[xi][yi] * w.x + data[xi + 1][yi] * w.y + data[xi][yi + 1] * w.z;
+    }
+};
+
+// from https://en.wikipedia.org/wiki/Barycentric_coordinate_system
+function barycentricWeights(x, y, ax, ay, bx, by, cx, cy, result){
+    result.x = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / ((by - cy) * (ax - cx) + (cx - bx) * (ay - cy));
+    result.y = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / ((by - cy) * (ax - cx) + (cx - bx) * (ay - cy));
+    result.z = 1 - result.x - result.y;
+}
+
+Heightfield.prototype.getCacheConvexTrianglePillarKey = function(xi, yi, getUpperTriangle){
+    return xi + '_' + yi + '_' + (getUpperTriangle ? 1 : 0);
+};
+
+Heightfield.prototype.getCachedConvexTrianglePillar = function(xi, yi, getUpperTriangle){
+    return this._cachedPillars[this.getCacheConvexTrianglePillarKey(xi, yi, getUpperTriangle)];
+};
+
+Heightfield.prototype.setCachedConvexTrianglePillar = function(xi, yi, getUpperTriangle, convex, offset){
+    this._cachedPillars[this.getCacheConvexTrianglePillarKey(xi, yi, getUpperTriangle)] = {
+        convex: convex,
+        offset: offset
+    };
+};
+
+Heightfield.prototype.clearCachedConvexTrianglePillar = function(xi, yi, getUpperTriangle){
+    delete this._cachedPillars[this.getCacheConvexTrianglePillarKey(xi, yi, getUpperTriangle)];
+};
+
+/**
+ * Get a triangle from the heightfield
+ * @param  {number} xi
+ * @param  {number} yi
+ * @param  {boolean} upper
+ * @param  {Vec3} a
+ * @param  {Vec3} b
+ * @param  {Vec3} c
+ */
+Heightfield.prototype.getTriangle = function(xi, yi, upper, a, b, c){
+    var data = this.data;
+    var elementSize = this.elementSize;
+
+    if(upper){
+
+        // Top triangle verts
+        a.set(
+            (xi + 1) * elementSize,
+            (yi + 1) * elementSize,
+            data[xi + 1][yi + 1]
+        );
+        b.set(
+            xi * elementSize,
+            (yi + 1) * elementSize,
+            data[xi][yi + 1]
+        );
+        c.set(
+            (xi + 1) * elementSize,
+            yi * elementSize,
+            data[xi + 1][yi]
+        );
+
+    } else {
+
+        // Top triangle verts
+        a.set(
+            xi * elementSize,
+            yi * elementSize,
+            data[xi][yi]
+        );
+        b.set(
+            (xi + 1) * elementSize,
+            yi * elementSize,
+            data[xi + 1][yi]
+        );
+        c.set(
+            xi * elementSize,
+            (yi + 1) * elementSize,
+            data[xi][yi + 1]
+        );
+    }
+};
+
+/**
+ * Get a triangle in the terrain in the form of a triangular convex shape.
+ * @method getConvexTrianglePillar
+ * @param  {integer} i
+ * @param  {integer} j
+ * @param  {boolean} getUpperTriangle
+ */
+Heightfield.prototype.getConvexTrianglePillar = function(xi, yi, getUpperTriangle){
+    var result = this.pillarConvex;
+    var offsetResult = this.pillarOffset;
+
+    if(this.cacheEnabled){
+        var data = this.getCachedConvexTrianglePillar(xi, yi, getUpperTriangle);
+        if(data){
+            this.pillarConvex = data.convex;
+            this.pillarOffset = data.offset;
+            return;
+        }
+
+        result = new ConvexPolyhedron();
+        offsetResult = new Vec3();
+
+        this.pillarConvex = result;
+        this.pillarOffset = offsetResult;
+    }
+
+    var data = this.data;
+    var elementSize = this.elementSize;
+    var faces = result.faces;
+
+    // Reuse verts if possible
+    result.vertices.length = 6;
+    for (var i = 0; i < 6; i++) {
+        if(!result.vertices[i]){
+            result.vertices[i] = new Vec3();
+        }
+    }
+
+    // Reuse faces if possible
+    faces.length = 5;
+    for (var i = 0; i < 5; i++) {
+        if(!faces[i]){
+            faces[i] = [];
+        }
+    }
+
+    var verts = result.vertices;
+
+    var h = (Math.min(
+        data[xi][yi],
+        data[xi+1][yi],
+        data[xi][yi+1],
+        data[xi+1][yi+1]
+    ) - this.minValue ) / 2 + this.minValue;
+
+    if (!getUpperTriangle) {
+
+        // Center of the triangle pillar - all polygons are given relative to this one
+        offsetResult.set(
+            (xi + 0.25) * elementSize, // sort of center of a triangle
+            (yi + 0.25) * elementSize,
+            h // vertical center
+        );
+
+        // Top triangle verts
+        verts[0].set(
+            -0.25 * elementSize,
+            -0.25 * elementSize,
+            data[xi][yi] - h
+        );
+        verts[1].set(
+            0.75 * elementSize,
+            -0.25 * elementSize,
+            data[xi + 1][yi] - h
+        );
+        verts[2].set(
+            -0.25 * elementSize,
+            0.75 * elementSize,
+            data[xi][yi + 1] - h
+        );
+
+        // bottom triangle verts
+        verts[3].set(
+            -0.25 * elementSize,
+            -0.25 * elementSize,
+            -h-1
+        );
+        verts[4].set(
+            0.75 * elementSize,
+            -0.25 * elementSize,
+            -h-1
+        );
+        verts[5].set(
+            -0.25 * elementSize,
+            0.75  * elementSize,
+            -h-1
+        );
+
+        // top triangle
+        faces[0][0] = 0;
+        faces[0][1] = 1;
+        faces[0][2] = 2;
+
+        // bottom triangle
+        faces[1][0] = 5;
+        faces[1][1] = 4;
+        faces[1][2] = 3;
+
+        // -x facing quad
+        faces[2][0] = 0;
+        faces[2][1] = 2;
+        faces[2][2] = 5;
+        faces[2][3] = 3;
+
+        // -y facing quad
+        faces[3][0] = 1;
+        faces[3][1] = 0;
+        faces[3][2] = 3;
+        faces[3][3] = 4;
+
+        // +xy facing quad
+        faces[4][0] = 4;
+        faces[4][1] = 5;
+        faces[4][2] = 2;
+        faces[4][3] = 1;
+
+
+    } else {
+
+        // Center of the triangle pillar - all polygons are given relative to this one
+        offsetResult.set(
+            (xi + 0.75) * elementSize, // sort of center of a triangle
+            (yi + 0.75) * elementSize,
+            h // vertical center
+        );
+
+        // Top triangle verts
+        verts[0].set(
+            0.25 * elementSize,
+            0.25 * elementSize,
+            data[xi + 1][yi + 1] - h
+        );
+        verts[1].set(
+            -0.75 * elementSize,
+            0.25 * elementSize,
+            data[xi][yi + 1] - h
+        );
+        verts[2].set(
+            0.25 * elementSize,
+            -0.75 * elementSize,
+            data[xi + 1][yi] - h
+        );
+
+        // bottom triangle verts
+        verts[3].set(
+            0.25 * elementSize,
+            0.25 * elementSize,
+            - h-1
+        );
+        verts[4].set(
+            -0.75 * elementSize,
+            0.25 * elementSize,
+            - h-1
+        );
+        verts[5].set(
+            0.25 * elementSize,
+            -0.75 * elementSize,
+            - h-1
+        );
+
+        // Top triangle
+        faces[0][0] = 0;
+        faces[0][1] = 1;
+        faces[0][2] = 2;
+
+        // bottom triangle
+        faces[1][0] = 5;
+        faces[1][1] = 4;
+        faces[1][2] = 3;
+
+        // +x facing quad
+        faces[2][0] = 2;
+        faces[2][1] = 5;
+        faces[2][2] = 3;
+        faces[2][3] = 0;
+
+        // +y facing quad
+        faces[3][0] = 3;
+        faces[3][1] = 4;
+        faces[3][2] = 1;
+        faces[3][3] = 0;
+
+        // -xy facing quad
+        faces[4][0] = 1;
+        faces[4][1] = 4;
+        faces[4][2] = 5;
+        faces[4][3] = 2;
+    }
+
+    result.computeNormals();
+    result.computeEdges();
+    result.updateBoundingSphereRadius();
+
+    this.setCachedConvexTrianglePillar(xi, yi, getUpperTriangle, result, offsetResult);
+};
+
+Heightfield.prototype.calculateLocalInertia = function(mass, target){
+    target = target || new Vec3();
+    target.set(0, 0, 0);
+    return target;
+};
+
+Heightfield.prototype.volume = function(){
+    return Number.MAX_VALUE; // The terrain is infinite
+};
+
+Heightfield.prototype.calculateWorldAABB = function(pos, quat, min, max){
+    // TODO: do it properly
+    min.set(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
+    max.set(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
+};
+
+Heightfield.prototype.updateBoundingSphereRadius = function(){
+    // Use the bounding box of the min/max values
+    var data = this.data,
+        s = this.elementSize;
+    this.boundingSphereRadius = new Vec3(data.length * s, data[0].length * s, Math.max(Math.abs(this.maxValue), Math.abs(this.minValue))).norm();
+};
+
+/**
+ * Sets the height values from an image. Currently only supported in browser.
+ * @method setHeightsFromImage
+ * @param {Image} image
+ * @param {Vec3} scale
+ */
+Heightfield.prototype.setHeightsFromImage = function(image, scale){
+    var canvas = document.createElement('canvas');
+    canvas.width = image.width;
+    canvas.height = image.height;
+    var context = canvas.getContext('2d');
+    context.drawImage(image, 0, 0);
+    var imageData = context.getImageData(0, 0, image.width, image.height);
+
+    var matrix = this.data;
+    matrix.length = 0;
+    this.elementSize = Math.abs(scale.x) / imageData.width;
+    for(var i=0; i<imageData.height; i++){
+        var row = [];
+        for(var j=0; j<imageData.width; j++){
+            var a = imageData.data[(i*imageData.height + j) * 4];
+            var b = imageData.data[(i*imageData.height + j) * 4 + 1];
+            var c = imageData.data[(i*imageData.height + j) * 4 + 2];
+            var height = (a + b + c) / 4 / 255 * scale.z;
+            if(scale.x < 0){
+                row.push(height);
+            } else {
+                row.unshift(height);
+            }
+        }
+        if(scale.y < 0){
+            matrix.unshift(row);
+        } else {
+            matrix.push(row);
+        }
+    }
+    this.updateMaxValue();
+    this.updateMinValue();
+    this.update();
+};
+},{"../math/Vec3":46,"../utils/Utils":69,"./ConvexPolyhedron":54,"./Shape":59}],57:[function(require,module,exports){
+module.exports = Particle;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+
+/**
+ * Particle shape.
+ * @class Particle
+ * @constructor
+ * @author schteppe
+ * @extends Shape
+ */
+function Particle(){
+    Shape.call(this);
+
+    this.type = Shape.types.PARTICLE;
+}
+Particle.prototype = new Shape();
+Particle.prototype.constructor = Particle;
+
+/**
+ * @method calculateLocalInertia
+ * @param  {Number} mass
+ * @param  {Vec3} target
+ * @return {Vec3}
+ */
+Particle.prototype.calculateLocalInertia = function(mass,target){
+    target = target || new Vec3();
+    target.set(0, 0, 0);
+    return target;
+};
+
+Particle.prototype.volume = function(){
+    return 0;
+};
+
+Particle.prototype.updateBoundingSphereRadius = function(){
+    this.boundingSphereRadius = 0;
+};
+
+Particle.prototype.calculateWorldAABB = function(pos,quat,min,max){
+    // Get each axis max
+    min.copy(pos);
+    max.copy(pos);
+};
+
+},{"../math/Vec3":46,"./Shape":59}],58:[function(require,module,exports){
+module.exports = Plane;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+
+/**
+ * A plane, facing in the Z direction. The plane has its surface at z=0 and everything below z=0 is assumed to be solid plane. To make the plane face in some other direction than z, you must put it inside a RigidBody and rotate that body. See the demos.
+ * @class Plane
+ * @constructor
+ * @extends Shape
+ * @author schteppe
+ */
+function Plane(){
+    Shape.call(this);
+    this.type = Shape.types.PLANE;
+
+    // World oriented normal
+    this.worldNormal = new Vec3();
+    this.worldNormalNeedsUpdate = true;
+
+    this.boundingSphereRadius = Number.MAX_VALUE;
+}
+Plane.prototype = new Shape();
+Plane.prototype.constructor = Plane;
+
+Plane.prototype.computeWorldNormal = function(quat){
+    var n = this.worldNormal;
+    n.set(0,0,1);
+    quat.vmult(n,n);
+    this.worldNormalNeedsUpdate = false;
+};
+
+Plane.prototype.calculateLocalInertia = function(mass,target){
+    target = target || new Vec3();
+    return target;
+};
+
+Plane.prototype.volume = function(){
+    return Number.MAX_VALUE; // The plane is infinite...
+};
+
+var tempNormal = new Vec3();
+Plane.prototype.calculateWorldAABB = function(pos, quat, min, max){
+    // The plane AABB is infinite, except if the normal is pointing along any axis
+    tempNormal.set(0,0,1); // Default plane normal is z
+    quat.vmult(tempNormal,tempNormal);
+    var maxVal = Number.MAX_VALUE;
+    min.set(-maxVal, -maxVal, -maxVal);
+    max.set(maxVal, maxVal, maxVal);
+
+    if(tempNormal.x === 1){ max.x = pos.x; }
+    if(tempNormal.y === 1){ max.y = pos.y; }
+    if(tempNormal.z === 1){ max.z = pos.z; }
+
+    if(tempNormal.x === -1){ min.x = pos.x; }
+    if(tempNormal.y === -1){ min.y = pos.y; }
+    if(tempNormal.z === -1){ min.z = pos.z; }
+};
+
+Plane.prototype.updateBoundingSphereRadius = function(){
+    this.boundingSphereRadius = Number.MAX_VALUE;
+};
+},{"../math/Vec3":46,"./Shape":59}],59:[function(require,module,exports){
+module.exports = Shape;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Material = require('../material/Material');
+
+/**
+ * Base class for shapes
+ * @class Shape
+ * @constructor
+ * @author schteppe
+ * @todo Should have a mechanism for caching bounding sphere radius instead of calculating it each time
+ */
+function Shape(){
+
+    /**
+     * Identifyer of the Shape.
+     * @property {number} id
+     */
+    this.id = Shape.idCounter++;
+
+    /**
+     * The type of this shape. Must be set to an int > 0 by subclasses.
+     * @property type
+     * @type {Number}
+     * @see Shape.types
+     */
+    this.type = 0;
+
+    /**
+     * The local bounding sphere radius of this shape.
+     * @property {Number} boundingSphereRadius
+     */
+    this.boundingSphereRadius = 0;
+
+    /**
+     * Whether to produce contact forces when in contact with other bodies. Note that contacts will be generated, but they will be disabled.
+     * @property {boolean} collisionResponse
+     */
+    this.collisionResponse = true;
+
+    /**
+     * @property {Material} material
+     */
+    this.material = null;
+
+    /**
+     * @property {Body} body
+     */
+    this.body = null;
+}
+Shape.prototype.constructor = Shape;
+
+/**
+ * Computes the bounding sphere radius. The result is stored in the property .boundingSphereRadius
+ * @method updateBoundingSphereRadius
+ */
+Shape.prototype.updateBoundingSphereRadius = function(){
+    throw "computeBoundingSphereRadius() not implemented for shape type "+this.type;
+};
+
+/**
+ * Get the volume of this shape
+ * @method volume
+ * @return {Number}
+ */
+Shape.prototype.volume = function(){
+    throw "volume() not implemented for shape type "+this.type;
+};
+
+/**
+ * Calculates the inertia in the local frame for this shape.
+ * @method calculateLocalInertia
+ * @param {Number} mass
+ * @param {Vec3} target
+ * @see http://en.wikipedia.org/wiki/List_of_moments_of_inertia
+ */
+Shape.prototype.calculateLocalInertia = function(mass,target){
+    throw "calculateLocalInertia() not implemented for shape type "+this.type;
+};
+
+Shape.idCounter = 0;
+
+/**
+ * The available shape types.
+ * @static
+ * @property types
+ * @type {Object}
+ */
+Shape.types = {
+    SPHERE:1,
+    PLANE:2,
+    BOX:4,
+    COMPOUND:8,
+    CONVEXPOLYHEDRON:16,
+    HEIGHTFIELD:32,
+    PARTICLE:64,
+    CYLINDER:128,
+    TRIMESH:256
+};
+
+
+},{"../material/Material":41,"../math/Quaternion":44,"../math/Vec3":46,"./Shape":59}],60:[function(require,module,exports){
+module.exports = Sphere;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+
+/**
+ * Spherical shape
+ * @class Sphere
+ * @constructor
+ * @extends Shape
+ * @param {Number} radius The radius of the sphere, a non-negative number.
+ * @author schteppe / http://github.com/schteppe
+ */
+function Sphere(radius){
+    Shape.call(this);
+
+    /**
+     * @property {Number} radius
+     */
+    this.radius = radius!==undefined ? Number(radius) : 1.0;
+    this.type = Shape.types.SPHERE;
+
+    if(this.radius < 0){
+        throw new Error('The sphere radius cannot be negative.');
+    }
+
+    this.updateBoundingSphereRadius();
+}
+Sphere.prototype = new Shape();
+Sphere.prototype.constructor = Sphere;
+
+Sphere.prototype.calculateLocalInertia = function(mass,target){
+    target = target || new Vec3();
+    var I = 2.0*mass*this.radius*this.radius/5.0;
+    target.x = I;
+    target.y = I;
+    target.z = I;
+    return target;
+};
+
+Sphere.prototype.volume = function(){
+    return 4.0 * Math.PI * this.radius / 3.0;
+};
+
+Sphere.prototype.updateBoundingSphereRadius = function(){
+    this.boundingSphereRadius = this.radius;
+};
+
+Sphere.prototype.calculateWorldAABB = function(pos,quat,min,max){
+    var r = this.radius;
+    var axes = ['x','y','z'];
+    for(var i=0; i<axes.length; i++){
+        var ax = axes[i];
+        min[ax] = pos[ax] - r;
+        max[ax] = pos[ax] + r;
+    }
+};
+
+},{"../math/Vec3":46,"./Shape":59}],61:[function(require,module,exports){
+module.exports = Trimesh;
+
+var Shape = require('./Shape');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Transform = require('../math/Transform');
+var AABB = require('../collision/AABB');
+var Octree = require('../utils/Octree');
+
+/**
+ * @class Trimesh
+ * @constructor
+ * @param {array} vertices
+ * @param {array} indices
+ * @extends Shape
+ * @example
+ *     // How to make a mesh with a single triangle
+ *     var vertices = [
+ *         0, 0, 0, // vertex 0
+ *         1, 0, 0, // vertex 1
+ *         0, 1, 0  // vertex 2
+ *     ];
+ *     var indices = [
+ *         0, 1, 2  // triangle 0
+ *     ];
+ *     var trimeshShape = new Trimesh(vertices, indices);
+ */
+function Trimesh(vertices, indices) {
+    Shape.call(this);
+    this.type = Shape.types.TRIMESH;
+
+    /**
+     * @property vertices
+     * @type {Array}
+     */
+    this.vertices = new Float32Array(vertices);
+
+    /**
+     * Array of integers, indicating which vertices each triangle consists of. The length of this array is thus 3 times the number of triangles.
+     * @property indices
+     * @type {Array}
+     */
+    this.indices = new Int16Array(indices);
+
+    /**
+     * The normals data.
+     * @property normals
+     * @type {Array}
+     */
+    this.normals = new Float32Array(indices.length);
+
+    /**
+     * The local AABB of the mesh.
+     * @property aabb
+     * @type {Array}
+     */
+    this.aabb = new AABB();
+
+    /**
+     * References to vertex pairs, making up all unique edges in the trimesh.
+     * @property {array} edges
+     */
+    this.edges = null;
+
+    /**
+     * Local scaling of the mesh. Use .setScale() to set it.
+     * @property {Vec3} scale
+     */
+    this.scale = new Vec3(1, 1, 1);
+
+    /**
+     * The indexed triangles. Use .updateTree() to update it.
+     * @property {Octree} tree
+     */
+    this.tree = new Octree();
+
+    this.updateEdges();
+    this.updateNormals();
+    this.updateAABB();
+    this.updateBoundingSphereRadius();
+    this.updateTree();
+}
+Trimesh.prototype = new Shape();
+Trimesh.prototype.constructor = Trimesh;
+
+var computeNormals_n = new Vec3();
+
+/**
+ * @method updateTree
+ */
+Trimesh.prototype.updateTree = function(){
+    var tree = this.tree;
+
+    tree.reset();
+    tree.aabb.copy(this.aabb);
+    var scale = this.scale; // The local mesh AABB is scaled, but the octree AABB should be unscaled
+    tree.aabb.lowerBound.x *= 1 / scale.x;
+    tree.aabb.lowerBound.y *= 1 / scale.y;
+    tree.aabb.lowerBound.z *= 1 / scale.z;
+    tree.aabb.upperBound.x *= 1 / scale.x;
+    tree.aabb.upperBound.y *= 1 / scale.y;
+    tree.aabb.upperBound.z *= 1 / scale.z;
+
+    // Insert all triangles
+    var triangleAABB = new AABB();
+    var a = new Vec3();
+    var b = new Vec3();
+    var c = new Vec3();
+    var points = [a, b, c];
+    for (var i = 0; i < this.indices.length / 3; i++) {
+        //this.getTriangleVertices(i, a, b, c);
+
+        // Get unscaled triangle verts
+        var i3 = i * 3;
+        this._getUnscaledVertex(this.indices[i3], a);
+        this._getUnscaledVertex(this.indices[i3 + 1], b);
+        this._getUnscaledVertex(this.indices[i3 + 2], c);
+
+        triangleAABB.setFromPoints(points);
+        tree.insert(triangleAABB, i);
+    }
+    tree.removeEmptyNodes();
+};
+
+var unscaledAABB = new AABB();
+
+/**
+ * Get triangles in a local AABB from the trimesh.
+ * @method getTrianglesInAABB
+ * @param  {AABB} aabb
+ * @param  {array} result An array of integers, referencing the queried triangles.
+ */
+Trimesh.prototype.getTrianglesInAABB = function(aabb, result){
+    unscaledAABB.copy(aabb);
+
+    // Scale it to local
+    var scale = this.scale;
+    var isx = scale.x;
+    var isy = scale.y;
+    var isz = scale.z;
+    var l = unscaledAABB.lowerBound;
+    var u = unscaledAABB.upperBound;
+    l.x /= isx;
+    l.y /= isy;
+    l.z /= isz;
+    u.x /= isx;
+    u.y /= isy;
+    u.z /= isz;
+
+    return this.tree.aabbQuery(unscaledAABB, result);
+};
+
+/**
+ * @method setScale
+ * @param {Vec3} scale
+ */
+Trimesh.prototype.setScale = function(scale){
+    var wasUniform = this.scale.x === this.scale.y === this.scale.z;
+    var isUniform = scale.x === scale.y === scale.z;
+
+    if(!(wasUniform && isUniform)){
+        // Non-uniform scaling. Need to update normals.
+        this.updateNormals();
+    }
+    this.scale.copy(scale);
+    this.updateAABB();
+    this.updateBoundingSphereRadius();
+};
+
+/**
+ * Compute the normals of the faces. Will save in the .normals array.
+ * @method updateNormals
+ */
+Trimesh.prototype.updateNormals = function(){
+    var n = computeNormals_n;
+
+    // Generate normals
+    var normals = this.normals;
+    for(var i=0; i < this.indices.length / 3; i++){
+        var i3 = i * 3;
+
+        var a = this.indices[i3],
+            b = this.indices[i3 + 1],
+            c = this.indices[i3 + 2];
+
+        this.getVertex(a, va);
+        this.getVertex(b, vb);
+        this.getVertex(c, vc);
+
+        Trimesh.computeNormal(vb, va, vc, n);
+
+        normals[i3] = n.x;
+        normals[i3 + 1] = n.y;
+        normals[i3 + 2] = n.z;
+    }
+};
+
+/**
+ * Update the .edges property
+ * @method updateEdges
+ */
+Trimesh.prototype.updateEdges = function(){
+    var edges = {};
+    var add = function(indexA, indexB){
+        var key = a < b ? a + '_' + b : b + '_' + a;
+        edges[key] = true;
+    };
+    for(var i=0; i < this.indices.length / 3; i++){
+        var i3 = i * 3;
+        var a = this.indices[i3],
+            b = this.indices[i3 + 1],
+            c = this.indices[i3 + 2];
+        add(a,b);
+        add(b,c);
+        add(c,a);
+    }
+    var keys = Object.keys(edges);
+    this.edges = new Int16Array(keys.length * 2);
+    for (var i = 0; i < keys.length; i++) {
+        var indices = keys[i].split('_');
+        this.edges[2 * i] = parseInt(indices[0], 10);
+        this.edges[2 * i + 1] = parseInt(indices[1], 10);
+    }
+};
+
+/**
+ * Get an edge vertex
+ * @method getEdgeVertex
+ * @param  {number} edgeIndex
+ * @param  {number} firstOrSecond 0 or 1, depending on which one of the vertices you need.
+ * @param  {Vec3} vertexStore Where to store the result
+ */
+Trimesh.prototype.getEdgeVertex = function(edgeIndex, firstOrSecond, vertexStore){
+    var vertexIndex = this.edges[edgeIndex * 2 + (firstOrSecond ? 1 : 0)];
+    this.getVertex(vertexIndex, vertexStore);
+};
+
+var getEdgeVector_va = new Vec3();
+var getEdgeVector_vb = new Vec3();
+
+/**
+ * Get a vector along an edge.
+ * @method getEdgeVector
+ * @param  {number} edgeIndex
+ * @param  {Vec3} vectorStore
+ */
+Trimesh.prototype.getEdgeVector = function(edgeIndex, vectorStore){
+    var va = getEdgeVector_va;
+    var vb = getEdgeVector_vb;
+    this.getEdgeVertex(edgeIndex, 0, va);
+    this.getEdgeVertex(edgeIndex, 1, vb);
+    vb.vsub(va, vectorStore);
+};
+
+/**
+ * Get face normal given 3 vertices
+ * @static
+ * @method computeNormal
+ * @param {Vec3} va
+ * @param {Vec3} vb
+ * @param {Vec3} vc
+ * @param {Vec3} target
+ */
+var cb = new Vec3();
+var ab = new Vec3();
+Trimesh.computeNormal = function ( va, vb, vc, target ) {
+    vb.vsub(va,ab);
+    vc.vsub(vb,cb);
+    cb.cross(ab,target);
+    if ( !target.isZero() ) {
+        target.normalize();
+    }
+};
+
+var va = new Vec3();
+var vb = new Vec3();
+var vc = new Vec3();
+
+/**
+ * Get vertex i.
+ * @method getVertex
+ * @param  {number} i
+ * @param  {Vec3} out
+ * @return {Vec3} The "out" vector object
+ */
+Trimesh.prototype.getVertex = function(i, out){
+    var scale = this.scale;
+    this._getUnscaledVertex(i, out);
+    out.x *= scale.x;
+    out.y *= scale.y;
+    out.z *= scale.z;
+    return out;
+};
+
+/**
+ * Get raw vertex i
+ * @private
+ * @method _getUnscaledVertex
+ * @param  {number} i
+ * @param  {Vec3} out
+ * @return {Vec3} The "out" vector object
+ */
+Trimesh.prototype._getUnscaledVertex = function(i, out){
+    var i3 = i * 3;
+    var vertices = this.vertices;
+    return out.set(
+        vertices[i3],
+        vertices[i3 + 1],
+        vertices[i3 + 2]
+    );
+};
+
+/**
+ * Get a vertex from the trimesh,transformed by the given position and quaternion.
+ * @method getWorldVertex
+ * @param  {number} i
+ * @param  {Vec3} pos
+ * @param  {Quaternion} quat
+ * @param  {Vec3} out
+ * @return {Vec3} The "out" vector object
+ */
+Trimesh.prototype.getWorldVertex = function(i, pos, quat, out){
+    this.getVertex(i, out);
+    Transform.pointToWorldFrame(pos, quat, out, out);
+    return out;
+};
+
+/**
+ * Get the three vertices for triangle i.
+ * @method getTriangleVertices
+ * @param  {number} i
+ * @param  {Vec3} a
+ * @param  {Vec3} b
+ * @param  {Vec3} c
+ */
+Trimesh.prototype.getTriangleVertices = function(i, a, b, c){
+    var i3 = i * 3;
+    this.getVertex(this.indices[i3], a);
+    this.getVertex(this.indices[i3 + 1], b);
+    this.getVertex(this.indices[i3 + 2], c);
+};
+
+/**
+ * Compute the normal of triangle i.
+ * @method getNormal
+ * @param  {Number} i
+ * @param  {Vec3} target
+ * @return {Vec3} The "target" vector object
+ */
+Trimesh.prototype.getNormal = function(i, target){
+    var i3 = i * 3;
+    return target.set(
+        this.normals[i3],
+        this.normals[i3 + 1],
+        this.normals[i3 + 2]
+    );
+};
+
+var cli_aabb = new AABB();
+
+/**
+ * @method calculateLocalInertia
+ * @param  {Number} mass
+ * @param  {Vec3} target
+ * @return {Vec3} The "target" vector object
+ */
+Trimesh.prototype.calculateLocalInertia = function(mass,target){
+    // Approximate with box inertia
+    // Exact inertia calculation is overkill, but see http://geometrictools.com/Documentation/PolyhedralMassProperties.pdf for the correct way to do it
+    this.computeLocalAABB(cli_aabb);
+    var x = cli_aabb.upperBound.x - cli_aabb.lowerBound.x,
+        y = cli_aabb.upperBound.y - cli_aabb.lowerBound.y,
+        z = cli_aabb.upperBound.z - cli_aabb.lowerBound.z;
+    return target.set(
+        1.0 / 12.0 * mass * ( 2*y*2*y + 2*z*2*z ),
+        1.0 / 12.0 * mass * ( 2*x*2*x + 2*z*2*z ),
+        1.0 / 12.0 * mass * ( 2*y*2*y + 2*x*2*x )
+    );
+};
+
+var computeLocalAABB_worldVert = new Vec3();
+
+/**
+ * Compute the local AABB for the trimesh
+ * @method computeLocalAABB
+ * @param  {AABB} aabb
+ */
+Trimesh.prototype.computeLocalAABB = function(aabb){
+    var l = aabb.lowerBound,
+        u = aabb.upperBound,
+        n = this.vertices.length,
+        vertices = this.vertices,
+        v = computeLocalAABB_worldVert;
+
+    this.getVertex(0, v);
+    l.copy(v);
+    u.copy(v);
+
+    for(var i=0; i !== n; i++){
+        this.getVertex(i, v);
+
+        if(v.x < l.x){
+            l.x = v.x;
+        } else if(v.x > u.x){
+            u.x = v.x;
+        }
+
+        if(v.y < l.y){
+            l.y = v.y;
+        } else if(v.y > u.y){
+            u.y = v.y;
+        }
+
+        if(v.z < l.z){
+            l.z = v.z;
+        } else if(v.z > u.z){
+            u.z = v.z;
+        }
+    }
+};
+
+
+/**
+ * Update the .aabb property
+ * @method updateAABB
+ */
+Trimesh.prototype.updateAABB = function(){
+    this.computeLocalAABB(this.aabb);
+};
+
+/**
+ * Will update the .boundingSphereRadius property
+ * @method updateBoundingSphereRadius
+ */
+Trimesh.prototype.updateBoundingSphereRadius = function(){
+    // Assume points are distributed with local (0,0,0) as center
+    var max2 = 0;
+    var vertices = this.vertices;
+    var v = new Vec3();
+    for(var i=0, N=vertices.length / 3; i !== N; i++) {
+        this.getVertex(i, v);
+        var norm2 = v.norm2();
+        if(norm2 > max2){
+            max2 = norm2;
+        }
+    }
+    this.boundingSphereRadius = Math.sqrt(max2);
+};
+
+var tempWorldVertex = new Vec3();
+var calculateWorldAABB_frame = new Transform();
+var calculateWorldAABB_aabb = new AABB();
+
+/**
+ * @method calculateWorldAABB
+ * @param {Vec3}        pos
+ * @param {Quaternion}  quat
+ * @param {Vec3}        min
+ * @param {Vec3}        max
+ */
+Trimesh.prototype.calculateWorldAABB = function(pos,quat,min,max){
+    /*
+    var n = this.vertices.length / 3,
+        verts = this.vertices;
+    var minx,miny,minz,maxx,maxy,maxz;
+
+    var v = tempWorldVertex;
+    for(var i=0; i<n; i++){
+        this.getVertex(i, v);
+        quat.vmult(v, v);
+        pos.vadd(v, v);
+        if (v.x < minx || minx===undefined){
+            minx = v.x;
+        } else if(v.x > maxx || maxx===undefined){
+            maxx = v.x;
+        }
+
+        if (v.y < miny || miny===undefined){
+            miny = v.y;
+        } else if(v.y > maxy || maxy===undefined){
+            maxy = v.y;
+        }
+
+        if (v.z < minz || minz===undefined){
+            minz = v.z;
+        } else if(v.z > maxz || maxz===undefined){
+            maxz = v.z;
+        }
+    }
+    min.set(minx,miny,minz);
+    max.set(maxx,maxy,maxz);
+    */
+
+    // Faster approximation using local AABB
+    var frame = calculateWorldAABB_frame;
+    var result = calculateWorldAABB_aabb;
+    frame.position = pos;
+    frame.quaternion = quat;
+    this.aabb.toWorldFrame(frame, result);
+    min.copy(result.lowerBound);
+    max.copy(result.upperBound);
+};
+
+/**
+ * Get approximate volume
+ * @method volume
+ * @return {Number}
+ */
+Trimesh.prototype.volume = function(){
+    return 4.0 * Math.PI * this.boundingSphereRadius / 3.0;
+};
+
+/**
+ * Create a Trimesh instance, shaped as a torus.
+ * @static
+ * @method createTorus
+ * @param  {number} [radius=1]
+ * @param  {number} [tube=0.5]
+ * @param  {number} [radialSegments=8]
+ * @param  {number} [tubularSegments=6]
+ * @param  {number} [arc=6.283185307179586]
+ * @return {Trimesh} A torus
+ */
+Trimesh.createTorus = function (radius, tube, radialSegments, tubularSegments, arc) {
+    radius = radius || 1;
+    tube = tube || 0.5;
+    radialSegments = radialSegments || 8;
+    tubularSegments = tubularSegments || 6;
+    arc = arc || Math.PI * 2;
+
+    var vertices = [];
+    var indices = [];
+
+    for ( var j = 0; j <= radialSegments; j ++ ) {
+        for ( var i = 0; i <= tubularSegments; i ++ ) {
+            var u = i / tubularSegments * arc;
+            var v = j / radialSegments * Math.PI * 2;
+
+            var x = ( radius + tube * Math.cos( v ) ) * Math.cos( u );
+            var y = ( radius + tube * Math.cos( v ) ) * Math.sin( u );
+            var z = tube * Math.sin( v );
+
+            vertices.push( x, y, z );
+        }
+    }
+
+    for ( var j = 1; j <= radialSegments; j ++ ) {
+        for ( var i = 1; i <= tubularSegments; i ++ ) {
+            var a = ( tubularSegments + 1 ) * j + i - 1;
+            var b = ( tubularSegments + 1 ) * ( j - 1 ) + i - 1;
+            var c = ( tubularSegments + 1 ) * ( j - 1 ) + i;
+            var d = ( tubularSegments + 1 ) * j + i;
+
+            indices.push(a, b, d);
+            indices.push(b, c, d);
+        }
+    }
+
+    return new Trimesh(vertices, indices);
+};
+
+},{"../collision/AABB":18,"../math/Quaternion":44,"../math/Transform":45,"../math/Vec3":46,"../utils/Octree":66,"./Shape":59}],62:[function(require,module,exports){
+module.exports = GSSolver;
+
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Solver = require('./Solver');
+
+/**
+ * Constraint equation Gauss-Seidel solver.
+ * @class GSSolver
+ * @constructor
+ * @todo The spook parameters should be specified for each constraint, not globally.
+ * @author schteppe / https://github.com/schteppe
+ * @see https://www8.cs.umu.se/kurser/5DV058/VT09/lectures/spooknotes.pdf
+ * @extends Solver
+ */
+function GSSolver(){
+    Solver.call(this);
+
+    /**
+     * The number of solver iterations determines quality of the constraints in the world. The more iterations, the more correct simulation. More iterations need more computations though. If you have a large gravity force in your world, you will need more iterations.
+     * @property iterations
+     * @type {Number}
+     * @todo write more about solver and iterations in the wiki
+     */
+    this.iterations = 10;
+
+    /**
+     * When tolerance is reached, the system is assumed to be converged.
+     * @property tolerance
+     * @type {Number}
+     */
+    this.tolerance = 1e-7;
+}
+GSSolver.prototype = new Solver();
+
+var GSSolver_solve_lambda = []; // Just temporary number holders that we want to reuse each solve.
+var GSSolver_solve_invCs = [];
+var GSSolver_solve_Bs = [];
+GSSolver.prototype.solve = function(dt,world){
+    var iter = 0,
+        maxIter = this.iterations,
+        tolSquared = this.tolerance*this.tolerance,
+        equations = this.equations,
+        Neq = equations.length,
+        bodies = world.bodies,
+        Nbodies = bodies.length,
+        h = dt,
+        q, B, invC, deltalambda, deltalambdaTot, GWlambda, lambdaj;
+
+    // Update solve mass
+    if(Neq !== 0){
+        for(var i=0; i!==Nbodies; i++){
+            bodies[i].updateSolveMassProperties();
+        }
+    }
+
+    // Things that does not change during iteration can be computed once
+    var invCs = GSSolver_solve_invCs,
+        Bs = GSSolver_solve_Bs,
+        lambda = GSSolver_solve_lambda;
+    invCs.length = Neq;
+    Bs.length = Neq;
+    lambda.length = Neq;
+    for(var i=0; i!==Neq; i++){
+        var c = equations[i];
+        lambda[i] = 0.0;
+        Bs[i] = c.computeB(h);
+        invCs[i] = 1.0 / c.computeC();
+    }
+
+    if(Neq !== 0){
+
+        // Reset vlambda
+        for(var i=0; i!==Nbodies; i++){
+            var b=bodies[i],
+                vlambda=b.vlambda,
+                wlambda=b.wlambda;
+            vlambda.set(0,0,0);
+            wlambda.set(0,0,0);
+        }
+
+        // Iterate over equations
+        for(iter=0; iter!==maxIter; iter++){
+
+            // Accumulate the total error for each iteration.
+            deltalambdaTot = 0.0;
+
+            for(var j=0; j!==Neq; j++){
+
+                var c = equations[j];
+
+                // Compute iteration
+                B = Bs[j];
+                invC = invCs[j];
+                lambdaj = lambda[j];
+                GWlambda = c.computeGWlambda();
+                deltalambda = invC * ( B - GWlambda - c.eps * lambdaj );
+
+                // Clamp if we are not within the min/max interval
+                if(lambdaj + deltalambda < c.minForce){
+                    deltalambda = c.minForce - lambdaj;
+                } else if(lambdaj + deltalambda > c.maxForce){
+                    deltalambda = c.maxForce - lambdaj;
+                }
+                lambda[j] += deltalambda;
+
+                deltalambdaTot += deltalambda > 0.0 ? deltalambda : -deltalambda; // abs(deltalambda)
+
+                c.addToWlambda(deltalambda);
+            }
+
+            // If the total error is small enough - stop iterate
+            if(deltalambdaTot*deltalambdaTot < tolSquared){
+                break;
+            }
+        }
+
+        // Add result to velocity
+        for(var i=0; i!==Nbodies; i++){
+            var b=bodies[i],
+                v=b.velocity,
+                w=b.angularVelocity;
+
+            b.vlambda.vmul(b.linearFactor, b.vlambda);
+            v.vadd(b.vlambda, v);
+
+            b.wlambda.vmul(b.angularFactor, b.wlambda);
+            w.vadd(b.wlambda, w);
+        }
+
+        // Set the .multiplier property of each equation
+        var l = equations.length;
+        var invDt = 1 / h;
+        while(l--){
+            equations[l].multiplier = lambda[l] * invDt;
+        }
+    }
+
+    return iter;
+};
+
+},{"../math/Quaternion":44,"../math/Vec3":46,"./Solver":63}],63:[function(require,module,exports){
+module.exports = Solver;
+
+/**
+ * Constraint equation solver base class.
+ * @class Solver
+ * @constructor
+ * @author schteppe / https://github.com/schteppe
+ */
+function Solver(){
+    /**
+     * All equations to be solved
+     * @property {Array} equations
+     */
+    this.equations = [];
+}
+
+/**
+ * Should be implemented in subclasses!
+ * @method solve
+ * @param  {Number} dt
+ * @param  {World} world
+ */
+Solver.prototype.solve = function(dt,world){
+    // Should return the number of iterations done!
+    return 0;
+};
+
+/**
+ * Add an equation
+ * @method addEquation
+ * @param {Equation} eq
+ */
+Solver.prototype.addEquation = function(eq){
+    if (eq.enabled) {
+        this.equations.push(eq);
+    }
+};
+
+/**
+ * Remove an equation
+ * @method removeEquation
+ * @param {Equation} eq
+ */
+Solver.prototype.removeEquation = function(eq){
+    var eqs = this.equations;
+    var i = eqs.indexOf(eq);
+    if(i !== -1){
+        eqs.splice(i,1);
+    }
+};
+
+/**
+ * Add all equations
+ * @method removeAllEquations
+ */
+Solver.prototype.removeAllEquations = function(){
+    this.equations.length = 0;
+};
+
+
+},{}],64:[function(require,module,exports){
+module.exports = SplitSolver;
+
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var Solver = require('./Solver');
+var Body = require('../objects/Body');
+
+/**
+ * Splits the equations into islands and solves them independently. Can improve performance.
+ * @class SplitSolver
+ * @constructor
+ * @extends Solver
+ * @param {Solver} subsolver
+ */
+function SplitSolver(subsolver){
+    Solver.call(this);
+    this.iterations = 10;
+    this.tolerance = 1e-7;
+    this.subsolver = subsolver;
+    this.nodes = [];
+    this.nodePool = [];
+
+    // Create needed nodes, reuse if possible
+    while(this.nodePool.length < 128){
+        this.nodePool.push(this.createNode());
+    }
+}
+SplitSolver.prototype = new Solver();
+
+// Returns the number of subsystems
+var SplitSolver_solve_nodes = []; // All allocated node objects
+var SplitSolver_solve_nodePool = []; // All allocated node objects
+var SplitSolver_solve_eqs = [];   // Temp array
+var SplitSolver_solve_bds = [];   // Temp array
+var SplitSolver_solve_dummyWorld = {bodies:[]}; // Temp object
+
+var STATIC = Body.STATIC;
+function getUnvisitedNode(nodes){
+    var Nnodes = nodes.length;
+    for(var i=0; i!==Nnodes; i++){
+        var node = nodes[i];
+        if(!node.visited && !(node.body.type & STATIC)){
+            return node;
+        }
+    }
+    return false;
+}
+
+var queue = [];
+function bfs(root,visitFunc,bds,eqs){
+    queue.push(root);
+    root.visited = true;
+    visitFunc(root,bds,eqs);
+    while(queue.length) {
+        var node = queue.pop();
+        // Loop over unvisited child nodes
+        var child;
+        while((child = getUnvisitedNode(node.children))) {
+            child.visited = true;
+            visitFunc(child,bds,eqs);
+            queue.push(child);
+        }
+    }
+}
+
+function visitFunc(node,bds,eqs){
+    bds.push(node.body);
+    var Neqs = node.eqs.length;
+    for(var i=0; i!==Neqs; i++){
+        var eq = node.eqs[i];
+        if(eqs.indexOf(eq) === -1){
+            eqs.push(eq);
+        }
+    }
+}
+
+SplitSolver.prototype.createNode = function(){
+    return { body:null, children:[], eqs:[], visited:false };
+};
+
+/**
+ * Solve the subsystems
+ * @method solve
+ * @param  {Number} dt
+ * @param  {World} world
+ */
+SplitSolver.prototype.solve = function(dt,world){
+    var nodes=SplitSolver_solve_nodes,
+        nodePool=this.nodePool,
+        bodies=world.bodies,
+        equations=this.equations,
+        Neq=equations.length,
+        Nbodies=bodies.length,
+        subsolver=this.subsolver;
+
+    // Create needed nodes, reuse if possible
+    while(nodePool.length < Nbodies){
+        nodePool.push(this.createNode());
+    }
+    nodes.length = Nbodies;
+    for (var i = 0; i < Nbodies; i++) {
+        nodes[i] = nodePool[i];
+    }
+
+    // Reset node values
+    for(var i=0; i!==Nbodies; i++){
+        var node = nodes[i];
+        node.body = bodies[i];
+        node.children.length = 0;
+        node.eqs.length = 0;
+        node.visited = false;
+    }
+    for(var k=0; k!==Neq; k++){
+        var eq=equations[k],
+            i=bodies.indexOf(eq.bi),
+            j=bodies.indexOf(eq.bj),
+            ni=nodes[i],
+            nj=nodes[j];
+        ni.children.push(nj);
+        ni.eqs.push(eq);
+        nj.children.push(ni);
+        nj.eqs.push(eq);
+    }
+
+    var child, n=0, eqs=SplitSolver_solve_eqs;
+
+    subsolver.tolerance = this.tolerance;
+    subsolver.iterations = this.iterations;
+
+    var dummyWorld = SplitSolver_solve_dummyWorld;
+    while((child = getUnvisitedNode(nodes))){
+        eqs.length = 0;
+        dummyWorld.bodies.length = 0;
+        bfs(child, visitFunc, dummyWorld.bodies, eqs);
+
+        var Neqs = eqs.length;
+
+        eqs = eqs.sort(sortById);
+
+        for(var i=0; i!==Neqs; i++){
+            subsolver.addEquation(eqs[i]);
+        }
+
+        var iter = subsolver.solve(dt,dummyWorld);
+        subsolver.removeAllEquations();
+        n++;
+    }
+
+    return n;
+};
+
+function sortById(a, b){
+    return b.id - a.id;
+}
+},{"../math/Quaternion":44,"../math/Vec3":46,"../objects/Body":47,"./Solver":63}],65:[function(require,module,exports){
+/**
+ * Base class for objects that dispatches events.
+ * @class EventTarget
+ * @constructor
+ */
+var EventTarget = function () {
+
+};
+
+module.exports = EventTarget;
+
+EventTarget.prototype = {
+    constructor: EventTarget,
+
+    /**
+     * Add an event listener
+     * @method addEventListener
+     * @param  {String} type
+     * @param  {Function} listener
+     * @return {EventTarget} The self object, for chainability.
+     */
+    addEventListener: function ( type, listener ) {
+        if ( this._listeners === undefined ){ this._listeners = {}; }
+        var listeners = this._listeners;
+        if ( listeners[ type ] === undefined ) {
+            listeners[ type ] = [];
+        }
+        if ( listeners[ type ].indexOf( listener ) === - 1 ) {
+            listeners[ type ].push( listener );
+        }
+        return this;
+    },
+
+    /**
+     * Check if an event listener is added
+     * @method hasEventListener
+     * @param  {String} type
+     * @param  {Function} listener
+     * @return {Boolean}
+     */
+    hasEventListener: function ( type, listener ) {
+        if ( this._listeners === undefined ){ return false; }
+        var listeners = this._listeners;
+        if ( listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== - 1 ) {
+            return true;
+        }
+        return false;
+    },
+
+    /**
+     * Check if any event listener of the given type is added
+     * @method hasAnyEventListener
+     * @param  {String} type
+     * @return {Boolean}
+     */
+    hasAnyEventListener: function ( type ) {
+        if ( this._listeners === undefined ){ return false; }
+        var listeners = this._listeners;
+        return ( listeners[ type ] !== undefined );
+    },
+
+    /**
+     * Remove an event listener
+     * @method removeEventListener
+     * @param  {String} type
+     * @param  {Function} listener
+     * @return {EventTarget} The self object, for chainability.
+     */
+    removeEventListener: function ( type, listener ) {
+        if ( this._listeners === undefined ){ return this; }
+        var listeners = this._listeners;
+        if ( listeners[type] === undefined ){ return this; }
+        var index = listeners[ type ].indexOf( listener );
+        if ( index !== - 1 ) {
+            listeners[ type ].splice( index, 1 );
+        }
+        return this;
+    },
+
+    /**
+     * Emit an event.
+     * @method dispatchEvent
+     * @param  {Object} event
+     * @param  {String} event.type
+     * @return {EventTarget} The self object, for chainability.
+     */
+    dispatchEvent: function ( event ) {
+        if ( this._listeners === undefined ){ return this; }
+        var listeners = this._listeners;
+        var listenerArray = listeners[ event.type ];
+        if ( listenerArray !== undefined ) {
+            event.target = this;
+            for ( var i = 0, l = listenerArray.length; i < l; i ++ ) {
+                listenerArray[ i ].call( this, event );
+            }
+        }
+        return this;
+    }
+};
+
+},{}],66:[function(require,module,exports){
+var AABB = require('../collision/AABB');
+var Vec3 = require('../math/Vec3');
+
+module.exports = Octree;
+
+/**
+ * @class OctreeNode
+ * @param {object} [options]
+ * @param {Octree} [options.root]
+ * @param {AABB} [options.aabb]
+ */
+function OctreeNode(options){
+    options = options || {};
+
+    /**
+     * The root node
+     * @property {OctreeNode} root
+     */
+    this.root = options.root || null;
+
+    /**
+     * Boundary of this node
+     * @property {AABB} aabb
+     */
+    this.aabb = options.aabb ? options.aabb.clone() : new AABB();
+
+    /**
+     * Contained data at the current node level.
+     * @property {Array} data
+     */
+    this.data = [];
+
+    /**
+     * Children to this node
+     * @property {Array} children
+     */
+    this.children = [];
+}
+
+/**
+ * @class Octree
+ * @param {AABB} aabb The total AABB of the tree
+ * @param {object} [options]
+ * @param {number} [options.maxDepth=8]
+ * @extends OctreeNode
+ */
+function Octree(aabb, options){
+    options = options || {};
+    options.root = null;
+    options.aabb = aabb;
+    OctreeNode.call(this, options);
+
+    /**
+     * Maximum subdivision depth
+     * @property {number} maxDepth
+     */
+    this.maxDepth = typeof(options.maxDepth) !== 'undefined' ? options.maxDepth : 8;
+}
+Octree.prototype = new OctreeNode();
+
+OctreeNode.prototype.reset = function(aabb, options){
+    this.children.length = this.data.length = 0;
+};
+
+/**
+ * Insert data into this node
+ * @method insert
+ * @param  {AABB} aabb
+ * @param  {object} elementData
+ * @return {boolean} True if successful, otherwise false
+ */
+OctreeNode.prototype.insert = function(aabb, elementData, level){
+    var nodeData = this.data;
+    level = level || 0;
+
+    // Ignore objects that do not belong in this node
+    if (!this.aabb.contains(aabb)){
+        return false; // object cannot be added
+    }
+
+    var children = this.children;
+
+    if(level < (this.maxDepth || this.root.maxDepth)){
+        // Subdivide if there are no children yet
+        var subdivided = false;
+        if (!children.length){
+            this.subdivide();
+            subdivided = true;
+        }
+
+        // add to whichever node will accept it
+        for (var i = 0; i !== 8; i++) {
+            if (children[i].insert(aabb, elementData, level + 1)){
+                return true;
+            }
+        }
+
+        if(subdivided){
+            // No children accepted! Might as well just remove em since they contain none
+            children.length = 0;
+        }
+    }
+
+    // Too deep, or children didnt want it. add it in current node
+    nodeData.push(elementData);
+
+    return true;
+};
+
+var halfDiagonal = new Vec3();
+
+/**
+ * Create 8 equally sized children nodes and put them in the .children array.
+ * @method subdivide
+ */
+OctreeNode.prototype.subdivide = function() {
+    var aabb = this.aabb;
+    var l = aabb.lowerBound;
+    var u = aabb.upperBound;
+
+    var children = this.children;
+
+    children.push(
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0,0,0) }) }),
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1,0,0) }) }),
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1,1,0) }) }),
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1,1,1) }) }),
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0,1,1) }) }),
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0,0,1) }) }),
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(1,0,1) }) }),
+        new OctreeNode({ aabb: new AABB({ lowerBound: new Vec3(0,1,0) }) })
+    );
+
+    u.vsub(l, halfDiagonal);
+    halfDiagonal.scale(0.5, halfDiagonal);
+
+    var root = this.root || this;
+
+    for (var i = 0; i !== 8; i++) {
+        var child = children[i];
+
+        // Set current node as root
+        child.root = root;
+
+        // Compute bounds
+        var lowerBound = child.aabb.lowerBound;
+        lowerBound.x *= halfDiagonal.x;
+        lowerBound.y *= halfDiagonal.y;
+        lowerBound.z *= halfDiagonal.z;
+
+        lowerBound.vadd(l, lowerBound);
+
+        // Upper bound is always lower bound + halfDiagonal
+        lowerBound.vadd(halfDiagonal, child.aabb.upperBound);
+    }
+};
+
+/**
+ * Get all data, potentially within an AABB
+ * @method aabbQuery
+ * @param  {AABB} aabb
+ * @param  {array} result
+ * @return {array} The "result" object
+ */
+OctreeNode.prototype.aabbQuery = function(aabb, result) {
+
+    var nodeData = this.data;
+
+    // abort if the range does not intersect this node
+    // if (!this.aabb.overlaps(aabb)){
+    //     return result;
+    // }
+
+    // Add objects at this level
+    // Array.prototype.push.apply(result, nodeData);
+
+    // Add child data
+    // @todo unwrap recursion into a queue / loop, that's faster in JS
+    var children = this.children;
+
+
+    // for (var i = 0, N = this.children.length; i !== N; i++) {
+    //     children[i].aabbQuery(aabb, result);
+    // }
+
+    var queue = [this];
+    while (queue.length) {
+        var node = queue.pop();
+        if (node.aabb.overlaps(aabb)){
+            Array.prototype.push.apply(result, node.data);
+        }
+        Array.prototype.push.apply(queue, node.children);
+    }
+
+    return result;
+};
+
+var tmpAABB = new AABB();
+
+/**
+ * Get all data, potentially intersected by a ray.
+ * @method rayQuery
+ * @param  {Ray} ray
+ * @param  {Transform} treeTransform
+ * @param  {array} result
+ * @return {array} The "result" object
+ */
+OctreeNode.prototype.rayQuery = function(ray, treeTransform, result) {
+
+    // Use aabb query for now.
+    // @todo implement real ray query which needs less lookups
+    ray.getAABB(tmpAABB);
+    tmpAABB.toLocalFrame(treeTransform, tmpAABB);
+    this.aabbQuery(tmpAABB, result);
+
+    return result;
+};
+
+/**
+ * @method removeEmptyNodes
+ */
+OctreeNode.prototype.removeEmptyNodes = function() {
+    var queue = [this];
+    while (queue.length) {
+        var node = queue.pop();
+        for (var i = node.children.length - 1; i >= 0; i--) {
+            if(!node.children[i].data.length){
+                node.children.splice(i, 1);
+            }
+        }
+        Array.prototype.push.apply(queue, node.children);
+    }
+};
+
+},{"../collision/AABB":18,"../math/Vec3":46}],67:[function(require,module,exports){
+module.exports = Pool;
+
+/**
+ * For pooling objects that can be reused.
+ * @class Pool
+ * @constructor
+ */
+function Pool(){
+    /**
+     * The pooled objects
+     * @property {Array} objects
+     */
+    this.objects = [];
+
+    /**
+     * Constructor of the objects
+     * @property {mixed} type
+     */
+    this.type = Object;
+}
+
+/**
+ * Release an object after use
+ * @method release
+ * @param {Object} obj
+ */
+Pool.prototype.release = function(){
+    var Nargs = arguments.length;
+    for(var i=0; i!==Nargs; i++){
+        this.objects.push(arguments[i]);
+    }
+    return this;
+};
+
+/**
+ * Get an object
+ * @method get
+ * @return {mixed}
+ */
+Pool.prototype.get = function(){
+    if(this.objects.length===0){
+        return this.constructObject();
+    } else {
+        return this.objects.pop();
+    }
+};
+
+/**
+ * Construct an object. Should be implmented in each subclass.
+ * @method constructObject
+ * @return {mixed}
+ */
+Pool.prototype.constructObject = function(){
+    throw new Error("constructObject() not implemented in this Pool subclass yet!");
+};
+
+/**
+ * @method resize
+ * @param {number} size
+ * @return {Pool} Self, for chaining
+ */
+Pool.prototype.resize = function (size) {
+    var objects = this.objects;
+
+    while (objects.length > size) {
+        objects.pop();
+    }
+
+    while (objects.length < size) {
+        objects.push(this.constructObject());
+    }
+
+    return this;
+};
+
+
+},{}],68:[function(require,module,exports){
+module.exports = TupleDictionary;
+
+/**
+ * @class TupleDictionary
+ * @constructor
+ */
+function TupleDictionary() {
+
+    /**
+     * The data storage
+     * @property data
+     * @type {Object}
+     */
+    this.data = { keys:[] };
+}
+
+/**
+ * @method get
+ * @param  {Number} i
+ * @param  {Number} j
+ * @return {Number}
+ */
+TupleDictionary.prototype.get = function(i, j) {
+    if (i > j) {
+        // swap
+        var temp = j;
+        j = i;
+        i = temp;
+    }
+    return this.data[i+'-'+j];
+};
+
+/**
+ * @method set
+ * @param  {Number} i
+ * @param  {Number} j
+ * @param {Number} value
+ */
+TupleDictionary.prototype.set = function(i, j, value) {
+    if (i > j) {
+        var temp = j;
+        j = i;
+        i = temp;
+    }
+    var key = i+'-'+j;
+
+    // Check if key already exists
+    if(!this.get(i,j)){
+        this.data.keys.push(key);
+    }
+
+    this.data[key] = value;
+};
+
+/**
+ * @method reset
+ */
+TupleDictionary.prototype.reset = function() {
+    var data = this.data,
+        keys = data.keys;
+    while(keys.length > 0){
+        var key = keys.pop();
+        delete data[key];
+    }
+};
+
+},{}],69:[function(require,module,exports){
+function Utils(){}
+
+module.exports = Utils;
+
+/**
+ * Extend an options object with default values.
+ * @static
+ * @method defaults
+ * @param  {object} options The options object. May be falsy: in this case, a new object is created and returned.
+ * @param  {object} defaults An object containing default values.
+ * @return {object} The modified options object.
+ */
+Utils.defaults = function(options, defaults){
+    options = options || {};
+
+    for(var key in defaults){
+        if(!(key in options)){
+            options[key] = defaults[key];
+        }
+    }
+
+    return options;
+};
+
+},{}],70:[function(require,module,exports){
+module.exports = Vec3Pool;
+
+var Vec3 = require('../math/Vec3');
+var Pool = require('./Pool');
+
+/**
+ * @class Vec3Pool
+ * @constructor
+ * @extends Pool
+ */
+function Vec3Pool(){
+    Pool.call(this);
+    this.type = Vec3;
+}
+Vec3Pool.prototype = new Pool();
+
+/**
+ * Construct a vector
+ * @method constructObject
+ * @return {Vec3}
+ */
+Vec3Pool.prototype.constructObject = function(){
+    return new Vec3();
+};
+
+},{"../math/Vec3":46,"./Pool":67}],71:[function(require,module,exports){
+module.exports = Narrowphase;
+
+var AABB = require('../collision/AABB');
+var Body = require('../objects/Body');
+var Shape = require('../shapes/Shape');
+var Ray = require('../collision/Ray');
+var Vec3 = require('../math/Vec3');
+var Transform = require('../math/Transform');
+var ConvexPolyhedron = require('../shapes/ConvexPolyhedron');
+var Quaternion = require('../math/Quaternion');
+var Solver = require('../solver/Solver');
+var Vec3Pool = require('../utils/Vec3Pool');
+var ContactEquation = require('../equations/ContactEquation');
+var FrictionEquation = require('../equations/FrictionEquation');
+
+/**
+ * Helper class for the World. Generates ContactEquations.
+ * @class Narrowphase
+ * @constructor
+ * @todo Sphere-ConvexPolyhedron contacts
+ * @todo Contact reduction
+ * @todo  should move methods to prototype
+ */
+function Narrowphase(world){
+
+    /**
+     * Internal storage of pooled contact points.
+     * @property {Array} contactPointPool
+     */
+    this.contactPointPool = [];
+
+    this.frictionEquationPool = [];
+
+    this.result = [];
+    this.frictionResult = [];
+
+    /**
+     * Pooled vectors.
+     * @property {Vec3Pool} v3pool
+     */
+    this.v3pool = new Vec3Pool();
+
+    this.world = world;
+    this.currentContactMaterial = null;
+
+    /**
+     * @property {Boolean} enableFrictionReduction
+     */
+    this.enableFrictionReduction = false;
+}
+
+/**
+ * Make a contact object, by using the internal pool or creating a new one.
+ * @method createContactEquation
+ * @param {Body} bi
+ * @param {Body} bj
+ * @param {Shape} si
+ * @param {Shape} sj
+ * @param {Shape} overrideShapeA
+ * @param {Shape} overrideShapeB
+ * @return {ContactEquation}
+ */
+Narrowphase.prototype.createContactEquation = function(bi, bj, si, sj, overrideShapeA, overrideShapeB){
+    var c;
+    if(this.contactPointPool.length){
+        c = this.contactPointPool.pop();
+        c.bi = bi;
+        c.bj = bj;
+    } else {
+        c = new ContactEquation(bi, bj);
+    }
+
+    c.enabled = bi.collisionResponse && bj.collisionResponse && si.collisionResponse && sj.collisionResponse;
+
+    var cm = this.currentContactMaterial;
+
+    c.restitution = cm.restitution;
+
+    c.setSpookParams(
+        cm.contactEquationStiffness,
+        cm.contactEquationRelaxation,
+        this.world.dt
+    );
+
+    var matA = si.material || bi.material;
+    var matB = sj.material || bj.material;
+    if(matA && matB && matA.restitution >= 0 && matB.restitution >= 0){
+        c.restitution = matA.restitution * matB.restitution;
+    }
+
+    c.si = overrideShapeA || si;
+    c.sj = overrideShapeB || sj;
+
+    return c;
+};
+
+Narrowphase.prototype.createFrictionEquationsFromContact = function(contactEquation, outArray){
+    var bodyA = contactEquation.bi;
+    var bodyB = contactEquation.bj;
+    var shapeA = contactEquation.si;
+    var shapeB = contactEquation.sj;
+
+    var world = this.world;
+    var cm = this.currentContactMaterial;
+
+    // If friction or restitution were specified in the material, use them
+    var friction = cm.friction;
+    var matA = shapeA.material || bodyA.material;
+    var matB = shapeB.material || bodyB.material;
+    if(matA && matB && matA.friction >= 0 && matB.friction >= 0){
+        friction = matA.friction * matB.friction;
+    }
+
+    if(friction > 0){
+
+        // Create 2 tangent equations
+        var mug = friction * world.gravity.length();
+        var reducedMass = (bodyA.invMass + bodyB.invMass);
+        if(reducedMass > 0){
+            reducedMass = 1/reducedMass;
+        }
+        var pool = this.frictionEquationPool;
+        var c1 = pool.length ? pool.pop() : new FrictionEquation(bodyA,bodyB,mug*reducedMass);
+        var c2 = pool.length ? pool.pop() : new FrictionEquation(bodyA,bodyB,mug*reducedMass);
+
+        c1.bi = c2.bi = bodyA;
+        c1.bj = c2.bj = bodyB;
+        c1.minForce = c2.minForce = -mug*reducedMass;
+        c1.maxForce = c2.maxForce = mug*reducedMass;
+
+        // Copy over the relative vectors
+        c1.ri.copy(contactEquation.ri);
+        c1.rj.copy(contactEquation.rj);
+        c2.ri.copy(contactEquation.ri);
+        c2.rj.copy(contactEquation.rj);
+
+        // Construct tangents
+        contactEquation.ni.tangents(c1.t, c2.t);
+
+        // Set spook params
+        c1.setSpookParams(cm.frictionEquationStiffness, cm.frictionEquationRelaxation, world.dt);
+        c2.setSpookParams(cm.frictionEquationStiffness, cm.frictionEquationRelaxation, world.dt);
+
+        c1.enabled = c2.enabled = contactEquation.enabled;
+
+        outArray.push(c1, c2);
+
+        return true;
+    }
+
+    return false;
+};
+
+var averageNormal = new Vec3();
+var averageContactPointA = new Vec3();
+var averageContactPointB = new Vec3();
+
+// Take the average N latest contact point on the plane.
+Narrowphase.prototype.createFrictionFromAverage = function(numContacts){
+    // The last contactEquation
+    var c = this.result[this.result.length - 1];
+
+    // Create the result: two "average" friction equations
+    if (!this.createFrictionEquationsFromContact(c, this.frictionResult) || numContacts === 1) {
+        return;
+    }
+
+    var f1 = this.frictionResult[this.frictionResult.length - 2];
+    var f2 = this.frictionResult[this.frictionResult.length - 1];
+
+    averageNormal.setZero();
+    averageContactPointA.setZero();
+    averageContactPointB.setZero();
+
+    var bodyA = c.bi;
+    var bodyB = c.bj;
+    for(var i=0; i!==numContacts; i++){
+        c = this.result[this.result.length - 1 - i];
+        if(c.bodyA !== bodyA){
+            averageNormal.vadd(c.ni, averageNormal);
+            averageContactPointA.vadd(c.ri, averageContactPointA);
+            averageContactPointB.vadd(c.rj, averageContactPointB);
+        } else {
+            averageNormal.vsub(c.ni, averageNormal);
+            averageContactPointA.vadd(c.rj, averageContactPointA);
+            averageContactPointB.vadd(c.ri, averageContactPointB);
+        }
+    }
+
+    var invNumContacts = 1 / numContacts;
+    averageContactPointA.scale(invNumContacts, f1.ri);
+    averageContactPointB.scale(invNumContacts, f1.rj);
+    f2.ri.copy(f1.ri); // Should be the same
+    f2.rj.copy(f1.rj);
+    averageNormal.normalize();
+    averageNormal.tangents(f1.t, f2.t);
+    // return eq;
+};
+
+
+var tmpVec1 = new Vec3();
+var tmpVec2 = new Vec3();
+var tmpQuat1 = new Quaternion();
+var tmpQuat2 = new Quaternion();
+
+/**
+ * Generate all contacts between a list of body pairs
+ * @method getContacts
+ * @param {array} p1 Array of body indices
+ * @param {array} p2 Array of body indices
+ * @param {World} world
+ * @param {array} result Array to store generated contacts
+ * @param {array} oldcontacts Optional. Array of reusable contact objects
+ */
+Narrowphase.prototype.getContacts = function(p1, p2, world, result, oldcontacts, frictionResult, frictionPool){
+    // Save old contact objects
+    this.contactPointPool = oldcontacts;
+    this.frictionEquationPool = frictionPool;
+    this.result = result;
+    this.frictionResult = frictionResult;
+
+    var qi = tmpQuat1;
+    var qj = tmpQuat2;
+    var xi = tmpVec1;
+    var xj = tmpVec2;
+
+    for(var k=0, N=p1.length; k!==N; k++){
+
+        // Get current collision bodies
+        var bi = p1[k],
+            bj = p2[k];
+
+        // Get contact material
+        var bodyContactMaterial = null;
+        if(bi.material && bj.material){
+            bodyContactMaterial = world.getContactMaterial(bi.material,bj.material) || null;
+        }
+
+        var justTest = (
+            (
+                (bi.type & Body.KINEMATIC) && (bj.type & Body.STATIC)
+            ) || (
+                (bi.type & Body.STATIC) && (bj.type & Body.KINEMATIC)
+            ) || (
+                (bi.type & Body.KINEMATIC) && (bj.type & Body.KINEMATIC)
+            )
+        );
+
+        for (var i = 0; i < bi.shapes.length; i++) {
+            bi.quaternion.mult(bi.shapeOrientations[i], qi);
+            bi.quaternion.vmult(bi.shapeOffsets[i], xi);
+            xi.vadd(bi.position, xi);
+            var si = bi.shapes[i];
+
+            for (var j = 0; j < bj.shapes.length; j++) {
+
+                // Compute world transform of shapes
+                bj.quaternion.mult(bj.shapeOrientations[j], qj);
+                bj.quaternion.vmult(bj.shapeOffsets[j], xj);
+                xj.vadd(bj.position, xj);
+                var sj = bj.shapes[j];
+
+                if(xi.distanceTo(xj) > si.boundingSphereRadius + sj.boundingSphereRadius){
+                    continue;
+                }
+
+                // Get collision material
+                var shapeContactMaterial = null;
+                if(si.material && sj.material){
+                    shapeContactMaterial = world.getContactMaterial(si.material,sj.material) || null;
+                }
+
+                this.currentContactMaterial = shapeContactMaterial || bodyContactMaterial || world.defaultContactMaterial;
+
+                // Get contacts
+                var resolver = this[si.type | sj.type];
+                if(resolver){
+                    var retval = false;
+                    if (si.type < sj.type) {
+                        retval = resolver.call(this, si, sj, xi, xj, qi, qj, bi, bj, si, sj, justTest);
+                    } else {
+                        retval = resolver.call(this, sj, si, xj, xi, qj, qi, bj, bi, si, sj, justTest);
+                    }
+
+                    if(retval && justTest){
+                        // Register overlap
+                        world.shapeOverlapKeeper.set(si.id, sj.id);
+                        world.bodyOverlapKeeper.set(bi.id, bj.id);
+                    }
+                }
+            }
+        }
+    }
+};
+
+var numWarnings = 0;
+var maxWarnings = 10;
+
+function warn(msg){
+    if(numWarnings > maxWarnings){
+        return;
+    }
+
+    numWarnings++;
+
+    console.warn(msg);
+}
+
+Narrowphase.prototype[Shape.types.BOX | Shape.types.BOX] =
+Narrowphase.prototype.boxBox = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    si.convexPolyhedronRepresentation.material = si.material;
+    sj.convexPolyhedronRepresentation.material = sj.material;
+    si.convexPolyhedronRepresentation.collisionResponse = si.collisionResponse;
+    sj.convexPolyhedronRepresentation.collisionResponse = sj.collisionResponse;
+    return this.convexConvex(si.convexPolyhedronRepresentation,sj.convexPolyhedronRepresentation,xi,xj,qi,qj,bi,bj,si,sj,justTest);
+};
+
+Narrowphase.prototype[Shape.types.BOX | Shape.types.CONVEXPOLYHEDRON] =
+Narrowphase.prototype.boxConvex = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    si.convexPolyhedronRepresentation.material = si.material;
+    si.convexPolyhedronRepresentation.collisionResponse = si.collisionResponse;
+    return this.convexConvex(si.convexPolyhedronRepresentation,sj,xi,xj,qi,qj,bi,bj,si,sj,justTest);
+};
+
+Narrowphase.prototype[Shape.types.BOX | Shape.types.PARTICLE] =
+Narrowphase.prototype.boxParticle = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    si.convexPolyhedronRepresentation.material = si.material;
+    si.convexPolyhedronRepresentation.collisionResponse = si.collisionResponse;
+    return this.convexParticle(si.convexPolyhedronRepresentation,sj,xi,xj,qi,qj,bi,bj,si,sj,justTest);
+};
+
+/**
+ * @method sphereSphere
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.SPHERE] =
+Narrowphase.prototype.sphereSphere = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    if(justTest){
+        return xi.distanceSquared(xj) < Math.pow(si.radius + sj.radius, 2);
+    }
+
+    // We will have only one contact in this case
+    var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+
+    // Contact normal
+    xj.vsub(xi, r.ni);
+    r.ni.normalize();
+
+    // Contact point locations
+    r.ri.copy(r.ni);
+    r.rj.copy(r.ni);
+    r.ri.mult(si.radius, r.ri);
+    r.rj.mult(-sj.radius, r.rj);
+
+    r.ri.vadd(xi, r.ri);
+    r.ri.vsub(bi.position, r.ri);
+
+    r.rj.vadd(xj, r.rj);
+    r.rj.vsub(bj.position, r.rj);
+
+    this.result.push(r);
+
+    this.createFrictionEquationsFromContact(r, this.frictionResult);
+};
+
+/**
+ * @method planeTrimesh
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+var planeTrimesh_normal = new Vec3();
+var planeTrimesh_relpos = new Vec3();
+var planeTrimesh_projected = new Vec3();
+Narrowphase.prototype[Shape.types.PLANE | Shape.types.TRIMESH] =
+Narrowphase.prototype.planeTrimesh = function(
+    planeShape,
+    trimeshShape,
+    planePos,
+    trimeshPos,
+    planeQuat,
+    trimeshQuat,
+    planeBody,
+    trimeshBody,
+    rsi,
+    rsj,
+    justTest
+){
+    // Make contacts!
+    var v = new Vec3();
+
+    var normal = planeTrimesh_normal;
+    normal.set(0,0,1);
+    planeQuat.vmult(normal,normal); // Turn normal according to plane
+
+    for(var i=0; i<trimeshShape.vertices.length / 3; i++){
+
+        // Get world vertex from trimesh
+        trimeshShape.getVertex(i, v);
+
+        // Safe up
+        var v2 = new Vec3();
+        v2.copy(v);
+        Transform.pointToWorldFrame(trimeshPos, trimeshQuat, v2, v);
+
+        // Check plane side
+        var relpos = planeTrimesh_relpos;
+        v.vsub(planePos, relpos);
+        var dot = normal.dot(relpos);
+
+        if(dot <= 0.0){
+            if(justTest){
+                return true;
+            }
+
+            var r = this.createContactEquation(planeBody,trimeshBody,planeShape,trimeshShape,rsi,rsj);
+
+            r.ni.copy(normal); // Contact normal is the plane normal
+
+            // Get vertex position projected on plane
+            var projected = planeTrimesh_projected;
+            normal.scale(relpos.dot(normal), projected);
+            v.vsub(projected,projected);
+
+            // ri is the projected world position minus plane position
+            r.ri.copy(projected);
+            r.ri.vsub(planeBody.position, r.ri);
+
+            r.rj.copy(v);
+            r.rj.vsub(trimeshBody.position, r.rj);
+
+            // Store result
+            this.result.push(r);
+            this.createFrictionEquationsFromContact(r, this.frictionResult);
+        }
+    }
+};
+
+/**
+ * @method sphereTrimesh
+ * @param  {Shape}      sphereShape
+ * @param  {Shape}      trimeshShape
+ * @param  {Vec3}       spherePos
+ * @param  {Vec3}       trimeshPos
+ * @param  {Quaternion} sphereQuat
+ * @param  {Quaternion} trimeshQuat
+ * @param  {Body}       sphereBody
+ * @param  {Body}       trimeshBody
+ */
+var sphereTrimesh_normal = new Vec3();
+var sphereTrimesh_relpos = new Vec3();
+var sphereTrimesh_projected = new Vec3();
+var sphereTrimesh_v = new Vec3();
+var sphereTrimesh_v2 = new Vec3();
+var sphereTrimesh_edgeVertexA = new Vec3();
+var sphereTrimesh_edgeVertexB = new Vec3();
+var sphereTrimesh_edgeVector = new Vec3();
+var sphereTrimesh_edgeVectorUnit = new Vec3();
+var sphereTrimesh_localSpherePos = new Vec3();
+var sphereTrimesh_tmp = new Vec3();
+var sphereTrimesh_va = new Vec3();
+var sphereTrimesh_vb = new Vec3();
+var sphereTrimesh_vc = new Vec3();
+var sphereTrimesh_localSphereAABB = new AABB();
+var sphereTrimesh_triangles = [];
+Narrowphase.prototype[Shape.types.SPHERE | Shape.types.TRIMESH] =
+Narrowphase.prototype.sphereTrimesh = function (
+    sphereShape,
+    trimeshShape,
+    spherePos,
+    trimeshPos,
+    sphereQuat,
+    trimeshQuat,
+    sphereBody,
+    trimeshBody,
+    rsi,
+    rsj,
+    justTest
+) {
+
+    var edgeVertexA = sphereTrimesh_edgeVertexA;
+    var edgeVertexB = sphereTrimesh_edgeVertexB;
+    var edgeVector = sphereTrimesh_edgeVector;
+    var edgeVectorUnit = sphereTrimesh_edgeVectorUnit;
+    var localSpherePos = sphereTrimesh_localSpherePos;
+    var tmp = sphereTrimesh_tmp;
+    var localSphereAABB = sphereTrimesh_localSphereAABB;
+    var v2 = sphereTrimesh_v2;
+    var relpos = sphereTrimesh_relpos;
+    var triangles = sphereTrimesh_triangles;
+
+    // Convert sphere position to local in the trimesh
+    Transform.pointToLocalFrame(trimeshPos, trimeshQuat, spherePos, localSpherePos);
+
+    // Get the aabb of the sphere locally in the trimesh
+    var sphereRadius = sphereShape.radius;
+    localSphereAABB.lowerBound.set(
+        localSpherePos.x - sphereRadius,
+        localSpherePos.y - sphereRadius,
+        localSpherePos.z - sphereRadius
+    );
+    localSphereAABB.upperBound.set(
+        localSpherePos.x + sphereRadius,
+        localSpherePos.y + sphereRadius,
+        localSpherePos.z + sphereRadius
+    );
+
+    trimeshShape.getTrianglesInAABB(localSphereAABB, triangles);
+    //for (var i = 0; i < trimeshShape.indices.length / 3; i++) triangles.push(i); // All
+
+    // Vertices
+    var v = sphereTrimesh_v;
+    var radiusSquared = sphereShape.radius * sphereShape.radius;
+    for(var i=0; i<triangles.length; i++){
+        for (var j = 0; j < 3; j++) {
+
+            trimeshShape.getVertex(trimeshShape.indices[triangles[i] * 3 + j], v);
+
+            // Check vertex overlap in sphere
+            v.vsub(localSpherePos, relpos);
+
+            if(relpos.norm2() <= radiusSquared){
+
+                // Safe up
+                v2.copy(v);
+                Transform.pointToWorldFrame(trimeshPos, trimeshQuat, v2, v);
+
+                v.vsub(spherePos, relpos);
+
+                if(justTest){
+                    return true;
+                }
+
+                var r = this.createContactEquation(sphereBody,trimeshBody,sphereShape,trimeshShape,rsi,rsj);
+                r.ni.copy(relpos);
+                r.ni.normalize();
+
+                // ri is the vector from sphere center to the sphere surface
+                r.ri.copy(r.ni);
+                r.ri.scale(sphereShape.radius, r.ri);
+                r.ri.vadd(spherePos, r.ri);
+                r.ri.vsub(sphereBody.position, r.ri);
+
+                r.rj.copy(v);
+                r.rj.vsub(trimeshBody.position, r.rj);
+
+                // Store result
+                this.result.push(r);
+                this.createFrictionEquationsFromContact(r, this.frictionResult);
+            }
+        }
+    }
+
+    // Check all edges
+    for(var i=0; i<triangles.length; i++){
+        for (var j = 0; j < 3; j++) {
+
+            trimeshShape.getVertex(trimeshShape.indices[triangles[i] * 3 + j], edgeVertexA);
+            trimeshShape.getVertex(trimeshShape.indices[triangles[i] * 3 + ((j+1)%3)], edgeVertexB);
+            edgeVertexB.vsub(edgeVertexA, edgeVector);
+
+            // Project sphere position to the edge
+            localSpherePos.vsub(edgeVertexB, tmp);
+            var positionAlongEdgeB = tmp.dot(edgeVector);
+
+            localSpherePos.vsub(edgeVertexA, tmp);
+            var positionAlongEdgeA = tmp.dot(edgeVector);
+
+            if(positionAlongEdgeA > 0 && positionAlongEdgeB < 0){
+
+                // Now check the orthogonal distance from edge to sphere center
+                localSpherePos.vsub(edgeVertexA, tmp);
+
+                edgeVectorUnit.copy(edgeVector);
+                edgeVectorUnit.normalize();
+                positionAlongEdgeA = tmp.dot(edgeVectorUnit);
+
+                edgeVectorUnit.scale(positionAlongEdgeA, tmp);
+                tmp.vadd(edgeVertexA, tmp);
+
+                // tmp is now the sphere center position projected to the edge, defined locally in the trimesh frame
+                var dist = tmp.distanceTo(localSpherePos);
+                if(dist < sphereShape.radius){
+
+                    if(justTest){
+                        return true;
+                    }
+
+                    var r = this.createContactEquation(sphereBody, trimeshBody, sphereShape, trimeshShape,rsi,rsj);
+
+                    tmp.vsub(localSpherePos, r.ni);
+                    r.ni.normalize();
+                    r.ni.scale(sphereShape.radius, r.ri);
+
+                    Transform.pointToWorldFrame(trimeshPos, trimeshQuat, tmp, tmp);
+                    tmp.vsub(trimeshBody.position, r.rj);
+
+                    Transform.vectorToWorldFrame(trimeshQuat, r.ni, r.ni);
+                    Transform.vectorToWorldFrame(trimeshQuat, r.ri, r.ri);
+
+                    this.result.push(r);
+                    this.createFrictionEquationsFromContact(r, this.frictionResult);
+                }
+            }
+        }
+    }
+
+    // Triangle faces
+    var va = sphereTrimesh_va;
+    var vb = sphereTrimesh_vb;
+    var vc = sphereTrimesh_vc;
+    var normal = sphereTrimesh_normal;
+    for(var i=0, N = triangles.length; i !== N; i++){
+        trimeshShape.getTriangleVertices(triangles[i], va, vb, vc);
+        trimeshShape.getNormal(triangles[i], normal);
+        localSpherePos.vsub(va, tmp);
+        var dist = tmp.dot(normal);
+        normal.scale(dist, tmp);
+        localSpherePos.vsub(tmp, tmp);
+
+        // tmp is now the sphere position projected to the triangle plane
+        dist = tmp.distanceTo(localSpherePos);
+        if(Ray.pointInTriangle(tmp, va, vb, vc) && dist < sphereShape.radius){
+            if(justTest){
+                return true;
+            }
+            var r = this.createContactEquation(sphereBody, trimeshBody, sphereShape, trimeshShape,rsi,rsj);
+
+            tmp.vsub(localSpherePos, r.ni);
+            r.ni.normalize();
+            r.ni.scale(sphereShape.radius, r.ri);
+
+            Transform.pointToWorldFrame(trimeshPos, trimeshQuat, tmp, tmp);
+            tmp.vsub(trimeshBody.position, r.rj);
+
+            Transform.vectorToWorldFrame(trimeshQuat, r.ni, r.ni);
+            Transform.vectorToWorldFrame(trimeshQuat, r.ri, r.ri);
+
+            this.result.push(r);
+            this.createFrictionEquationsFromContact(r, this.frictionResult);
+        }
+    }
+
+    triangles.length = 0;
+};
+
+var point_on_plane_to_sphere = new Vec3();
+var plane_to_sphere_ortho = new Vec3();
+
+/**
+ * @method spherePlane
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.SPHERE | Shape.types.PLANE] =
+Narrowphase.prototype.spherePlane = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    // We will have one contact in this case
+    var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+
+    // Contact normal
+    r.ni.set(0,0,1);
+    qj.vmult(r.ni, r.ni);
+    r.ni.negate(r.ni); // body i is the sphere, flip normal
+    r.ni.normalize(); // Needed?
+
+    // Vector from sphere center to contact point
+    r.ni.mult(si.radius, r.ri);
+
+    // Project down sphere on plane
+    xi.vsub(xj, point_on_plane_to_sphere);
+    r.ni.mult(r.ni.dot(point_on_plane_to_sphere), plane_to_sphere_ortho);
+    point_on_plane_to_sphere.vsub(plane_to_sphere_ortho,r.rj); // The sphere position projected to plane
+
+    if(-point_on_plane_to_sphere.dot(r.ni) <= si.radius){
+
+        if(justTest){
+            return true;
+        }
+
+        // Make it relative to the body
+        var ri = r.ri;
+        var rj = r.rj;
+        ri.vadd(xi, ri);
+        ri.vsub(bi.position, ri);
+        rj.vadd(xj, rj);
+        rj.vsub(bj.position, rj);
+
+        this.result.push(r);
+        this.createFrictionEquationsFromContact(r, this.frictionResult);
+    }
+};
+
+// See http://bulletphysics.com/Bullet/BulletFull/SphereTriangleDetector_8cpp_source.html
+var pointInPolygon_edge = new Vec3();
+var pointInPolygon_edge_x_normal = new Vec3();
+var pointInPolygon_vtp = new Vec3();
+function pointInPolygon(verts, normal, p){
+    var positiveResult = null;
+    var N = verts.length;
+    for(var i=0; i!==N; i++){
+        var v = verts[i];
+
+        // Get edge to the next vertex
+        var edge = pointInPolygon_edge;
+        verts[(i+1) % (N)].vsub(v,edge);
+
+        // Get cross product between polygon normal and the edge
+        var edge_x_normal = pointInPolygon_edge_x_normal;
+        //var edge_x_normal = new Vec3();
+        edge.cross(normal,edge_x_normal);
+
+        // Get vector between point and current vertex
+        var vertex_to_p = pointInPolygon_vtp;
+        p.vsub(v,vertex_to_p);
+
+        // This dot product determines which side of the edge the point is
+        var r = edge_x_normal.dot(vertex_to_p);
+
+        // If all such dot products have same sign, we are inside the polygon.
+        if(positiveResult===null || (r>0 && positiveResult===true) || (r<=0 && positiveResult===false)){
+            if(positiveResult===null){
+                positiveResult = r>0;
+            }
+            continue;
+        } else {
+            return false; // Encountered some other sign. Exit.
+        }
+    }
+
+    // If we got here, all dot products were of the same sign.
+    return true;
+}
+
+var box_to_sphere = new Vec3();
+var sphereBox_ns = new Vec3();
+var sphereBox_ns1 = new Vec3();
+var sphereBox_ns2 = new Vec3();
+var sphereBox_sides = [new Vec3(),new Vec3(),new Vec3(),new Vec3(),new Vec3(),new Vec3()];
+var sphereBox_sphere_to_corner = new Vec3();
+var sphereBox_side_ns = new Vec3();
+var sphereBox_side_ns1 = new Vec3();
+var sphereBox_side_ns2 = new Vec3();
+
+/**
+ * @method sphereBox
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.SPHERE | Shape.types.BOX] =
+Narrowphase.prototype.sphereBox = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    var v3pool = this.v3pool;
+
+    // we refer to the box as body j
+    var sides = sphereBox_sides;
+    xi.vsub(xj,box_to_sphere);
+    sj.getSideNormals(sides,qj);
+    var R =     si.radius;
+    var penetrating_sides = [];
+
+    // Check side (plane) intersections
+    var found = false;
+
+    // Store the resulting side penetration info
+    var side_ns = sphereBox_side_ns;
+    var side_ns1 = sphereBox_side_ns1;
+    var side_ns2 = sphereBox_side_ns2;
+    var side_h = null;
+    var side_penetrations = 0;
+    var side_dot1 = 0;
+    var side_dot2 = 0;
+    var side_distance = null;
+    for(var idx=0,nsides=sides.length; idx!==nsides && found===false; idx++){
+        // Get the plane side normal (ns)
+        var ns = sphereBox_ns;
+        ns.copy(sides[idx]);
+
+        var h = ns.norm();
+        ns.normalize();
+
+        // The normal/distance dot product tells which side of the plane we are
+        var dot = box_to_sphere.dot(ns);
+
+        if(dot<h+R && dot>0){
+            // Intersects plane. Now check the other two dimensions
+            var ns1 = sphereBox_ns1;
+            var ns2 = sphereBox_ns2;
+            ns1.copy(sides[(idx+1)%3]);
+            ns2.copy(sides[(idx+2)%3]);
+            var h1 = ns1.norm();
+            var h2 = ns2.norm();
+            ns1.normalize();
+            ns2.normalize();
+            var dot1 = box_to_sphere.dot(ns1);
+            var dot2 = box_to_sphere.dot(ns2);
+            if(dot1<h1 && dot1>-h1 && dot2<h2 && dot2>-h2){
+                var dist = Math.abs(dot-h-R);
+                if(side_distance===null || dist < side_distance){
+                    side_distance = dist;
+                    side_dot1 = dot1;
+                    side_dot2 = dot2;
+                    side_h = h;
+                    side_ns.copy(ns);
+                    side_ns1.copy(ns1);
+                    side_ns2.copy(ns2);
+                    side_penetrations++;
+
+                    if(justTest){
+                        return true;
+                    }
+                }
+            }
+        }
+    }
+    if(side_penetrations){
+        found = true;
+        var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+        side_ns.mult(-R,r.ri); // Sphere r
+        r.ni.copy(side_ns);
+        r.ni.negate(r.ni); // Normal should be out of sphere
+        side_ns.mult(side_h,side_ns);
+        side_ns1.mult(side_dot1,side_ns1);
+        side_ns.vadd(side_ns1,side_ns);
+        side_ns2.mult(side_dot2,side_ns2);
+        side_ns.vadd(side_ns2,r.rj);
+
+        // Make relative to bodies
+        r.ri.vadd(xi, r.ri);
+        r.ri.vsub(bi.position, r.ri);
+        r.rj.vadd(xj, r.rj);
+        r.rj.vsub(bj.position, r.rj);
+
+        this.result.push(r);
+        this.createFrictionEquationsFromContact(r, this.frictionResult);
+    }
+
+    // Check corners
+    var rj = v3pool.get();
+    var sphere_to_corner = sphereBox_sphere_to_corner;
+    for(var j=0; j!==2 && !found; j++){
+        for(var k=0; k!==2 && !found; k++){
+            for(var l=0; l!==2 && !found; l++){
+                rj.set(0,0,0);
+                if(j){
+                    rj.vadd(sides[0],rj);
+                } else {
+                    rj.vsub(sides[0],rj);
+                }
+                if(k){
+                    rj.vadd(sides[1],rj);
+                } else {
+                    rj.vsub(sides[1],rj);
+                }
+                if(l){
+                    rj.vadd(sides[2],rj);
+                } else {
+                    rj.vsub(sides[2],rj);
+                }
+
+                // World position of corner
+                xj.vadd(rj,sphere_to_corner);
+                sphere_to_corner.vsub(xi,sphere_to_corner);
+
+                if(sphere_to_corner.norm2() < R*R){
+                    if(justTest){
+                        return true;
+                    }
+                    found = true;
+                    var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+                    r.ri.copy(sphere_to_corner);
+                    r.ri.normalize();
+                    r.ni.copy(r.ri);
+                    r.ri.mult(R,r.ri);
+                    r.rj.copy(rj);
+
+                    // Make relative to bodies
+                    r.ri.vadd(xi, r.ri);
+                    r.ri.vsub(bi.position, r.ri);
+                    r.rj.vadd(xj, r.rj);
+                    r.rj.vsub(bj.position, r.rj);
+
+                    this.result.push(r);
+                    this.createFrictionEquationsFromContact(r, this.frictionResult);
+                }
+            }
+        }
+    }
+    v3pool.release(rj);
+    rj = null;
+
+    // Check edges
+    var edgeTangent = v3pool.get();
+    var edgeCenter = v3pool.get();
+    var r = v3pool.get(); // r = edge center to sphere center
+    var orthogonal = v3pool.get();
+    var dist = v3pool.get();
+    var Nsides = sides.length;
+    for(var j=0; j!==Nsides && !found; j++){
+        for(var k=0; k!==Nsides && !found; k++){
+            if(j%3 !== k%3){
+                // Get edge tangent
+                sides[k].cross(sides[j],edgeTangent);
+                edgeTangent.normalize();
+                sides[j].vadd(sides[k], edgeCenter);
+                r.copy(xi);
+                r.vsub(edgeCenter,r);
+                r.vsub(xj,r);
+                var orthonorm = r.dot(edgeTangent); // distance from edge center to sphere center in the tangent direction
+                edgeTangent.mult(orthonorm,orthogonal); // Vector from edge center to sphere center in the tangent direction
+
+                // Find the third side orthogonal to this one
+                var l = 0;
+                while(l===j%3 || l===k%3){
+                    l++;
+                }
+
+                // vec from edge center to sphere projected to the plane orthogonal to the edge tangent
+                dist.copy(xi);
+                dist.vsub(orthogonal,dist);
+                dist.vsub(edgeCenter,dist);
+                dist.vsub(xj,dist);
+
+                // Distances in tangent direction and distance in the plane orthogonal to it
+                var tdist = Math.abs(orthonorm);
+                var ndist = dist.norm();
+
+                if(tdist < sides[l].norm() && ndist<R){
+                    if(justTest){
+                        return true;
+                    }
+                    found = true;
+                    var res = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+                    edgeCenter.vadd(orthogonal,res.rj); // box rj
+                    res.rj.copy(res.rj);
+                    dist.negate(res.ni);
+                    res.ni.normalize();
+
+                    res.ri.copy(res.rj);
+                    res.ri.vadd(xj,res.ri);
+                    res.ri.vsub(xi,res.ri);
+                    res.ri.normalize();
+                    res.ri.mult(R,res.ri);
+
+                    // Make relative to bodies
+                    res.ri.vadd(xi, res.ri);
+                    res.ri.vsub(bi.position, res.ri);
+                    res.rj.vadd(xj, res.rj);
+                    res.rj.vsub(bj.position, res.rj);
+
+                    this.result.push(res);
+                    this.createFrictionEquationsFromContact(res, this.frictionResult);
+                }
+            }
+        }
+    }
+    v3pool.release(edgeTangent,edgeCenter,r,orthogonal,dist);
+};
+
+var convex_to_sphere = new Vec3();
+var sphereConvex_edge = new Vec3();
+var sphereConvex_edgeUnit = new Vec3();
+var sphereConvex_sphereToCorner = new Vec3();
+var sphereConvex_worldCorner = new Vec3();
+var sphereConvex_worldNormal = new Vec3();
+var sphereConvex_worldPoint = new Vec3();
+var sphereConvex_worldSpherePointClosestToPlane = new Vec3();
+var sphereConvex_penetrationVec = new Vec3();
+var sphereConvex_sphereToWorldPoint = new Vec3();
+
+/**
+ * @method sphereConvex
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.SPHERE | Shape.types.CONVEXPOLYHEDRON] =
+Narrowphase.prototype.sphereConvex = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    var v3pool = this.v3pool;
+    xi.vsub(xj,convex_to_sphere);
+    var normals = sj.faceNormals;
+    var faces = sj.faces;
+    var verts = sj.vertices;
+    var R =     si.radius;
+    var penetrating_sides = [];
+
+    // if(convex_to_sphere.norm2() > si.boundingSphereRadius + sj.boundingSphereRadius){
+    //     return;
+    // }
+
+    // Check corners
+    for(var i=0; i!==verts.length; i++){
+        var v = verts[i];
+
+        // World position of corner
+        var worldCorner = sphereConvex_worldCorner;
+        qj.vmult(v,worldCorner);
+        xj.vadd(worldCorner,worldCorner);
+        var sphere_to_corner = sphereConvex_sphereToCorner;
+        worldCorner.vsub(xi, sphere_to_corner);
+        if(sphere_to_corner.norm2() < R * R){
+            if(justTest){
+                return true;
+            }
+            found = true;
+            var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+            r.ri.copy(sphere_to_corner);
+            r.ri.normalize();
+            r.ni.copy(r.ri);
+            r.ri.mult(R,r.ri);
+            worldCorner.vsub(xj,r.rj);
+
+            // Should be relative to the body.
+            r.ri.vadd(xi, r.ri);
+            r.ri.vsub(bi.position, r.ri);
+
+            // Should be relative to the body.
+            r.rj.vadd(xj, r.rj);
+            r.rj.vsub(bj.position, r.rj);
+
+            this.result.push(r);
+            this.createFrictionEquationsFromContact(r, this.frictionResult);
+            return;
+        }
+    }
+
+    // Check side (plane) intersections
+    var found = false;
+    for(var i=0, nfaces=faces.length; i!==nfaces && found===false; i++){
+        var normal = normals[i];
+        var face = faces[i];
+
+        // Get world-transformed normal of the face
+        var worldNormal = sphereConvex_worldNormal;
+        qj.vmult(normal,worldNormal);
+
+        // Get a world vertex from the face
+        var worldPoint = sphereConvex_worldPoint;
+        qj.vmult(verts[face[0]],worldPoint);
+        worldPoint.vadd(xj,worldPoint);
+
+        // Get a point on the sphere, closest to the face normal
+        var worldSpherePointClosestToPlane = sphereConvex_worldSpherePointClosestToPlane;
+        worldNormal.mult(-R, worldSpherePointClosestToPlane);
+        xi.vadd(worldSpherePointClosestToPlane, worldSpherePointClosestToPlane);
+
+        // Vector from a face point to the closest point on the sphere
+        var penetrationVec = sphereConvex_penetrationVec;
+        worldSpherePointClosestToPlane.vsub(worldPoint,penetrationVec);
+
+        // The penetration. Negative value means overlap.
+        var penetration = penetrationVec.dot(worldNormal);
+
+        var worldPointToSphere = sphereConvex_sphereToWorldPoint;
+        xi.vsub(worldPoint, worldPointToSphere);
+
+        if(penetration < 0 && worldPointToSphere.dot(worldNormal)>0){
+            // Intersects plane. Now check if the sphere is inside the face polygon
+            var faceVerts = []; // Face vertices, in world coords
+            for(var j=0, Nverts=face.length; j!==Nverts; j++){
+                var worldVertex = v3pool.get();
+                qj.vmult(verts[face[j]], worldVertex);
+                xj.vadd(worldVertex,worldVertex);
+                faceVerts.push(worldVertex);
+            }
+
+            if(pointInPolygon(faceVerts,worldNormal,xi)){ // Is the sphere center in the face polygon?
+                if(justTest){
+                    return true;
+                }
+                found = true;
+                var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+
+                worldNormal.mult(-R, r.ri); // Contact offset, from sphere center to contact
+                worldNormal.negate(r.ni); // Normal pointing out of sphere
+
+                var penetrationVec2 = v3pool.get();
+                worldNormal.mult(-penetration, penetrationVec2);
+                var penetrationSpherePoint = v3pool.get();
+                worldNormal.mult(-R, penetrationSpherePoint);
+
+                //xi.vsub(xj).vadd(penetrationSpherePoint).vadd(penetrationVec2 , r.rj);
+                xi.vsub(xj,r.rj);
+                r.rj.vadd(penetrationSpherePoint,r.rj);
+                r.rj.vadd(penetrationVec2 , r.rj);
+
+                // Should be relative to the body.
+                r.rj.vadd(xj, r.rj);
+                r.rj.vsub(bj.position, r.rj);
+
+                // Should be relative to the body.
+                r.ri.vadd(xi, r.ri);
+                r.ri.vsub(bi.position, r.ri);
+
+                v3pool.release(penetrationVec2);
+                v3pool.release(penetrationSpherePoint);
+
+                this.result.push(r);
+                this.createFrictionEquationsFromContact(r, this.frictionResult);
+
+                // Release world vertices
+                for(var j=0, Nfaceverts=faceVerts.length; j!==Nfaceverts; j++){
+                    v3pool.release(faceVerts[j]);
+                }
+
+                return; // We only expect *one* face contact
+            } else {
+                // Edge?
+                for(var j=0; j!==face.length; j++){
+
+                    // Get two world transformed vertices
+                    var v1 = v3pool.get();
+                    var v2 = v3pool.get();
+                    qj.vmult(verts[face[(j+1)%face.length]], v1);
+                    qj.vmult(verts[face[(j+2)%face.length]], v2);
+                    xj.vadd(v1, v1);
+                    xj.vadd(v2, v2);
+
+                    // Construct edge vector
+                    var edge = sphereConvex_edge;
+                    v2.vsub(v1,edge);
+
+                    // Construct the same vector, but normalized
+                    var edgeUnit = sphereConvex_edgeUnit;
+                    edge.unit(edgeUnit);
+
+                    // p is xi projected onto the edge
+                    var p = v3pool.get();
+                    var v1_to_xi = v3pool.get();
+                    xi.vsub(v1, v1_to_xi);
+                    var dot = v1_to_xi.dot(edgeUnit);
+                    edgeUnit.mult(dot, p);
+                    p.vadd(v1, p);
+
+                    // Compute a vector from p to the center of the sphere
+                    var xi_to_p = v3pool.get();
+                    p.vsub(xi, xi_to_p);
+
+                    // Collision if the edge-sphere distance is less than the radius
+                    // AND if p is in between v1 and v2
+                    if(dot > 0 && dot*dot<edge.norm2() && xi_to_p.norm2() < R*R){ // Collision if the edge-sphere distance is less than the radius
+                        // Edge contact!
+                        if(justTest){
+                            return true;
+                        }
+                        var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+                        p.vsub(xj,r.rj);
+
+                        p.vsub(xi,r.ni);
+                        r.ni.normalize();
+
+                        r.ni.mult(R,r.ri);
+
+                        // Should be relative to the body.
+                        r.rj.vadd(xj, r.rj);
+                        r.rj.vsub(bj.position, r.rj);
+
+                        // Should be relative to the body.
+                        r.ri.vadd(xi, r.ri);
+                        r.ri.vsub(bi.position, r.ri);
+
+                        this.result.push(r);
+                        this.createFrictionEquationsFromContact(r, this.frictionResult);
+
+                        // Release world vertices
+                        for(var j=0, Nfaceverts=faceVerts.length; j!==Nfaceverts; j++){
+                            v3pool.release(faceVerts[j]);
+                        }
+
+                        v3pool.release(v1);
+                        v3pool.release(v2);
+                        v3pool.release(p);
+                        v3pool.release(xi_to_p);
+                        v3pool.release(v1_to_xi);
+
+                        return;
+                    }
+
+                    v3pool.release(v1);
+                    v3pool.release(v2);
+                    v3pool.release(p);
+                    v3pool.release(xi_to_p);
+                    v3pool.release(v1_to_xi);
+                }
+            }
+
+            // Release world vertices
+            for(var j=0, Nfaceverts=faceVerts.length; j!==Nfaceverts; j++){
+                v3pool.release(faceVerts[j]);
+            }
+        }
+    }
+};
+
+var planeBox_normal = new Vec3();
+var plane_to_corner = new Vec3();
+
+/**
+ * @method planeBox
+ * @param  {Array}      result
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.PLANE | Shape.types.BOX] =
+Narrowphase.prototype.planeBox = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    sj.convexPolyhedronRepresentation.material = sj.material;
+    sj.convexPolyhedronRepresentation.collisionResponse = sj.collisionResponse;
+    sj.convexPolyhedronRepresentation.id = sj.id;
+    return this.planeConvex(si,sj.convexPolyhedronRepresentation,xi,xj,qi,qj,bi,bj,si,sj,justTest);
+};
+
+var planeConvex_v = new Vec3();
+var planeConvex_normal = new Vec3();
+var planeConvex_relpos = new Vec3();
+var planeConvex_projected = new Vec3();
+
+/**
+ * @method planeConvex
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.PLANE | Shape.types.CONVEXPOLYHEDRON] =
+Narrowphase.prototype.planeConvex = function(
+    planeShape,
+    convexShape,
+    planePosition,
+    convexPosition,
+    planeQuat,
+    convexQuat,
+    planeBody,
+    convexBody,
+    si,
+    sj,
+    justTest
+){
+    // Simply return the points behind the plane.
+    var worldVertex = planeConvex_v,
+        worldNormal = planeConvex_normal;
+    worldNormal.set(0,0,1);
+    planeQuat.vmult(worldNormal,worldNormal); // Turn normal according to plane orientation
+
+    var numContacts = 0;
+    var relpos = planeConvex_relpos;
+    for(var i = 0; i !== convexShape.vertices.length; i++){
+
+        // Get world convex vertex
+        worldVertex.copy(convexShape.vertices[i]);
+        convexQuat.vmult(worldVertex, worldVertex);
+        convexPosition.vadd(worldVertex, worldVertex);
+        worldVertex.vsub(planePosition, relpos);
+
+        var dot = worldNormal.dot(relpos);
+        if(dot <= 0.0){
+            if(justTest){
+                return true;
+            }
+
+            var r = this.createContactEquation(planeBody, convexBody, planeShape, convexShape, si, sj);
+
+            // Get vertex position projected on plane
+            var projected = planeConvex_projected;
+            worldNormal.mult(worldNormal.dot(relpos),projected);
+            worldVertex.vsub(projected, projected);
+            projected.vsub(planePosition, r.ri); // From plane to vertex projected on plane
+
+            r.ni.copy(worldNormal); // Contact normal is the plane normal out from plane
+
+            // rj is now just the vector from the convex center to the vertex
+            worldVertex.vsub(convexPosition, r.rj);
+
+            // Make it relative to the body
+            r.ri.vadd(planePosition, r.ri);
+            r.ri.vsub(planeBody.position, r.ri);
+            r.rj.vadd(convexPosition, r.rj);
+            r.rj.vsub(convexBody.position, r.rj);
+
+            this.result.push(r);
+            numContacts++;
+            if(!this.enableFrictionReduction){
+                this.createFrictionEquationsFromContact(r, this.frictionResult);
+            }
+        }
+    }
+
+    if(this.enableFrictionReduction && numContacts){
+        this.createFrictionFromAverage(numContacts);
+    }
+};
+
+var convexConvex_sepAxis = new Vec3();
+var convexConvex_q = new Vec3();
+
+/**
+ * @method convexConvex
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.CONVEXPOLYHEDRON] =
+Narrowphase.prototype.convexConvex = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest,faceListA,faceListB){
+    var sepAxis = convexConvex_sepAxis;
+
+    if(xi.distanceTo(xj) > si.boundingSphereRadius + sj.boundingSphereRadius){
+        return;
+    }
+
+    if(si.findSeparatingAxis(sj,xi,qi,xj,qj,sepAxis,faceListA,faceListB)){
+        var res = [];
+        var q = convexConvex_q;
+        si.clipAgainstHull(xi,qi,sj,xj,qj,sepAxis,-100,100,res);
+        var numContacts = 0;
+        for(var j = 0; j !== res.length; j++){
+            if(justTest){
+                return true;
+            }
+            var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj),
+                ri = r.ri,
+                rj = r.rj;
+            sepAxis.negate(r.ni);
+            res[j].normal.negate(q);
+            q.mult(res[j].depth, q);
+            res[j].point.vadd(q, ri);
+            rj.copy(res[j].point);
+
+            // Contact points are in world coordinates. Transform back to relative
+            ri.vsub(xi,ri);
+            rj.vsub(xj,rj);
+
+            // Make relative to bodies
+            ri.vadd(xi, ri);
+            ri.vsub(bi.position, ri);
+            rj.vadd(xj, rj);
+            rj.vsub(bj.position, rj);
+
+            this.result.push(r);
+            numContacts++;
+            if(!this.enableFrictionReduction){
+                this.createFrictionEquationsFromContact(r, this.frictionResult);
+            }
+        }
+        if(this.enableFrictionReduction && numContacts){
+            this.createFrictionFromAverage(numContacts);
+        }
+    }
+};
+
+
+/**
+ * @method convexTrimesh
+ * @param  {Array}      result
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+// Narrowphase.prototype[Shape.types.CONVEXPOLYHEDRON | Shape.types.TRIMESH] =
+// Narrowphase.prototype.convexTrimesh = function(si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,faceListA,faceListB){
+//     var sepAxis = convexConvex_sepAxis;
+
+//     if(xi.distanceTo(xj) > si.boundingSphereRadius + sj.boundingSphereRadius){
+//         return;
+//     }
+
+//     // Construct a temp hull for each triangle
+//     var hullB = new ConvexPolyhedron();
+
+//     hullB.faces = [[0,1,2]];
+//     var va = new Vec3();
+//     var vb = new Vec3();
+//     var vc = new Vec3();
+//     hullB.vertices = [
+//         va,
+//         vb,
+//         vc
+//     ];
+
+//     for (var i = 0; i < sj.indices.length / 3; i++) {
+
+//         var triangleNormal = new Vec3();
+//         sj.getNormal(i, triangleNormal);
+//         hullB.faceNormals = [triangleNormal];
+
+//         sj.getTriangleVertices(i, va, vb, vc);
+
+//         var d = si.testSepAxis(triangleNormal, hullB, xi, qi, xj, qj);
+//         if(!d){
+//             triangleNormal.scale(-1, triangleNormal);
+//             d = si.testSepAxis(triangleNormal, hullB, xi, qi, xj, qj);
+
+//             if(!d){
+//                 continue;
+//             }
+//         }
+
+//         var res = [];
+//         var q = convexConvex_q;
+//         si.clipAgainstHull(xi,qi,hullB,xj,qj,triangleNormal,-100,100,res);
+//         for(var j = 0; j !== res.length; j++){
+//             var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj),
+//                 ri = r.ri,
+//                 rj = r.rj;
+//             r.ni.copy(triangleNormal);
+//             r.ni.negate(r.ni);
+//             res[j].normal.negate(q);
+//             q.mult(res[j].depth, q);
+//             res[j].point.vadd(q, ri);
+//             rj.copy(res[j].point);
+
+//             // Contact points are in world coordinates. Transform back to relative
+//             ri.vsub(xi,ri);
+//             rj.vsub(xj,rj);
+
+//             // Make relative to bodies
+//             ri.vadd(xi, ri);
+//             ri.vsub(bi.position, ri);
+//             rj.vadd(xj, rj);
+//             rj.vsub(bj.position, rj);
+
+//             result.push(r);
+//         }
+//     }
+// };
+
+var particlePlane_normal = new Vec3();
+var particlePlane_relpos = new Vec3();
+var particlePlane_projected = new Vec3();
+
+/**
+ * @method particlePlane
+ * @param  {Array}      result
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.PLANE | Shape.types.PARTICLE] =
+Narrowphase.prototype.planeParticle = function(sj,si,xj,xi,qj,qi,bj,bi,rsi,rsj,justTest){
+    var normal = particlePlane_normal;
+    normal.set(0,0,1);
+    bj.quaternion.vmult(normal,normal); // Turn normal according to plane orientation
+    var relpos = particlePlane_relpos;
+    xi.vsub(bj.position,relpos);
+    var dot = normal.dot(relpos);
+    if(dot <= 0.0){
+
+        if(justTest){
+            return true;
+        }
+
+        var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+        r.ni.copy(normal); // Contact normal is the plane normal
+        r.ni.negate(r.ni);
+        r.ri.set(0,0,0); // Center of particle
+
+        // Get particle position projected on plane
+        var projected = particlePlane_projected;
+        normal.mult(normal.dot(xi),projected);
+        xi.vsub(projected,projected);
+        //projected.vadd(bj.position,projected);
+
+        // rj is now the projected world position minus plane position
+        r.rj.copy(projected);
+        this.result.push(r);
+        this.createFrictionEquationsFromContact(r, this.frictionResult);
+    }
+};
+
+var particleSphere_normal = new Vec3();
+
+/**
+ * @method particleSphere
+ * @param  {Array}      result
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.PARTICLE | Shape.types.SPHERE] =
+Narrowphase.prototype.sphereParticle = function(sj,si,xj,xi,qj,qi,bj,bi,rsi,rsj,justTest){
+    // The normal is the unit vector from sphere center to particle center
+    var normal = particleSphere_normal;
+    normal.set(0,0,1);
+    xi.vsub(xj,normal);
+    var lengthSquared = normal.norm2();
+
+    if(lengthSquared <= sj.radius * sj.radius){
+        if(justTest){
+            return true;
+        }
+        var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+        normal.normalize();
+        r.rj.copy(normal);
+        r.rj.mult(sj.radius,r.rj);
+        r.ni.copy(normal); // Contact normal
+        r.ni.negate(r.ni);
+        r.ri.set(0,0,0); // Center of particle
+        this.result.push(r);
+        this.createFrictionEquationsFromContact(r, this.frictionResult);
+    }
+};
+
+// WIP
+var cqj = new Quaternion();
+var convexParticle_local = new Vec3();
+var convexParticle_normal = new Vec3();
+var convexParticle_penetratedFaceNormal = new Vec3();
+var convexParticle_vertexToParticle = new Vec3();
+var convexParticle_worldPenetrationVec = new Vec3();
+
+/**
+ * @method convexParticle
+ * @param  {Array}      result
+ * @param  {Shape}      si
+ * @param  {Shape}      sj
+ * @param  {Vec3}       xi
+ * @param  {Vec3}       xj
+ * @param  {Quaternion} qi
+ * @param  {Quaternion} qj
+ * @param  {Body}       bi
+ * @param  {Body}       bj
+ */
+Narrowphase.prototype[Shape.types.PARTICLE | Shape.types.CONVEXPOLYHEDRON] =
+Narrowphase.prototype.convexParticle = function(sj,si,xj,xi,qj,qi,bj,bi,rsi,rsj,justTest){
+    var penetratedFaceIndex = -1;
+    var penetratedFaceNormal = convexParticle_penetratedFaceNormal;
+    var worldPenetrationVec = convexParticle_worldPenetrationVec;
+    var minPenetration = null;
+    var numDetectedFaces = 0;
+
+    // Convert particle position xi to local coords in the convex
+    var local = convexParticle_local;
+    local.copy(xi);
+    local.vsub(xj,local); // Convert position to relative the convex origin
+    qj.conjugate(cqj);
+    cqj.vmult(local,local);
+
+    if(sj.pointIsInside(local)){
+
+        if(sj.worldVerticesNeedsUpdate){
+            sj.computeWorldVertices(xj,qj);
+        }
+        if(sj.worldFaceNormalsNeedsUpdate){
+            sj.computeWorldFaceNormals(qj);
+        }
+
+        // For each world polygon in the polyhedra
+        for(var i=0,nfaces=sj.faces.length; i!==nfaces; i++){
+
+            // Construct world face vertices
+            var verts = [ sj.worldVertices[ sj.faces[i][0] ] ];
+            var normal = sj.worldFaceNormals[i];
+
+            // Check how much the particle penetrates the polygon plane.
+            xi.vsub(verts[0],convexParticle_vertexToParticle);
+            var penetration = -normal.dot(convexParticle_vertexToParticle);
+            if(minPenetration===null || Math.abs(penetration)<Math.abs(minPenetration)){
+
+                if(justTest){
+                    return true;
+                }
+
+                minPenetration = penetration;
+                penetratedFaceIndex = i;
+                penetratedFaceNormal.copy(normal);
+                numDetectedFaces++;
+            }
+        }
+
+        if(penetratedFaceIndex!==-1){
+            // Setup contact
+            var r = this.createContactEquation(bi,bj,si,sj,rsi,rsj);
+            penetratedFaceNormal.mult(minPenetration, worldPenetrationVec);
+
+            // rj is the particle position projected to the face
+            worldPenetrationVec.vadd(xi,worldPenetrationVec);
+            worldPenetrationVec.vsub(xj,worldPenetrationVec);
+            r.rj.copy(worldPenetrationVec);
+            //var projectedToFace = xi.vsub(xj).vadd(worldPenetrationVec);
+            //projectedToFace.copy(r.rj);
+
+            //qj.vmult(r.rj,r.rj);
+            penetratedFaceNormal.negate( r.ni ); // Contact normal
+            r.ri.set(0,0,0); // Center of particle
+
+            var ri = r.ri,
+                rj = r.rj;
+
+            // Make relative to bodies
+            ri.vadd(xi, ri);
+            ri.vsub(bi.position, ri);
+            rj.vadd(xj, rj);
+            rj.vsub(bj.position, rj);
+
+            this.result.push(r);
+            this.createFrictionEquationsFromContact(r, this.frictionResult);
+        } else {
+            console.warn("Point found inside convex, but did not find penetrating face!");
+        }
+    }
+};
+
+Narrowphase.prototype[Shape.types.BOX | Shape.types.HEIGHTFIELD] =
+Narrowphase.prototype.boxHeightfield = function (si,sj,xi,xj,qi,qj,bi,bj,rsi,rsj,justTest){
+    si.convexPolyhedronRepresentation.material = si.material;
+    si.convexPolyhedronRepresentation.collisionResponse = si.collisionResponse;
+    return this.convexHeightfield(si.convexPolyhedronRepresentation,sj,xi,xj,qi,qj,bi,bj,si,sj,justTest);
+};
+
+var convexHeightfield_tmp1 = new Vec3();
+var convexHeightfield_tmp2 = new Vec3();
+var convexHeightfield_faceList = [0];
+
+/**
+ * @method convexHeightfield
+ */
+Narrowphase.prototype[Shape.types.CONVEXPOLYHEDRON | Shape.types.HEIGHTFIELD] =
+Narrowphase.prototype.convexHeightfield = function (
+    convexShape,
+    hfShape,
+    convexPos,
+    hfPos,
+    convexQuat,
+    hfQuat,
+    convexBody,
+    hfBody,
+    rsi,
+    rsj,
+    justTest
+){
+    var data = hfShape.data,
+        w = hfShape.elementSize,
+        radius = convexShape.boundingSphereRadius,
+        worldPillarOffset = convexHeightfield_tmp2,
+        faceList = convexHeightfield_faceList;
+
+    // Get sphere position to heightfield local!
+    var localConvexPos = convexHeightfield_tmp1;
+    Transform.pointToLocalFrame(hfPos, hfQuat, convexPos, localConvexPos);
+
+    // Get the index of the data points to test against
+    var iMinX = Math.floor((localConvexPos.x - radius) / w) - 1,
+        iMaxX = Math.ceil((localConvexPos.x + radius) / w) + 1,
+        iMinY = Math.floor((localConvexPos.y - radius) / w) - 1,
+        iMaxY = Math.ceil((localConvexPos.y + radius) / w) + 1;
+
+    // Bail out if we are out of the terrain
+    if(iMaxX < 0 || iMaxY < 0 || iMinX > data.length || iMinY > data[0].length){
+        return;
+    }
+
+    // Clamp index to edges
+    if(iMinX < 0){ iMinX = 0; }
+    if(iMaxX < 0){ iMaxX = 0; }
+    if(iMinY < 0){ iMinY = 0; }
+    if(iMaxY < 0){ iMaxY = 0; }
+    if(iMinX >= data.length){ iMinX = data.length - 1; }
+    if(iMaxX >= data.length){ iMaxX = data.length - 1; }
+    if(iMaxY >= data[0].length){ iMaxY = data[0].length - 1; }
+    if(iMinY >= data[0].length){ iMinY = data[0].length - 1; }
+
+    var minMax = [];
+    hfShape.getRectMinMax(iMinX, iMinY, iMaxX, iMaxY, minMax);
+    var min = minMax[0];
+    var max = minMax[1];
+
+    // Bail out if we're cant touch the bounding height box
+    if(localConvexPos.z - radius > max || localConvexPos.z + radius < min){
+        return;
+    }
+
+    for(var i = iMinX; i < iMaxX; i++){
+        for(var j = iMinY; j < iMaxY; j++){
+
+            var intersecting = false;
+
+            // Lower triangle
+            hfShape.getConvexTrianglePillar(i, j, false);
+            Transform.pointToWorldFrame(hfPos, hfQuat, hfShape.pillarOffset, worldPillarOffset);
+            if (convexPos.distanceTo(worldPillarOffset) < hfShape.pillarConvex.boundingSphereRadius + convexShape.boundingSphereRadius) {
+                intersecting = this.convexConvex(convexShape, hfShape.pillarConvex, convexPos, worldPillarOffset, convexQuat, hfQuat, convexBody, hfBody, null, null, justTest, faceList, null);
+            }
+
+            if(justTest && intersecting){
+                return true;
+            }
+
+            // Upper triangle
+            hfShape.getConvexTrianglePillar(i, j, true);
+            Transform.pointToWorldFrame(hfPos, hfQuat, hfShape.pillarOffset, worldPillarOffset);
+            if (convexPos.distanceTo(worldPillarOffset) < hfShape.pillarConvex.boundingSphereRadius + convexShape.boundingSphereRadius) {
+                intersecting = this.convexConvex(convexShape, hfShape.pillarConvex, convexPos, worldPillarOffset, convexQuat, hfQuat, convexBody, hfBody, null, null, justTest, faceList, null);
+            }
+
+            if(justTest && intersecting){
+                return true;
+            }
+        }
+    }
+};
+
+var sphereHeightfield_tmp1 = new Vec3();
+var sphereHeightfield_tmp2 = new Vec3();
+
+/**
+ * @method sphereHeightfield
+ */
+Narrowphase.prototype[Shape.types.SPHERE | Shape.types.HEIGHTFIELD] =
+Narrowphase.prototype.sphereHeightfield = function (
+    sphereShape,
+    hfShape,
+    spherePos,
+    hfPos,
+    sphereQuat,
+    hfQuat,
+    sphereBody,
+    hfBody,
+    rsi,
+    rsj,
+    justTest
+){
+    var data = hfShape.data,
+        radius = sphereShape.radius,
+        w = hfShape.elementSize,
+        worldPillarOffset = sphereHeightfield_tmp2;
+
+    // Get sphere position to heightfield local!
+    var localSpherePos = sphereHeightfield_tmp1;
+    Transform.pointToLocalFrame(hfPos, hfQuat, spherePos, localSpherePos);
+
+    // Get the index of the data points to test against
+    var iMinX = Math.floor((localSpherePos.x - radius) / w) - 1,
+        iMaxX = Math.ceil((localSpherePos.x + radius) / w) + 1,
+        iMinY = Math.floor((localSpherePos.y - radius) / w) - 1,
+        iMaxY = Math.ceil((localSpherePos.y + radius) / w) + 1;
+
+    // Bail out if we are out of the terrain
+    if(iMaxX < 0 || iMaxY < 0 || iMinX > data.length || iMaxY > data[0].length){
+        return;
+    }
+
+    // Clamp index to edges
+    if(iMinX < 0){ iMinX = 0; }
+    if(iMaxX < 0){ iMaxX = 0; }
+    if(iMinY < 0){ iMinY = 0; }
+    if(iMaxY < 0){ iMaxY = 0; }
+    if(iMinX >= data.length){ iMinX = data.length - 1; }
+    if(iMaxX >= data.length){ iMaxX = data.length - 1; }
+    if(iMaxY >= data[0].length){ iMaxY = data[0].length - 1; }
+    if(iMinY >= data[0].length){ iMinY = data[0].length - 1; }
+
+    var minMax = [];
+    hfShape.getRectMinMax(iMinX, iMinY, iMaxX, iMaxY, minMax);
+    var min = minMax[0];
+    var max = minMax[1];
+
+    // Bail out if we're cant touch the bounding height box
+    if(localSpherePos.z - radius > max || localSpherePos.z + radius < min){
+        return;
+    }
+
+    var result = this.result;
+    for(var i = iMinX; i < iMaxX; i++){
+        for(var j = iMinY; j < iMaxY; j++){
+
+            var numContactsBefore = result.length;
+
+            var intersecting = false;
+
+            // Lower triangle
+            hfShape.getConvexTrianglePillar(i, j, false);
+            Transform.pointToWorldFrame(hfPos, hfQuat, hfShape.pillarOffset, worldPillarOffset);
+            if (spherePos.distanceTo(worldPillarOffset) < hfShape.pillarConvex.boundingSphereRadius + sphereShape.boundingSphereRadius) {
+                intersecting = this.sphereConvex(sphereShape, hfShape.pillarConvex, spherePos, worldPillarOffset, sphereQuat, hfQuat, sphereBody, hfBody, sphereShape, hfShape, justTest);
+            }
+
+            if(justTest && intersecting){
+                return true;
+            }
+
+            // Upper triangle
+            hfShape.getConvexTrianglePillar(i, j, true);
+            Transform.pointToWorldFrame(hfPos, hfQuat, hfShape.pillarOffset, worldPillarOffset);
+            if (spherePos.distanceTo(worldPillarOffset) < hfShape.pillarConvex.boundingSphereRadius + sphereShape.boundingSphereRadius) {
+                intersecting = this.sphereConvex(sphereShape, hfShape.pillarConvex, spherePos, worldPillarOffset, sphereQuat, hfQuat, sphereBody, hfBody, sphereShape, hfShape, justTest);
+            }
+
+            if(justTest && intersecting){
+                return true;
+            }
+
+            var numContacts = result.length - numContactsBefore;
+
+            if(numContacts > 2){
+                return;
+            }
+            /*
+            // Skip all but 1
+            for (var k = 0; k < numContacts - 1; k++) {
+                result.pop();
+            }
+            */
+        }
+    }
+};
+
+},{"../collision/AABB":18,"../collision/Ray":25,"../equations/ContactEquation":35,"../equations/FrictionEquation":37,"../math/Quaternion":44,"../math/Transform":45,"../math/Vec3":46,"../objects/Body":47,"../shapes/ConvexPolyhedron":54,"../shapes/Shape":59,"../solver/Solver":63,"../utils/Vec3Pool":70}],72:[function(require,module,exports){
+/* global performance */
+
+module.exports = World;
+
+var Shape = require('../shapes/Shape');
+var Vec3 = require('../math/Vec3');
+var Quaternion = require('../math/Quaternion');
+var GSSolver = require('../solver/GSSolver');
+var ContactEquation = require('../equations/ContactEquation');
+var FrictionEquation = require('../equations/FrictionEquation');
+var Narrowphase = require('./Narrowphase');
+var EventTarget = require('../utils/EventTarget');
+var ArrayCollisionMatrix = require('../collision/ArrayCollisionMatrix');
+var OverlapKeeper = require('../collision/OverlapKeeper');
+var Material = require('../material/Material');
+var ContactMaterial = require('../material/ContactMaterial');
+var Body = require('../objects/Body');
+var TupleDictionary = require('../utils/TupleDictionary');
+var RaycastResult = require('../collision/RaycastResult');
+var AABB = require('../collision/AABB');
+var Ray = require('../collision/Ray');
+var NaiveBroadphase = require('../collision/NaiveBroadphase');
+
+/**
+ * The physics world
+ * @class World
+ * @constructor
+ * @extends EventTarget
+ * @param {object} [options]
+ * @param {Vec3} [options.gravity]
+ * @param {boolean} [options.allowSleep]
+ * @param {Broadphase} [options.broadphase]
+ * @param {Solver} [options.solver]
+ * @param {boolean} [options.quatNormalizeFast]
+ * @param {number} [options.quatNormalizeSkip]
+ */
+function World(options){
+    options = options || {};
+    EventTarget.apply(this);
+
+    /**
+     * Currently / last used timestep. Is set to -1 if not available. This value is updated before each internal step, which means that it is "fresh" inside event callbacks.
+     * @property {Number} dt
+     */
+    this.dt = -1;
+
+    /**
+     * Makes bodies go to sleep when they've been inactive
+     * @property allowSleep
+     * @type {Boolean}
+     * @default false
+     */
+    this.allowSleep = !!options.allowSleep;
+
+    /**
+     * All the current contacts (instances of ContactEquation) in the world.
+     * @property contacts
+     * @type {Array}
+     */
+    this.contacts = [];
+    this.frictionEquations = [];
+
+    /**
+     * How often to normalize quaternions. Set to 0 for every step, 1 for every second etc.. A larger value increases performance. If bodies tend to explode, set to a smaller value (zero to be sure nothing can go wrong).
+     * @property quatNormalizeSkip
+     * @type {Number}
+     * @default 0
+     */
+    this.quatNormalizeSkip = options.quatNormalizeSkip !== undefined ? options.quatNormalizeSkip : 0;
+
+    /**
+     * Set to true to use fast quaternion normalization. It is often enough accurate to use. If bodies tend to explode, set to false.
+     * @property quatNormalizeFast
+     * @type {Boolean}
+     * @see Quaternion.normalizeFast
+     * @see Quaternion.normalize
+     * @default false
+     */
+    this.quatNormalizeFast = options.quatNormalizeFast !== undefined ? options.quatNormalizeFast : false;
+
+    /**
+     * The wall-clock time since simulation start
+     * @property time
+     * @type {Number}
+     */
+    this.time = 0.0;
+
+    /**
+     * Number of timesteps taken since start
+     * @property stepnumber
+     * @type {Number}
+     */
+    this.stepnumber = 0;
+
+    /// Default and last timestep sizes
+    this.default_dt = 1/60;
+
+    this.nextId = 0;
+    /**
+     * @property gravity
+     * @type {Vec3}
+     */
+    this.gravity = new Vec3();
+    if(options.gravity){
+        this.gravity.copy(options.gravity);
+    }
+
+    /**
+     * The broadphase algorithm to use. Default is NaiveBroadphase
+     * @property broadphase
+     * @type {Broadphase}
+     */
+    this.broadphase = options.broadphase !== undefined ? options.broadphase : new NaiveBroadphase();
+
+    /**
+     * @property bodies
+     * @type {Array}
+     */
+    this.bodies = [];
+
+    /**
+     * The solver algorithm to use. Default is GSSolver
+     * @property solver
+     * @type {Solver}
+     */
+    this.solver = options.solver !== undefined ? options.solver : new GSSolver();
+
+    /**
+     * @property constraints
+     * @type {Array}
+     */
+    this.constraints = [];
+
+    /**
+     * @property narrowphase
+     * @type {Narrowphase}
+     */
+    this.narrowphase = new Narrowphase(this);
+
+    /**
+     * @property {ArrayCollisionMatrix} collisionMatrix
+	 * @type {ArrayCollisionMatrix}
+	 */
+	this.collisionMatrix = new ArrayCollisionMatrix();
+
+    /**
+     * CollisionMatrix from the previous step.
+     * @property {ArrayCollisionMatrix} collisionMatrixPrevious
+	 * @type {ArrayCollisionMatrix}
+	 */
+	this.collisionMatrixPrevious = new ArrayCollisionMatrix();
+
+    this.bodyOverlapKeeper = new OverlapKeeper();
+    this.shapeOverlapKeeper = new OverlapKeeper();
+
+    /**
+     * All added materials
+     * @property materials
+     * @type {Array}
+     */
+    this.materials = [];
+
+    /**
+     * @property contactmaterials
+     * @type {Array}
+     */
+    this.contactmaterials = [];
+
+    /**
+     * Used to look up a ContactMaterial given two instances of Material.
+     * @property {TupleDictionary} contactMaterialTable
+     */
+    this.contactMaterialTable = new TupleDictionary();
+
+    this.defaultMaterial = new Material("default");
+
+    /**
+     * This contact material is used if no suitable contactmaterial is found for a contact.
+     * @property defaultContactMaterial
+     * @type {ContactMaterial}
+     */
+    this.defaultContactMaterial = new ContactMaterial(this.defaultMaterial, this.defaultMaterial, { friction: 0.3, restitution: 0.0 });
+
+    /**
+     * @property doProfiling
+     * @type {Boolean}
+     */
+    this.doProfiling = false;
+
+    /**
+     * @property profile
+     * @type {Object}
+     */
+    this.profile = {
+        solve:0,
+        makeContactConstraints:0,
+        broadphase:0,
+        integrate:0,
+        narrowphase:0,
+    };
+
+    /**
+     * Time accumulator for interpolation. See http://gafferongames.com/game-physics/fix-your-timestep/
+     * @property {Number} accumulator
+     */
+    this.accumulator = 0;
+
+    /**
+     * @property subsystems
+     * @type {Array}
+     */
+    this.subsystems = [];
+
+    /**
+     * Dispatched after a body has been added to the world.
+     * @event addBody
+     * @param {Body} body The body that has been added to the world.
+     */
+    this.addBodyEvent = {
+        type:"addBody",
+        body : null
+    };
+
+    /**
+     * Dispatched after a body has been removed from the world.
+     * @event removeBody
+     * @param {Body} body The body that has been removed from the world.
+     */
+    this.removeBodyEvent = {
+        type:"removeBody",
+        body : null
+    };
+
+    this.idToBodyMap = {};
+
+    this.broadphase.setWorld(this);
+}
+World.prototype = new EventTarget();
+
+// Temp stuff
+var tmpAABB1 = new AABB();
+var tmpArray1 = [];
+var tmpRay = new Ray();
+
+/**
+ * Get the contact material between materials m1 and m2
+ * @method getContactMaterial
+ * @param {Material} m1
+ * @param {Material} m2
+ * @return {ContactMaterial} The contact material if it was found.
+ */
+World.prototype.getContactMaterial = function(m1,m2){
+    return this.contactMaterialTable.get(m1.id,m2.id); //this.contactmaterials[this.mats2cmat[i+j*this.materials.length]];
+};
+
+/**
+ * Get number of objects in the world.
+ * @method numObjects
+ * @return {Number}
+ * @deprecated
+ */
+World.prototype.numObjects = function(){
+    return this.bodies.length;
+};
+
+/**
+ * Store old collision state info
+ * @method collisionMatrixTick
+ */
+World.prototype.collisionMatrixTick = function(){
+	var temp = this.collisionMatrixPrevious;
+	this.collisionMatrixPrevious = this.collisionMatrix;
+	this.collisionMatrix = temp;
+	this.collisionMatrix.reset();
+
+    this.bodyOverlapKeeper.tick();
+    this.shapeOverlapKeeper.tick();
+};
+
+/**
+ * Add a rigid body to the simulation.
+ * @method add
+ * @param {Body} body
+ * @todo If the simulation has not yet started, why recrete and copy arrays for each body? Accumulate in dynamic arrays in this case.
+ * @todo Adding an array of bodies should be possible. This would save some loops too
+ * @deprecated Use .addBody instead
+ */
+World.prototype.add = World.prototype.addBody = function(body){
+    if(this.bodies.indexOf(body) !== -1){
+        return;
+    }
+    body.index = this.bodies.length;
+    this.bodies.push(body);
+    body.world = this;
+    body.initPosition.copy(body.position);
+    body.initVelocity.copy(body.velocity);
+    body.timeLastSleepy = this.time;
+    if(body instanceof Body){
+        body.initAngularVelocity.copy(body.angularVelocity);
+        body.initQuaternion.copy(body.quaternion);
+    }
+	this.collisionMatrix.setNumObjects(this.bodies.length);
+    this.addBodyEvent.body = body;
+    this.idToBodyMap[body.id] = body;
+    this.dispatchEvent(this.addBodyEvent);
+};
+
+/**
+ * Add a constraint to the simulation.
+ * @method addConstraint
+ * @param {Constraint} c
+ */
+World.prototype.addConstraint = function(c){
+    this.constraints.push(c);
+};
+
+/**
+ * Removes a constraint
+ * @method removeConstraint
+ * @param {Constraint} c
+ */
+World.prototype.removeConstraint = function(c){
+    var idx = this.constraints.indexOf(c);
+    if(idx!==-1){
+        this.constraints.splice(idx,1);
+    }
+};
+
+/**
+ * Raycast test
+ * @method rayTest
+ * @param {Vec3} from
+ * @param {Vec3} to
+ * @param {RaycastResult} result
+ * @deprecated Use .raycastAll, .raycastClosest or .raycastAny instead.
+ */
+World.prototype.rayTest = function(from, to, result){
+    if(result instanceof RaycastResult){
+        // Do raycastclosest
+        this.raycastClosest(from, to, {
+            skipBackfaces: true
+        }, result);
+    } else {
+        // Do raycastAll
+        this.raycastAll(from, to, {
+            skipBackfaces: true
+        }, result);
+    }
+};
+
+/**
+ * Ray cast against all bodies. The provided callback will be executed for each hit with a RaycastResult as single argument.
+ * @method raycastAll
+ * @param  {Vec3} from
+ * @param  {Vec3} to
+ * @param  {Object} options
+ * @param  {number} [options.collisionFilterMask=-1]
+ * @param  {number} [options.collisionFilterGroup=-1]
+ * @param  {boolean} [options.skipBackfaces=false]
+ * @param  {boolean} [options.checkCollisionResponse=true]
+ * @param  {Function} callback
+ * @return {boolean} True if any body was hit.
+ */
+World.prototype.raycastAll = function(from, to, options, callback){
+    options.mode = Ray.ALL;
+    options.from = from;
+    options.to = to;
+    options.callback = callback;
+    return tmpRay.intersectWorld(this, options);
+};
+
+/**
+ * Ray cast, and stop at the first result. Note that the order is random - but the method is fast.
+ * @method raycastAny
+ * @param  {Vec3} from
+ * @param  {Vec3} to
+ * @param  {Object} options
+ * @param  {number} [options.collisionFilterMask=-1]
+ * @param  {number} [options.collisionFilterGroup=-1]
+ * @param  {boolean} [options.skipBackfaces=false]
+ * @param  {boolean} [options.checkCollisionResponse=true]
+ * @param  {RaycastResult} result
+ * @return {boolean} True if any body was hit.
+ */
+World.prototype.raycastAny = function(from, to, options, result){
+    options.mode = Ray.ANY;
+    options.from = from;
+    options.to = to;
+    options.result = result;
+    return tmpRay.intersectWorld(this, options);
+};
+
+/**
+ * Ray cast, and return information of the closest hit.
+ * @method raycastClosest
+ * @param  {Vec3} from
+ * @param  {Vec3} to
+ * @param  {Object} options
+ * @param  {number} [options.collisionFilterMask=-1]
+ * @param  {number} [options.collisionFilterGroup=-1]
+ * @param  {boolean} [options.skipBackfaces=false]
+ * @param  {boolean} [options.checkCollisionResponse=true]
+ * @param  {RaycastResult} result
+ * @return {boolean} True if any body was hit.
+ */
+World.prototype.raycastClosest = function(from, to, options, result){
+    options.mode = Ray.CLOSEST;
+    options.from = from;
+    options.to = to;
+    options.result = result;
+    return tmpRay.intersectWorld(this, options);
+};
+
+/**
+ * Remove a rigid body from the simulation.
+ * @method remove
+ * @param {Body} body
+ * @deprecated Use .removeBody instead
+ */
+World.prototype.remove = function(body){
+    body.world = null;
+    var n = this.bodies.length - 1,
+        bodies = this.bodies,
+        idx = bodies.indexOf(body);
+    if(idx !== -1){
+        bodies.splice(idx, 1); // Todo: should use a garbage free method
+
+        // Recompute index
+        for(var i=0; i!==bodies.length; i++){
+            bodies[i].index = i;
+        }
+
+        this.collisionMatrix.setNumObjects(n);
+        this.removeBodyEvent.body = body;
+        delete this.idToBodyMap[body.id];
+        this.dispatchEvent(this.removeBodyEvent);
+    }
+};
+
+/**
+ * Remove a rigid body from the simulation.
+ * @method removeBody
+ * @param {Body} body
+ */
+World.prototype.removeBody = World.prototype.remove;
+
+World.prototype.getBodyById = function(id){
+    return this.idToBodyMap[id];
+};
+
+// TODO Make a faster map
+World.prototype.getShapeById = function(id){
+    var bodies = this.bodies;
+    for(var i=0, bl = bodies.length; i<bl; i++){
+        var shapes = bodies[i].shapes;
+        for (var j = 0, sl = shapes.length; j < sl; j++) {
+            var shape = shapes[j];
+            if(shape.id === id){
+                return shape;
+            }
+        }
+    }
+};
+
+/**
+ * Adds a material to the World.
+ * @method addMaterial
+ * @param {Material} m
+ * @todo Necessary?
+ */
+World.prototype.addMaterial = function(m){
+    this.materials.push(m);
+};
+
+/**
+ * Adds a contact material to the World
+ * @method addContactMaterial
+ * @param {ContactMaterial} cmat
+ */
+World.prototype.addContactMaterial = function(cmat) {
+
+    // Add contact material
+    this.contactmaterials.push(cmat);
+
+    // Add current contact material to the material table
+    this.contactMaterialTable.set(cmat.materials[0].id,cmat.materials[1].id,cmat);
+};
+
+// performance.now()
+if(typeof performance === 'undefined'){
+    performance = {};
+}
+if(!performance.now){
+    var nowOffset = Date.now();
+    if (performance.timing && performance.timing.navigationStart){
+        nowOffset = performance.timing.navigationStart;
+    }
+    performance.now = function(){
+        return Date.now() - nowOffset;
+    };
+}
+
+var step_tmp1 = new Vec3();
+
+/**
+ * Step the physics world forward in time.
+ *
+ * There are two modes. The simple mode is fixed timestepping without interpolation. In this case you only use the first argument. The second case uses interpolation. In that you also provide the time since the function was last used, as well as the maximum fixed timesteps to take.
+ *
+ * @method step
+ * @param {Number} dt                       The fixed time step size to use.
+ * @param {Number} [timeSinceLastCalled]    The time elapsed since the function was last called.
+ * @param {Number} [maxSubSteps=10]         Maximum number of fixed steps to take per function call.
+ *
+ * @example
+ *     // fixed timestepping without interpolation
+ *     world.step(1/60);
+ *
+ * @see http://bulletphysics.org/mediawiki-1.5.8/index.php/Stepping_The_World
+ */
+World.prototype.step = function(dt, timeSinceLastCalled, maxSubSteps){
+    maxSubSteps = maxSubSteps || 10;
+    timeSinceLastCalled = timeSinceLastCalled || 0;
+
+    if(timeSinceLastCalled === 0){ // Fixed, simple stepping
+
+        this.internalStep(dt);
+
+        // Increment time
+        this.time += dt;
+
+    } else {
+
+        this.accumulator += timeSinceLastCalled;
+        var substeps = 0;
+        while (this.accumulator >= dt && substeps < maxSubSteps) {
+            // Do fixed steps to catch up
+            this.internalStep(dt);
+            this.accumulator -= dt;
+            substeps++;
+        }
+
+        var t = (this.accumulator % dt) / dt;
+        for(var j=0; j !== this.bodies.length; j++){
+            var b = this.bodies[j];
+            b.previousPosition.lerp(b.position, t, b.interpolatedPosition);
+            b.previousQuaternion.slerp(b.quaternion, t, b.interpolatedQuaternion);
+            b.previousQuaternion.normalize();
+        }
+        this.time += timeSinceLastCalled;
+    }
+};
+
+var
+    /**
+     * Dispatched after the world has stepped forward in time.
+     * @event postStep
+     */
+    World_step_postStepEvent = {type:"postStep"}, // Reusable event objects to save memory
+    /**
+     * Dispatched before the world steps forward in time.
+     * @event preStep
+     */
+    World_step_preStepEvent = {type:"preStep"},
+    World_step_collideEvent = {type:Body.COLLIDE_EVENT_NAME, body:null, contact:null },
+    World_step_oldContacts = [], // Pools for unused objects
+    World_step_frictionEquationPool = [],
+    World_step_p1 = [], // Reusable arrays for collision pairs
+    World_step_p2 = [],
+    World_step_gvec = new Vec3(), // Temporary vectors and quats
+    World_step_vi = new Vec3(),
+    World_step_vj = new Vec3(),
+    World_step_wi = new Vec3(),
+    World_step_wj = new Vec3(),
+    World_step_t1 = new Vec3(),
+    World_step_t2 = new Vec3(),
+    World_step_rixn = new Vec3(),
+    World_step_rjxn = new Vec3(),
+    World_step_step_q = new Quaternion(),
+    World_step_step_w = new Quaternion(),
+    World_step_step_wq = new Quaternion(),
+    invI_tau_dt = new Vec3();
+World.prototype.internalStep = function(dt){
+    this.dt = dt;
+
+    var world = this,
+        that = this,
+        contacts = this.contacts,
+        p1 = World_step_p1,
+        p2 = World_step_p2,
+        N = this.numObjects(),
+        bodies = this.bodies,
+        solver = this.solver,
+        gravity = this.gravity,
+        doProfiling = this.doProfiling,
+        profile = this.profile,
+        DYNAMIC = Body.DYNAMIC,
+        profilingStart,
+        constraints = this.constraints,
+        frictionEquationPool = World_step_frictionEquationPool,
+        gnorm = gravity.norm(),
+        gx = gravity.x,
+        gy = gravity.y,
+        gz = gravity.z,
+        i=0;
+
+    if(doProfiling){
+        profilingStart = performance.now();
+    }
+
+    // Add gravity to all objects
+    for(i=0; i!==N; i++){
+        var bi = bodies[i];
+        if(bi.type === DYNAMIC){ // Only for dynamic bodies
+            var f = bi.force, m = bi.mass;
+            f.x += m*gx;
+            f.y += m*gy;
+            f.z += m*gz;
+        }
+    }
+
+    // Update subsystems
+    for(var i=0, Nsubsystems=this.subsystems.length; i!==Nsubsystems; i++){
+        this.subsystems[i].update();
+    }
+
+    // Collision detection
+    if(doProfiling){ profilingStart = performance.now(); }
+    p1.length = 0; // Clean up pair arrays from last step
+    p2.length = 0;
+    this.broadphase.collisionPairs(this,p1,p2);
+    if(doProfiling){ profile.broadphase = performance.now() - profilingStart; }
+
+    // Remove constrained pairs with collideConnected == false
+    var Nconstraints = constraints.length;
+    for(i=0; i!==Nconstraints; i++){
+        var c = constraints[i];
+        if(!c.collideConnected){
+            for(var j = p1.length-1; j>=0; j-=1){
+                if( (c.bodyA === p1[j] && c.bodyB === p2[j]) ||
+                    (c.bodyB === p1[j] && c.bodyA === p2[j])){
+                    p1.splice(j, 1);
+                    p2.splice(j, 1);
+                }
+            }
+        }
+    }
+
+    this.collisionMatrixTick();
+
+    // Generate contacts
+    if(doProfiling){ profilingStart = performance.now(); }
+    var oldcontacts = World_step_oldContacts;
+    var NoldContacts = contacts.length;
+
+    for(i=0; i!==NoldContacts; i++){
+        oldcontacts.push(contacts[i]);
+    }
+    contacts.length = 0;
+
+    // Transfer FrictionEquation from current list to the pool for reuse
+    var NoldFrictionEquations = this.frictionEquations.length;
+    for(i=0; i!==NoldFrictionEquations; i++){
+        frictionEquationPool.push(this.frictionEquations[i]);
+    }
+    this.frictionEquations.length = 0;
+
+    this.narrowphase.getContacts(
+        p1,
+        p2,
+        this,
+        contacts,
+        oldcontacts, // To be reused
+        this.frictionEquations,
+        frictionEquationPool
+    );
+
+    if(doProfiling){
+        profile.narrowphase = performance.now() - profilingStart;
+    }
+
+    // Loop over all collisions
+    if(doProfiling){
+        profilingStart = performance.now();
+    }
+
+    // Add all friction eqs
+    for (var i = 0; i < this.frictionEquations.length; i++) {
+        solver.addEquation(this.frictionEquations[i]);
+    }
+
+    var ncontacts = contacts.length;
+    for(var k=0; k!==ncontacts; k++){
+
+        // Current contact
+        var c = contacts[k];
+
+        // Get current collision indeces
+        var bi = c.bi,
+            bj = c.bj,
+            si = c.si,
+            sj = c.sj;
+
+        // Get collision properties
+        var cm;
+        if(bi.material && bj.material){
+            cm = this.getContactMaterial(bi.material,bj.material) || this.defaultContactMaterial;
+        } else {
+            cm = this.defaultContactMaterial;
+        }
+
+        // c.enabled = bi.collisionResponse && bj.collisionResponse && si.collisionResponse && sj.collisionResponse;
+
+        var mu = cm.friction;
+        // c.restitution = cm.restitution;
+
+        // If friction or restitution were specified in the material, use them
+        if(bi.material && bj.material){
+            if(bi.material.friction >= 0 && bj.material.friction >= 0){
+                mu = bi.material.friction * bj.material.friction;
+            }
+
+            if(bi.material.restitution >= 0 && bj.material.restitution >= 0){
+                c.restitution = bi.material.restitution * bj.material.restitution;
+            }
+        }
+
+		// c.setSpookParams(
+  //           cm.contactEquationStiffness,
+  //           cm.contactEquationRelaxation,
+  //           dt
+  //       );
+
+		solver.addEquation(c);
+
+		// // Add friction constraint equation
+		// if(mu > 0){
+
+		// 	// Create 2 tangent equations
+		// 	var mug = mu * gnorm;
+		// 	var reducedMass = (bi.invMass + bj.invMass);
+		// 	if(reducedMass > 0){
+		// 		reducedMass = 1/reducedMass;
+		// 	}
+		// 	var pool = frictionEquationPool;
+		// 	var c1 = pool.length ? pool.pop() : new FrictionEquation(bi,bj,mug*reducedMass);
+		// 	var c2 = pool.length ? pool.pop() : new FrictionEquation(bi,bj,mug*reducedMass);
+		// 	this.frictionEquations.push(c1, c2);
+
+		// 	c1.bi = c2.bi = bi;
+		// 	c1.bj = c2.bj = bj;
+		// 	c1.minForce = c2.minForce = -mug*reducedMass;
+		// 	c1.maxForce = c2.maxForce = mug*reducedMass;
+
+		// 	// Copy over the relative vectors
+		// 	c1.ri.copy(c.ri);
+		// 	c1.rj.copy(c.rj);
+		// 	c2.ri.copy(c.ri);
+		// 	c2.rj.copy(c.rj);
+
+		// 	// Construct tangents
+		// 	c.ni.tangents(c1.t, c2.t);
+
+  //           // Set spook params
+  //           c1.setSpookParams(cm.frictionEquationStiffness, cm.frictionEquationRelaxation, dt);
+  //           c2.setSpookParams(cm.frictionEquationStiffness, cm.frictionEquationRelaxation, dt);
+
+  //           c1.enabled = c2.enabled = c.enabled;
+
+		// 	// Add equations to solver
+		// 	solver.addEquation(c1);
+		// 	solver.addEquation(c2);
+		// }
+
+        if( bi.allowSleep &&
+            bi.type === Body.DYNAMIC &&
+            bi.sleepState  === Body.SLEEPING &&
+            bj.sleepState  === Body.AWAKE &&
+            bj.type !== Body.STATIC
+        ){
+            var speedSquaredB = bj.velocity.norm2() + bj.angularVelocity.norm2();
+            var speedLimitSquaredB = Math.pow(bj.sleepSpeedLimit,2);
+            if(speedSquaredB >= speedLimitSquaredB*2){
+                bi._wakeUpAfterNarrowphase = true;
+            }
+        }
+
+        if( bj.allowSleep &&
+            bj.type === Body.DYNAMIC &&
+            bj.sleepState  === Body.SLEEPING &&
+            bi.sleepState  === Body.AWAKE &&
+            bi.type !== Body.STATIC
+        ){
+            var speedSquaredA = bi.velocity.norm2() + bi.angularVelocity.norm2();
+            var speedLimitSquaredA = Math.pow(bi.sleepSpeedLimit,2);
+            if(speedSquaredA >= speedLimitSquaredA*2){
+                bj._wakeUpAfterNarrowphase = true;
+            }
+        }
+
+        // Now we know that i and j are in contact. Set collision matrix state
+		this.collisionMatrix.set(bi, bj, true);
+
+        if (!this.collisionMatrixPrevious.get(bi, bj)) {
+            // First contact!
+            // We reuse the collideEvent object, otherwise we will end up creating new objects for each new contact, even if there's no event listener attached.
+            World_step_collideEvent.body = bj;
+            World_step_collideEvent.contact = c;
+            bi.dispatchEvent(World_step_collideEvent);
+
+            World_step_collideEvent.body = bi;
+            bj.dispatchEvent(World_step_collideEvent);
+        }
+
+        this.bodyOverlapKeeper.set(bi.id, bj.id);
+        this.shapeOverlapKeeper.set(si.id, sj.id);
+    }
+
+    this.emitContactEvents();
+
+    if(doProfiling){
+        profile.makeContactConstraints = performance.now() - profilingStart;
+        profilingStart = performance.now();
+    }
+
+    // Wake up bodies
+    for(i=0; i!==N; i++){
+        var bi = bodies[i];
+        if(bi._wakeUpAfterNarrowphase){
+            bi.wakeUp();
+            bi._wakeUpAfterNarrowphase = false;
+        }
+    }
+
+    // Add user-added constraints
+    var Nconstraints = constraints.length;
+    for(i=0; i!==Nconstraints; i++){
+        var c = constraints[i];
+        c.update();
+        for(var j=0, Neq=c.equations.length; j!==Neq; j++){
+            var eq = c.equations[j];
+            solver.addEquation(eq);
+        }
+    }
+
+    // Solve the constrained system
+    solver.solve(dt,this);
+
+    if(doProfiling){
+        profile.solve = performance.now() - profilingStart;
+    }
+
+    // Remove all contacts from solver
+    solver.removeAllEquations();
+
+    // Apply damping, see http://code.google.com/p/bullet/issues/detail?id=74 for details
+    var pow = Math.pow;
+    for(i=0; i!==N; i++){
+        var bi = bodies[i];
+        if(bi.type & DYNAMIC){ // Only for dynamic bodies
+            var ld = pow(1.0 - bi.linearDamping,dt);
+            var v = bi.velocity;
+            v.mult(ld,v);
+            var av = bi.angularVelocity;
+            if(av){
+                var ad = pow(1.0 - bi.angularDamping,dt);
+                av.mult(ad,av);
+            }
+        }
+    }
+
+    this.dispatchEvent(World_step_preStepEvent);
+
+    // Invoke pre-step callbacks
+    for(i=0; i!==N; i++){
+        var bi = bodies[i];
+        if(bi.preStep){
+            bi.preStep.call(bi);
+        }
+    }
+
+    // Leap frog
+    // vnew = v + h*f/m
+    // xnew = x + h*vnew
+    if(doProfiling){
+        profilingStart = performance.now();
+    }
+    var stepnumber = this.stepnumber;
+    var quatNormalize = stepnumber % (this.quatNormalizeSkip + 1) === 0;
+    var quatNormalizeFast = this.quatNormalizeFast;
+
+    for(i=0; i!==N; i++){
+        bodies[i].integrate(dt, quatNormalize, quatNormalizeFast);
+    }
+    this.clearForces();
+
+    this.broadphase.dirty = true;
+
+    if(doProfiling){
+        profile.integrate = performance.now() - profilingStart;
+    }
+
+    // Update world time
+    this.time += dt;
+    this.stepnumber += 1;
+
+    this.dispatchEvent(World_step_postStepEvent);
+
+    // Invoke post-step callbacks
+    for(i=0; i!==N; i++){
+        var bi = bodies[i];
+        var postStep = bi.postStep;
+        if(postStep){
+            postStep.call(bi);
+        }
+    }
+
+    // Sleeping update
+    if(this.allowSleep){
+        for(i=0; i!==N; i++){
+            bodies[i].sleepTick(this.time);
+        }
+    }
+};
+
+World.prototype.emitContactEvents = (function(){
+    var additions = [];
+    var removals = [];
+    var beginContactEvent = {
+        type: 'beginContact',
+        bodyA: null,
+        bodyB: null
+    };
+    var endContactEvent = {
+        type: 'endContact',
+        bodyA: null,
+        bodyB: null
+    };
+    var beginShapeContactEvent = {
+        type: 'beginShapeContact',
+        bodyA: null,
+        bodyB: null,
+        shapeA: null,
+        shapeB: null
+    };
+    var endShapeContactEvent = {
+        type: 'endShapeContact',
+        bodyA: null,
+        bodyB: null,
+        shapeA: null,
+        shapeB: null
+    };
+    return function(){
+        var hasBeginContact = this.hasAnyEventListener('beginContact');
+        var hasEndContact = this.hasAnyEventListener('endContact');
+
+        if(hasBeginContact || hasEndContact){
+            this.bodyOverlapKeeper.getDiff(additions, removals);
+        }
+
+        if(hasBeginContact){
+            for (var i = 0, l = additions.length; i < l; i += 2) {
+                beginContactEvent.bodyA = this.getBodyById(additions[i]);
+                beginContactEvent.bodyB = this.getBodyById(additions[i+1]);
+                this.dispatchEvent(beginContactEvent);
+            }
+            beginContactEvent.bodyA = beginContactEvent.bodyB = null;
+        }
+
+        if(hasEndContact){
+            for (var i = 0, l = removals.length; i < l; i += 2) {
+                endContactEvent.bodyA = this.getBodyById(removals[i]);
+                endContactEvent.bodyB = this.getBodyById(removals[i+1]);
+                this.dispatchEvent(endContactEvent);
+            }
+            endContactEvent.bodyA = endContactEvent.bodyB = null;
+        }
+
+        additions.length = removals.length = 0;
+
+        var hasBeginShapeContact = this.hasAnyEventListener('beginShapeContact');
+        var hasEndShapeContact = this.hasAnyEventListener('endShapeContact');
+
+        if(hasBeginShapeContact || hasEndShapeContact){
+            this.shapeOverlapKeeper.getDiff(additions, removals);
+        }
+
+        if(hasBeginShapeContact){
+            for (var i = 0, l = additions.length; i < l; i += 2) {
+                var shapeA = this.getShapeById(additions[i]);
+                var shapeB = this.getShapeById(additions[i+1]);
+                beginShapeContactEvent.shapeA = shapeA;
+                beginShapeContactEvent.shapeB = shapeB;
+                beginShapeContactEvent.bodyA = shapeA.body;
+                beginShapeContactEvent.bodyB = shapeB.body;
+                this.dispatchEvent(beginShapeContactEvent);
+            }
+            beginShapeContactEvent.bodyA = beginShapeContactEvent.bodyB = beginShapeContactEvent.shapeA = beginShapeContactEvent.shapeB = null;
+        }
+
+        if(hasEndShapeContact){
+            for (var i = 0, l = removals.length; i < l; i += 2) {
+                var shapeA = this.getShapeById(removals[i]);
+                var shapeB = this.getShapeById(removals[i+1]);
+                endShapeContactEvent.shapeA = shapeA;
+                endShapeContactEvent.shapeB = shapeB;
+                endShapeContactEvent.bodyA = shapeA.body;
+                endShapeContactEvent.bodyB = shapeB.body;
+                this.dispatchEvent(endShapeContactEvent);
+            }
+            endShapeContactEvent.bodyA = endShapeContactEvent.bodyB = endShapeContactEvent.shapeA = endShapeContactEvent.shapeB = null;
+        }
+
+    };
+})();
+
+/**
+ * Sets all body forces in the world to zero.
+ * @method clearForces
+ */
+World.prototype.clearForces = function(){
+    var bodies = this.bodies;
+    var N = bodies.length;
+    for(var i=0; i !== N; i++){
+        var b = bodies[i],
+            force = b.force,
+            tau = b.torque;
+
+        b.force.set(0,0,0);
+        b.torque.set(0,0,0);
+    }
+};
+
+},{"../collision/AABB":18,"../collision/ArrayCollisionMatrix":19,"../collision/NaiveBroadphase":22,"../collision/OverlapKeeper":24,"../collision/Ray":25,"../collision/RaycastResult":26,"../equations/ContactEquation":35,"../equations/FrictionEquation":37,"../material/ContactMaterial":40,"../material/Material":41,"../math/Quaternion":44,"../math/Vec3":46,"../objects/Body":47,"../shapes/Shape":59,"../solver/GSSolver":62,"../utils/EventTarget":65,"../utils/TupleDictionary":68,"./Narrowphase":71}],73:[function(require,module,exports){
+var CANNON = require('cannon'),
+    quickhull = require('./lib/THREE.quickhull');
+
+var PI_2 = Math.PI / 2;
+
+var Type = {
+  BOX: 'Box',
+  CYLINDER: 'Cylinder',
+  SPHERE: 'Sphere',
+  HULL: 'ConvexPolyhedron',
+  MESH: 'Trimesh'
+};
+
+/**
+ * Given a THREE.Object3D instance, creates a corresponding CANNON shape.
+ * @param  {THREE.Object3D} object
+ * @return {CANNON.Shape}
+ */
+module.exports = CANNON.mesh2shape = function (object, options) {
+  options = options || {};
+
+  var geometry;
+
+  if (options.type === Type.BOX) {
+    return createBoundingBoxShape(object);
+  } else if (options.type === Type.CYLINDER) {
+    return createBoundingCylinderShape(object, options);
+  } else if (options.type === Type.SPHERE) {
+    return createBoundingSphereShape(object, options);
+  } else if (options.type === Type.HULL) {
+    return createConvexPolyhedron(object);
+  } else if (options.type === Type.MESH) {
+    geometry = getGeometry(object);
+    return geometry ? createTrimeshShape(geometry) : null;
+  } else if (options.type) {
+    throw new Error('[CANNON.mesh2shape] Invalid type "%s".', options.type);
+  }
+
+  geometry = getGeometry(object);
+  if (!geometry) return null;
+
+  var type = geometry.metadata
+    ? geometry.metadata.type
+    : geometry.type;
+
+  switch (type) {
+    case 'BoxGeometry':
+    case 'BoxBufferGeometry':
+      return createBoxShape(geometry);
+    case 'CylinderGeometry':
+    case 'CylinderBufferGeometry':
+      return createCylinderShape(geometry);
+    case 'PlaneGeometry':
+    case 'PlaneBufferGeometry':
+      return createPlaneShape(geometry);
+    case 'SphereGeometry':
+    case 'SphereBufferGeometry':
+      return createSphereShape(geometry);
+    case 'TubeGeometry':
+    case 'Geometry':
+    case 'BufferGeometry':
+      return createBoundingBoxShape(object);
+    default:
+      console.warn('Unrecognized geometry: "%s". Using bounding box as shape.', geometry.type);
+      return createBoxShape(geometry);
+  }
+};
+
+CANNON.mesh2shape.Type = Type;
+
+/******************************************************************************
+ * Shape construction
+ */
+
+ /**
+  * @param  {THREE.Geometry} geometry
+  * @return {CANNON.Shape}
+  */
+ function createBoxShape (geometry) {
+   var vertices = getVertices(geometry);
+
+   if (!vertices.length) return null;
+
+   geometry.computeBoundingBox();
+   var box = geometry.boundingBox;
+   return new CANNON.Box(new CANNON.Vec3(
+     (box.max.x - box.min.x) / 2,
+     (box.max.y - box.min.y) / 2,
+     (box.max.z - box.min.z) / 2
+   ));
+ }
+
+/**
+ * Bounding box needs to be computed with the entire mesh, not just geometry.
+ * @param  {THREE.Object3D} mesh
+ * @return {CANNON.Shape}
+ */
+function createBoundingBoxShape (object) {
+  var shape, localPosition, worldPosition,
+      box = new THREE.Box3();
+
+  box.setFromObject(object);
+
+  if (!isFinite(box.min.lengthSq())) return null;
+
+  shape = new CANNON.Box(new CANNON.Vec3(
+    (box.max.x - box.min.x) / 2,
+    (box.max.y - box.min.y) / 2,
+    (box.max.z - box.min.z) / 2
+  ));
+
+  object.updateMatrixWorld();
+  worldPosition = new THREE.Vector3();
+  worldPosition.setFromMatrixPosition(object.matrixWorld);
+  localPosition = box.translate(worldPosition.negate()).getCenter();
+  if (localPosition.lengthSq()) {
+    shape.offset = localPosition;
+  }
+
+  return shape;
+}
+
+/**
+ * Computes 3D convex hull as a CANNON.ConvexPolyhedron.
+ * @param  {THREE.Object3D} mesh
+ * @return {CANNON.Shape}
+ */
+function createConvexPolyhedron (object) {
+  var i, vertices, faces, hull,
+      eps = 1e-4,
+      geometry = getGeometry(object);
+
+  if (!geometry || !geometry.vertices.length) return null;
+
+  // Perturb.
+  for (i = 0; i < geometry.vertices.length; i++) {
+    geometry.vertices[i].x += (Math.random() - 0.5) * eps;
+    geometry.vertices[i].y += (Math.random() - 0.5) * eps;
+    geometry.vertices[i].z += (Math.random() - 0.5) * eps;
+  }
+
+  // Compute the 3D convex hull.
+  hull = quickhull(geometry);
+
+  // Convert from THREE.Vector3 to CANNON.Vec3.
+  vertices = new Array(hull.vertices.length);
+  for (i = 0; i < hull.vertices.length; i++) {
+    vertices[i] = new CANNON.Vec3(hull.vertices[i].x, hull.vertices[i].y, hull.vertices[i].z);
+  }
+
+  // Convert from THREE.Face to Array<number>.
+  faces = new Array(hull.faces.length);
+  for (i = 0; i < hull.faces.length; i++) {
+    faces[i] = [hull.faces[i].a, hull.faces[i].b, hull.faces[i].c];
+  }
+
+  return new CANNON.ConvexPolyhedron(vertices, faces);
+}
+
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
+ */
+function createCylinderShape (geometry) {
+  var shape,
+      params = geometry.metadata
+        ? geometry.metadata.parameters
+        : geometry.parameters;
+  shape = new CANNON.Cylinder(
+    params.radiusTop,
+    params.radiusBottom,
+    params.height,
+    params.radialSegments
+  );
+
+  // Include metadata for serialization.
+  shape._type = CANNON.Shape.types.CYLINDER; // Patch schteppe/cannon.js#329.
+  shape.radiusTop = params.radiusTop;
+  shape.radiusBottom = params.radiusBottom;
+  shape.height = params.height;
+  shape.numSegments = params.radialSegments;
+
+  shape.orientation = new CANNON.Quaternion();
+  shape.orientation.setFromEuler(THREE.Math.degToRad(-90), 0, 0, 'XYZ').normalize();
+  return shape;
+}
+
+/**
+ * @param  {THREE.Object3D} object
+ * @return {CANNON.Shape}
+ */
+function createBoundingCylinderShape (object, options) {
+  var shape, height, radius,
+      box = new THREE.Box3(),
+      axes = ['x', 'y', 'z'],
+      majorAxis = options.cylinderAxis || 'y',
+      minorAxes = axes.splice(axes.indexOf(majorAxis), 1) && axes;
+
+  box.setFromObject(object);
+
+  if (!isFinite(box.min.lengthSq())) return null;
+
+  // Compute cylinder dimensions.
+  height = box.max[majorAxis] - box.min[majorAxis];
+  radius = 0.5 * Math.max(
+    box.max[minorAxes[0]] - box.min[minorAxes[0]],
+    box.max[minorAxes[1]] - box.min[minorAxes[1]]
+  );
+
+  // Create shape.
+  shape = new CANNON.Cylinder(radius, radius, height, 12);
+
+  // Include metadata for serialization.
+  shape._type = CANNON.Shape.types.CYLINDER; // Patch schteppe/cannon.js#329.
+  shape.radiusTop = radius;
+  shape.radiusBottom = radius;
+  shape.height = height;
+  shape.numSegments = 12;
+
+  shape.orientation = new CANNON.Quaternion();
+  shape.orientation.setFromEuler(
+    majorAxis === 'y' ? PI_2 : 0,
+    majorAxis === 'z' ? PI_2 : 0,
+    0,
+    'XYZ'
+  ).normalize();
+  return shape;
+}
+
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
+ */
+function createPlaneShape (geometry) {
+  geometry.computeBoundingBox();
+  var box = geometry.boundingBox;
+  return new CANNON.Box(new CANNON.Vec3(
+    (box.max.x - box.min.x) / 2 || 0.1,
+    (box.max.y - box.min.y) / 2 || 0.1,
+    (box.max.z - box.min.z) / 2 || 0.1
+  ));
+}
+
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
+ */
+function createSphereShape (geometry) {
+  var params = geometry.metadata
+    ? geometry.metadata.parameters
+    : geometry.parameters;
+  return new CANNON.Sphere(params.radius);
+}
+
+/**
+ * @param  {THREE.Object3D} object
+ * @return {CANNON.Shape}
+ */
+function createBoundingSphereShape (object, options) {
+  if (options.sphereRadius) {
+    return new CANNON.Sphere(options.sphereRadius);
+  }
+  var geometry = getGeometry(object);
+  if (!geometry) return null;
+  geometry.computeBoundingSphere();
+  return new CANNON.Sphere(geometry.boundingSphere.radius);
+}
+
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
+ */
+function createTrimeshShape (geometry) {
+  var indices,
+      vertices = getVertices(geometry);
+
+  if (!vertices.length) return null;
+
+  indices = Object.keys(vertices).map(Number);
+  return new CANNON.Trimesh(vertices, indices);
+}
+
+/******************************************************************************
+ * Utils
+ */
+
+/**
+ * Returns a single geometry for the given object. If the object is compound,
+ * its geometries are automatically merged.
+ * @param {THREE.Object3D} object
+ * @return {THREE.Geometry}
+ */
+function getGeometry (object) {
+  var matrix, mesh,
+      meshes = getMeshes(object),
+      tmp = new THREE.Geometry(),
+      combined = new THREE.Geometry();
+
+  if (meshes.length === 0) return null;
+
+  // Apply scale  – it can't easily be applied to a CANNON.Shape later.
+  if (meshes.length === 1) {
+    var position = new THREE.Vector3(),
+        quaternion = new THREE.Quaternion(),
+        scale = new THREE.Vector3();
+    if (meshes[0].geometry.isBufferGeometry) {
+      if (meshes[0].geometry.attributes.position) {
+        tmp.fromBufferGeometry(meshes[0].geometry);
+      }
+    } else {
+      tmp = meshes[0].geometry.clone();
+    }
+    tmp.metadata = meshes[0].geometry.metadata;
+    meshes[0].updateMatrixWorld();
+    meshes[0].matrixWorld.decompose(position, quaternion, scale);
+    return tmp.scale(scale.x, scale.y, scale.z);
+  }
+
+  // Recursively merge geometry, preserving local transforms.
+  while ((mesh = meshes.pop())) {
+    mesh.updateMatrixWorld();
+    if (mesh.geometry.isBufferGeometry) {
+      tmp.fromBufferGeometry(mesh.geometry);
+      combined.merge(tmp, mesh.matrixWorld);
+    } else {
+      combined.merge(mesh.geometry, mesh.matrixWorld);
+    }
+  }
+
+  matrix = new THREE.Matrix4();
+  matrix.scale(object.scale);
+  combined.applyMatrix(matrix);
+  return combined;
+}
+
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {Array<number>}
+ */
+function getVertices (geometry) {
+  if (!geometry.attributes) {
+    geometry = new THREE.BufferGeometry().fromGeometry(geometry);
+  }
+  return (geometry.attributes.position || {}).array || [];
+}
+
+/**
+ * Returns a flat array of THREE.Mesh instances from the given object. If
+ * nested transformations are found, they are applied to child meshes
+ * as mesh.userData.matrix, so that each mesh has its position/rotation/scale
+ * independently of all of its parents except the top-level object.
+ * @param  {THREE.Object3D} object
+ * @return {Array<THREE.Mesh>}
+ */
+function getMeshes (object) {
+  var meshes = [];
+  object.traverse(function (o) {
+    if (o.type === 'Mesh') {
+      meshes.push(o);
+    }
+  });
+  return meshes;
+}
+
+},{"./lib/THREE.quickhull":74,"cannon":17}],74:[function(require,module,exports){
+/**
+
+  QuickHull
+  ---------
+
+  The MIT License
+
+  Copyright &copy; 2010-2014 three.js authors
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+
+  THE SOFTWARE.
+
+
+    @author mark lundin / http://mark-lundin.com
+
+    This is a 3D implementation of the Quick Hull algorithm.
+    It is a fast way of computing a convex hull with average complexity
+    of O(n log(n)).
+    It uses depends on three.js and is supposed to create THREE.Geometry.
+
+    It's also very messy
+
+ */
+
+module.exports = (function(){
+
+
+  var faces     = [],
+    faceStack   = [],
+    i, NUM_POINTS, extremes,
+    max     = 0,
+    dcur, current, j, v0, v1, v2, v3,
+    N, D;
+
+  var ab, ac, ax,
+    suba, subb, normal,
+    diff, subaA, subaB, subC;
+
+  function reset(){
+
+    ab    = new THREE.Vector3(),
+    ac    = new THREE.Vector3(),
+    ax    = new THREE.Vector3(),
+    suba  = new THREE.Vector3(),
+    subb  = new THREE.Vector3(),
+    normal  = new THREE.Vector3(),
+    diff  = new THREE.Vector3(),
+    subaA = new THREE.Vector3(),
+    subaB = new THREE.Vector3(),
+    subC  = new THREE.Vector3();
+
+  }
+
+  //temporary vectors
+
+  function process( points ){
+
+    // Iterate through all the faces and remove
+    while( faceStack.length > 0  ){
+      cull( faceStack.shift(), points );
+    }
+  }
+
+
+  var norm = function(){
+
+    var ca = new THREE.Vector3(),
+      ba = new THREE.Vector3(),
+      N = new THREE.Vector3();
+
+    return function( a, b, c ){
+
+      ca.subVectors( c, a );
+      ba.subVectors( b, a );
+
+      N.crossVectors( ca, ba );
+
+      return N.normalize();
+    }
+
+  }();
+
+
+  function getNormal( face, points ){
+
+    if( face.normal !== undefined ) return face.normal;
+
+    var p0 = points[face[0]],
+      p1 = points[face[1]],
+      p2 = points[face[2]];
+
+    ab.subVectors( p1, p0 );
+    ac.subVectors( p2, p0 );
+    normal.crossVectors( ac, ab );
+    normal.normalize();
+
+    return face.normal = normal.clone();
+
+  }
+
+
+  function assignPoints( face, pointset, points ){
+
+    // ASSIGNING POINTS TO FACE
+    var p0 = points[face[0]],
+      dots = [], apex,
+      norm = getNormal( face, points );
+
+
+    // Sory all the points by there distance from the plane
+    pointset.sort( function( aItem, bItem ){
+
+
+      dots[aItem.x/3] = dots[aItem.x/3] !== undefined ? dots[aItem.x/3] : norm.dot( suba.subVectors( aItem, p0 ));
+      dots[bItem.x/3] = dots[bItem.x/3] !== undefined ? dots[bItem.x/3] : norm.dot( subb.subVectors( bItem, p0 ));
+
+      return dots[aItem.x/3] - dots[bItem.x/3] ;
+    });
+
+    //TODO :: Must be a faster way of finding and index in this array
+    var index = pointset.length;
+
+    if( index === 1 ) dots[pointset[0].x/3] = norm.dot( suba.subVectors( pointset[0], p0 ));
+    while( index-- > 0 && dots[pointset[index].x/3] > 0 )
+
+    var point;
+    if( index + 1 < pointset.length && dots[pointset[index+1].x/3] > 0 ){
+
+      face.visiblePoints  = pointset.splice( index + 1 );
+    }
+  }
+
+
+
+
+  function cull( face, points ){
+
+    var i = faces.length,
+      dot, visibleFace, currentFace,
+      visibleFaces = [face];
+
+    var apex = points.indexOf( face.visiblePoints.pop() );
+
+    // Iterate through all other faces...
+    while( i-- > 0 ){
+      currentFace = faces[i];
+      if( currentFace !== face ){
+        // ...and check if they're pointing in the same direction
+        dot = getNormal( currentFace, points ).dot( diff.subVectors( points[apex], points[currentFace[0]] ));
+        if( dot > 0 ){
+          visibleFaces.push( currentFace );
+        }
+      }
+    }
+
+    var index, neighbouringIndex, vertex;
+
+    // Determine Perimeter - Creates a bounded horizon
+
+    // 1. Pick an edge A out of all possible edges
+    // 2. Check if A is shared by any other face. a->b === b->a
+      // 2.1 for each edge in each triangle, isShared = ( f1.a == f2.a && f1.b == f2.b ) || ( f1.a == f2.b && f1.b == f2.a )
+    // 3. If not shared, then add to convex horizon set,
+        //pick an end point (N) of the current edge A and choose a new edge NA connected to A.
+        //Restart from 1.
+    // 4. If A is shared, it is not an horizon edge, therefore flag both faces that share this edge as candidates for culling
+    // 5. If candidate geometry is a degenrate triangle (ie. the tangent space normal cannot be computed) then remove that triangle from all further processing
+
+
+    var j = i = visibleFaces.length;
+    var isDistinct = false,
+      hasOneVisibleFace = i === 1,
+      cull = [],
+      perimeter = [],
+      edgeIndex = 0, compareFace, nextIndex,
+      a, b;
+
+    var allPoints = [];
+    var originFace = [visibleFaces[0][0], visibleFaces[0][1], visibleFaces[0][1], visibleFaces[0][2], visibleFaces[0][2], visibleFaces[0][0]];
+
+
+    if( visibleFaces.length === 1 ){
+      currentFace = visibleFaces[0];
+
+      perimeter = [currentFace[0], currentFace[1], currentFace[1], currentFace[2], currentFace[2], currentFace[0]];
+      // remove visible face from list of faces
+      if( faceStack.indexOf( currentFace ) > -1 ){
+        faceStack.splice( faceStack.indexOf( currentFace ), 1 );
+      }
+
+
+      if( currentFace.visiblePoints ) allPoints = allPoints.concat( currentFace.visiblePoints );
+      faces.splice( faces.indexOf( currentFace ), 1 );
+
+    }else{
+
+      while( i-- > 0  ){  // for each visible face
+
+        currentFace = visibleFaces[i];
+
+        // remove visible face from list of faces
+        if( faceStack.indexOf( currentFace ) > -1 ){
+          faceStack.splice( faceStack.indexOf( currentFace ), 1 );
+        }
+
+        if( currentFace.visiblePoints ) allPoints = allPoints.concat( currentFace.visiblePoints );
+        faces.splice( faces.indexOf( currentFace ), 1 );
+
+
+        var isSharedEdge;
+        cEdgeIndex = 0;
+
+        while( cEdgeIndex < 3 ){ // Iterate through it's edges
+
+          isSharedEdge = false;
+          j = visibleFaces.length;
+          a = currentFace[cEdgeIndex]
+          b = currentFace[(cEdgeIndex+1)%3];
+
+
+          while( j-- > 0 && !isSharedEdge ){ // find another visible faces
+
+            compareFace = visibleFaces[j];
+            edgeIndex = 0;
+
+            // isSharedEdge = compareFace == currentFace;
+            if( compareFace !== currentFace ){
+
+              while( edgeIndex < 3 && !isSharedEdge ){ //Check all it's indices
+
+                nextIndex = ( edgeIndex + 1 );
+                isSharedEdge = ( compareFace[edgeIndex] === a && compareFace[nextIndex%3] === b ) ||
+                         ( compareFace[edgeIndex] === b && compareFace[nextIndex%3] === a );
+
+                edgeIndex++;
+              }
+            }
+          }
+
+          if( !isSharedEdge || hasOneVisibleFace ){
+            perimeter.push( a );
+            perimeter.push( b );
+          }
+
+          cEdgeIndex++;
+        }
+      }
+    }
+
+    // create new face for all pairs around edge
+    i = 0;
+    var l = perimeter.length/2;
+    var f;
+
+    while( i < l ){
+      f = [ perimeter[i*2+1], apex, perimeter[i*2] ];
+      assignPoints( f, allPoints, points );
+      faces.push( f )
+      if( f.visiblePoints !== undefined  )faceStack.push( f );
+      i++;
+    }
+
+  }
+
+  var distSqPointSegment = function(){
+
+    var ab = new THREE.Vector3(),
+      ac = new THREE.Vector3(),
+      bc = new THREE.Vector3();
+
+    return function( a, b, c ){
+
+        ab.subVectors( b, a );
+        ac.subVectors( c, a );
+        bc.subVectors( c, b );
+
+        var e = ac.dot(ab);
+        if (e < 0.0) return ac.dot( ac );
+        var f = ab.dot( ab );
+        if (e >= f) return bc.dot(  bc );
+        return ac.dot( ac ) - e * e / f;
+
+      }
+
+  }();
+
+
+
+
+
+  return function( geometry ){
+
+    reset();
+
+
+    points    = geometry.vertices;
+    faces     = [],
+    faceStack   = [],
+    i       = NUM_POINTS = points.length,
+    extremes  = points.slice( 0, 6 ),
+    max     = 0;
+
+
+
+    /*
+     *  FIND EXTREMETIES
+     */
+    while( i-- > 0 ){
+      if( points[i].x < extremes[0].x ) extremes[0] = points[i];
+      if( points[i].x > extremes[1].x ) extremes[1] = points[i];
+
+      if( points[i].y < extremes[2].y ) extremes[2] = points[i];
+      if( points[i].y < extremes[3].y ) extremes[3] = points[i];
+
+      if( points[i].z < extremes[4].z ) extremes[4] = points[i];
+      if( points[i].z < extremes[5].z ) extremes[5] = points[i];
+    }
+
+
+    /*
+     *  Find the longest line between the extremeties
+     */
+
+    j = i = 6;
+    while( i-- > 0 ){
+      j = i - 1;
+      while( j-- > 0 ){
+          if( max < (dcur = extremes[i].distanceToSquared( extremes[j] )) ){
+        max = dcur;
+        v0 = extremes[ i ];
+        v1 = extremes[ j ];
+
+          }
+        }
+      }
+
+
+      // 3. Find the most distant point to the line segment, this creates a plane
+      i = 6;
+      max = 0;
+    while( i-- > 0 ){
+      dcur = distSqPointSegment( v0, v1, extremes[i]);
+      if( max < dcur ){
+        max = dcur;
+            v2 = extremes[ i ];
+          }
+    }
+
+
+      // 4. Find the most distant point to the plane.
+
+      N = norm(v0, v1, v2);
+      D = N.dot( v0 );
+
+
+      max = 0;
+      i = NUM_POINTS;
+      while( i-- > 0 ){
+        dcur = Math.abs( points[i].dot( N ) - D );
+          if( max < dcur ){
+            max = dcur;
+            v3 = points[i];
+      }
+      }
+
+
+
+      var v0Index = points.indexOf( v0 ),
+      v1Index = points.indexOf( v1 ),
+      v2Index = points.indexOf( v2 ),
+      v3Index = points.indexOf( v3 );
+
+
+    //  We now have a tetrahedron as the base geometry.
+    //  Now we must subdivide the
+
+      var tetrahedron =[
+        [ v2Index, v1Index, v0Index ],
+        [ v1Index, v3Index, v0Index ],
+        [ v2Index, v3Index, v1Index ],
+        [ v0Index, v3Index, v2Index ],
+    ];
+
+
+
+    subaA.subVectors( v1, v0 ).normalize();
+    subaB.subVectors( v2, v0 ).normalize();
+    subC.subVectors ( v3, v0 ).normalize();
+    var sign  = subC.dot( new THREE.Vector3().crossVectors( subaB, subaA ));
+
+
+    // Reverse the winding if negative sign
+    if( sign < 0 ){
+      tetrahedron[0].reverse();
+      tetrahedron[1].reverse();
+      tetrahedron[2].reverse();
+      tetrahedron[3].reverse();
+    }
+
+
+    //One for each face of the pyramid
+    var pointsCloned = points.slice();
+    pointsCloned.splice( pointsCloned.indexOf( v0 ), 1 );
+    pointsCloned.splice( pointsCloned.indexOf( v1 ), 1 );
+    pointsCloned.splice( pointsCloned.indexOf( v2 ), 1 );
+    pointsCloned.splice( pointsCloned.indexOf( v3 ), 1 );
+
+
+    var i = tetrahedron.length;
+    while( i-- > 0 ){
+      assignPoints( tetrahedron[i], pointsCloned, points );
+      if( tetrahedron[i].visiblePoints !== undefined ){
+        faceStack.push( tetrahedron[i] );
+      }
+      faces.push( tetrahedron[i] );
+    }
+
+    process( points );
+
+
+    //  Assign to our geometry object
+
+    var ll = faces.length;
+    while( ll-- > 0 ){
+      geometry.faces[ll] = new THREE.Face3( faces[ll][2], faces[ll][1], faces[ll][0], faces[ll].normal )
+    }
+
+    geometry.normalsNeedUpdate = true;
+
+    return geometry;
+
+  }
+
+}())
+
+},{}],75:[function(require,module,exports){
+var EPS = 0.1;
+
+module.exports = {
+  schema: {
+    enabled: {default: true},
+    mode: {default: 'teleport', oneOf: ['teleport', 'animate']},
+    animateSpeed: {default: 3.0}
+  },
+
+  init: function () {
+    this.active = true;
+    this.checkpoint = null;
+
+    this.offset = new THREE.Vector3();
+    this.position = new THREE.Vector3();
+    this.targetPosition = new THREE.Vector3();
+  },
+
+  play: function () { this.active = true; },
+  pause: function () { this.active = false; },
+
+  setCheckpoint: function (checkpoint) {
+    var el = this.el;
+
+    if (!this.active) return;
+    if (this.checkpoint === checkpoint) return;
+
+    if (this.checkpoint) {
+      el.emit('navigation-end', {checkpoint: this.checkpoint});
+    }
+
+    this.checkpoint = checkpoint;
+    this.sync();
+
+    // Ignore new checkpoint if we're already there.
+    if (this.position.distanceTo(this.targetPosition) < EPS) {
+      this.checkpoint = null;
+      return;
+    }
+
+    el.emit('navigation-start', {checkpoint: checkpoint});
+
+    if (this.data.mode === 'teleport') {
+      this.el.setAttribute('position', this.targetPosition);
+      this.checkpoint = null;
+      el.emit('navigation-end', {checkpoint: checkpoint});
+    }
+  },
+
+  isVelocityActive: function () {
+    return !!(this.active && this.checkpoint);
+  },
+
+  getVelocity: function () {
+    if (!this.active) return;
+
+    var data = this.data,
+        offset = this.offset,
+        position = this.position,
+        targetPosition = this.targetPosition,
+        checkpoint = this.checkpoint;
+
+    this.sync();
+    if (position.distanceTo(targetPosition) < EPS) {
+      this.checkpoint = null;
+      this.el.emit('navigation-end', {checkpoint: checkpoint});
+      return offset.set(0, 0, 0);
+    }
+    offset.setLength(data.animateSpeed);
+    return offset;
+  },
+
+  sync: function () {
+    var offset = this.offset,
+        position = this.position,
+        targetPosition = this.targetPosition;
+
+    position.copy(this.el.getAttribute('position'));
+    targetPosition.copy(this.checkpoint.object3D.getWorldPosition());
+    targetPosition.add(this.checkpoint.components.checkpoint.getOffset());
+    offset.copy(targetPosition).sub(position);
+  }
+};
+
+},{}],76:[function(require,module,exports){
+/**
+ * Gamepad controls for A-Frame.
+ *
+ * Stripped-down version of: https://github.com/donmccurdy/aframe-gamepad-controls
+ *
+ * For more information about the Gamepad API, see:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
+ */
+
+var GamepadButton = require('../../lib/GamepadButton'),
+    GamepadButtonEvent = require('../../lib/GamepadButtonEvent');
+
+var JOYSTICK_EPS = 0.2;
+
+module.exports = {
+
+  /*******************************************************************
+   * Statics
+   */
+
+  GamepadButton: GamepadButton,
+
+  /*******************************************************************
+   * Schema
+   */
+
+  schema: {
+    // Controller 0-3
+    controller:        { default: 0, oneOf: [0, 1, 2, 3] },
+
+    // Enable/disable features
+    enabled:           { default: true },
+
+    // Debugging
+    debug:             { default: false }
+  },
+
+  /*******************************************************************
+   * Core
+   */
+
+  /**
+   * Called once when component is attached. Generally for initial setup.
+   */
+  init: function () {
+    var scene = this.el.sceneEl;
+    this.prevTime = window.performance.now();
+
+    // Button state
+    this.buttons = {};
+
+    scene.addBehavior(this);
+  },
+
+  /**
+   * Called when component is attached and when component data changes.
+   * Generally modifies the entity based on the data.
+   */
+  update: function () { this.tick(); },
+
+  /**
+   * Called on each iteration of main render loop.
+   */
+  tick: function () {
+    this.updateButtonState();
+  },
+
+  /**
+   * Called when a component is removed (e.g., via removeAttribute).
+   * Generally undoes all modifications to the entity.
+   */
+  remove: function () { },
+
+  /*******************************************************************
+   * Universal controls - movement
+   */
+
+  isVelocityActive: function () {
+    if (!this.data.enabled || !this.isConnected()) return false;
+
+    var dpad = this.getDpad(),
+        joystick0 = this.getJoystick(0),
+        inputX = dpad.x || joystick0.x,
+        inputY = dpad.y || joystick0.y;
+
+    return Math.abs(inputX) > JOYSTICK_EPS || Math.abs(inputY) > JOYSTICK_EPS;
+  },
+
+  getVelocityDelta: function () {
+    var dpad = this.getDpad(),
+        joystick0 = this.getJoystick(0),
+        inputX = dpad.x || joystick0.x,
+        inputY = dpad.y || joystick0.y,
+        dVelocity = new THREE.Vector3();
+
+    if (Math.abs(inputX) > JOYSTICK_EPS) {
+      dVelocity.x += inputX;
+    }
+    if (Math.abs(inputY) > JOYSTICK_EPS) {
+      dVelocity.z += inputY;
+    }
+
+    return dVelocity;
+  },
+
+  /*******************************************************************
+   * Universal controls - rotation
+   */
+
+  isRotationActive: function () {
+    if (!this.data.enabled || !this.isConnected()) return false;
+
+    var joystick1 = this.getJoystick(1);
+
+    return Math.abs(joystick1.x) > JOYSTICK_EPS || Math.abs(joystick1.y) > JOYSTICK_EPS;
+  },
+
+  getRotationDelta: function () {
+    var lookVector = this.getJoystick(1);
+    if (Math.abs(lookVector.x) <= JOYSTICK_EPS) lookVector.x = 0;
+    if (Math.abs(lookVector.y) <= JOYSTICK_EPS) lookVector.y = 0;
+    return lookVector;
+  },
+
+  /*******************************************************************
+   * Button events
+   */
+
+  updateButtonState: function () {
+    var gamepad = this.getGamepad();
+    if (this.data.enabled && gamepad) {
+
+      // Fire DOM events for button state changes.
+      for (var i = 0; i < gamepad.buttons.length; i++) {
+        if (gamepad.buttons[i].pressed && !this.buttons[i]) {
+          this.emit(new GamepadButtonEvent('gamepadbuttondown', i, gamepad.buttons[i]));
+        } else if (!gamepad.buttons[i].pressed && this.buttons[i]) {
+          this.emit(new GamepadButtonEvent('gamepadbuttonup', i, gamepad.buttons[i]));
+        }
+        this.buttons[i] = gamepad.buttons[i].pressed;
+      }
+
+    } else if (Object.keys(this.buttons)) {
+      // Reset state if controls are disabled or controller is lost.
+      this.buttons = {};
+    }
+  },
+
+  emit: function (event) {
+    // Emit original event.
+    this.el.emit(event.type, event);
+
+    // Emit convenience event, identifying button index.
+    this.el.emit(
+      event.type + ':' + event.index,
+      new GamepadButtonEvent(event.type, event.index, event)
+    );
+  },
+
+  /*******************************************************************
+   * Gamepad state
+   */
+
+  /**
+   * Returns the Gamepad instance attached to the component. If connected,
+   * a proxy-controls component may provide access to Gamepad input from a
+   * remote device.
+   *
+   * @return {Gamepad}
+   */
+  getGamepad: function () {
+    var localGamepad = navigator.getGamepads
+          && navigator.getGamepads()[this.data.controller],
+        proxyControls = this.el.sceneEl.components['proxy-controls'],
+        proxyGamepad = proxyControls && proxyControls.isConnected()
+          && proxyControls.getGamepad(this.data.controller);
+    return proxyGamepad || localGamepad;
+  },
+
+  /**
+   * Returns the state of the given button.
+   * @param  {number} index The button (0-N) for which to find state.
+   * @return {GamepadButton}
+   */
+  getButton: function (index) {
+    return this.getGamepad().buttons[index];
+  },
+
+  /**
+   * Returns state of the given axis. Axes are labelled 0-N, where 0-1 will
+   * represent X/Y on the first joystick, and 2-3 X/Y on the second.
+   * @param  {number} index The axis (0-N) for which to find state.
+   * @return {number} On the interval [-1,1].
+   */
+  getAxis: function (index) {
+    return this.getGamepad().axes[index];
+  },
+
+  /**
+   * Returns the state of the given joystick (0 or 1) as a THREE.Vector2.
+   * @param  {number} id The joystick (0, 1) for which to find state.
+   * @return {THREE.Vector2}
+   */
+  getJoystick: function (index) {
+    var gamepad = this.getGamepad();
+    switch (index) {
+      case 0: return new THREE.Vector2(gamepad.axes[0], gamepad.axes[1]);
+      case 1: return new THREE.Vector2(gamepad.axes[2], gamepad.axes[3]);
+      default: throw new Error('Unexpected joystick index "%d".', index);
+    }
+  },
+
+  /**
+   * Returns the state of the dpad as a THREE.Vector2.
+   * @return {THREE.Vector2}
+   */
+  getDpad: function () {
+    var gamepad = this.getGamepad();
+    if (!gamepad.buttons[GamepadButton.DPAD_RIGHT]) {
+      return new THREE.Vector2();
+    }
+    return new THREE.Vector2(
+      (gamepad.buttons[GamepadButton.DPAD_RIGHT].pressed ? 1 : 0)
+      + (gamepad.buttons[GamepadButton.DPAD_LEFT].pressed ? -1 : 0),
+      (gamepad.buttons[GamepadButton.DPAD_UP].pressed ? -1 : 0)
+      + (gamepad.buttons[GamepadButton.DPAD_DOWN].pressed ? 1 : 0)
+    );
+  },
+
+  /**
+   * Returns true if the gamepad is currently connected to the system.
+   * @return {boolean}
+   */
+  isConnected: function () {
+    var gamepad = this.getGamepad();
+    return !!(gamepad && gamepad.connected);
+  },
+
+  /**
+   * Returns a string containing some information about the controller. Result
+   * may vary across browsers, for a given controller.
+   * @return {string}
+   */
+  getID: function () {
+    return this.getGamepad().id;
+  }
+};
+
+},{"../../lib/GamepadButton":2,"../../lib/GamepadButtonEvent":3}],77:[function(require,module,exports){
+var radToDeg = THREE.Math.radToDeg,
+    isMobile = AFRAME.utils.device.isMobile();
+
+module.exports = {
+  schema: {
+    enabled: {default: true},
+    standing: {default: true}
+  },
+
+  init: function () {
+    this.isPositionCalibrated = false;
+    this.dolly = new THREE.Object3D();
+    this.hmdEuler = new THREE.Euler();
+    this.previousHMDPosition = new THREE.Vector3();
+    this.deltaHMDPosition = new THREE.Vector3();
+    this.vrControls = new THREE.VRControls(this.dolly);
+    this.rotation = new THREE.Vector3();
+  },
+
+  update: function () {
+    var data = this.data;
+    var vrControls = this.vrControls;
+    vrControls.standing = data.standing;
+    vrControls.update();
+  },
+
+  tick: function () {
+    this.vrControls.update();
+  },
+
+  remove: function () {
+    this.vrControls.dispose();
+  },
+
+  isRotationActive: function () {
+    var hmdEuler = this.hmdEuler;
+    if (!this.data.enabled || !(this.el.sceneEl.is('vr-mode') || isMobile)) {
+      return false;
+    }
+    hmdEuler.setFromQuaternion(this.dolly.quaternion, 'YXZ');
+    return !isNullVector(hmdEuler);
+  },
+
+  getRotation: function () {
+    var hmdEuler = this.hmdEuler;
+    return this.rotation.set(
+      radToDeg(hmdEuler.x),
+      radToDeg(hmdEuler.y),
+      radToDeg(hmdEuler.z)
+    );
+  },
+
+  isVelocityActive: function () {
+    var deltaHMDPosition = this.deltaHMDPosition;
+    var previousHMDPosition = this.previousHMDPosition;
+    var currentHMDPosition = this.calculateHMDPosition();
+    this.isPositionCalibrated = this.isPositionCalibrated || !isNullVector(previousHMDPosition);
+    if (!this.data.enabled || !this.el.sceneEl.is('vr-mode') || isMobile) {
+      return false;
+    }
+    deltaHMDPosition.copy(currentHMDPosition).sub(previousHMDPosition);
+    previousHMDPosition.copy(currentHMDPosition);
+    return this.isPositionCalibrated && !isNullVector(deltaHMDPosition);
+  },
+
+  getPositionDelta: function () {
+    return this.deltaHMDPosition;
+  },
+
+  calculateHMDPosition: function () {
+    var dolly = this.dolly;
+    var position = new THREE.Vector3();
+    dolly.updateMatrix();
+    position.setFromMatrixPosition(dolly.matrix);
+    return position;
+  }
+};
+
+function isNullVector (vector) {
+  return vector.x === 0 && vector.y === 0 && vector.z === 0;
+}
+
+},{}],78:[function(require,module,exports){
+var physics = require('aframe-physics-system');
+
+module.exports = {
+  'checkpoint-controls': require('./checkpoint-controls'),
+  'gamepad-controls':    require('./gamepad-controls'),
+  'hmd-controls':        require('./hmd-controls'),
+  'keyboard-controls':   require('./keyboard-controls'),
+  'mouse-controls':      require('./mouse-controls'),
+  'touch-controls':      require('./touch-controls'),
+  'universal-controls':  require('./universal-controls'),
+
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
+
+    AFRAME = AFRAME || window.AFRAME;
+
+    physics.registerAll();
+    if (!AFRAME.components['checkpoint-controls'])  AFRAME.registerComponent('checkpoint-controls', this['checkpoint-controls']);
+    if (!AFRAME.components['gamepad-controls'])     AFRAME.registerComponent('gamepad-controls',    this['gamepad-controls']);
+    if (!AFRAME.components['hmd-controls'])         AFRAME.registerComponent('hmd-controls',        this['hmd-controls']);
+    if (!AFRAME.components['keyboard-controls'])    AFRAME.registerComponent('keyboard-controls',   this['keyboard-controls']);
+    if (!AFRAME.components['mouse-controls'])       AFRAME.registerComponent('mouse-controls',      this['mouse-controls']);
+    if (!AFRAME.components['touch-controls'])       AFRAME.registerComponent('touch-controls',      this['touch-controls']);
+    if (!AFRAME.components['universal-controls'])   AFRAME.registerComponent('universal-controls',  this['universal-controls']);
+
+    this._registered = true;
+  }
+};
+
+},{"./checkpoint-controls":75,"./gamepad-controls":76,"./hmd-controls":77,"./keyboard-controls":79,"./mouse-controls":80,"./touch-controls":81,"./universal-controls":82,"aframe-physics-system":5}],79:[function(require,module,exports){
+require('../../lib/keyboard.polyfill');
+
+var MAX_DELTA = 0.2,
+    PROXY_FLAG = '__keyboard-controls-proxy';
+
+var KeyboardEvent = window.KeyboardEvent;
+
+/**
+ * Keyboard Controls component.
+ *
+ * Stripped-down version of: https://github.com/donmccurdy/aframe-keyboard-controls
+ *
+ * Bind keyboard events to components, or control your entities with the WASD keys.
+ *
+ * Why use KeyboardEvent.code? "This is set to a string representing the key that was pressed to
+ * generate the KeyboardEvent, without taking the current keyboard layout (e.g., QWERTY vs.
+ * Dvorak), locale (e.g., English vs. French), or any modifier keys into account. This is useful
+ * when you care about which physical key was pressed, rather thanwhich character it corresponds
+ * to. For example, if you’re a writing a game, you might want a certain set of keys to move the
+ * player in different directions, and that mapping should ideally be independent of keyboard
+ * layout. See: https://developers.google.com/web/updates/2016/04/keyboardevent-keys-codes
+ *
+ * @namespace wasd-controls
+ * keys the entity moves and if you release it will stop. Easing simulates friction.
+ * to the entity when pressing the keys.
+ * @param {bool} [enabled=true] - To completely enable or disable the controls
+ */
+module.exports = {
+  schema: {
+    enabled:           { default: true },
+    debug:             { default: false }
+  },
+
+  init: function () {
+    this.dVelocity = new THREE.Vector3();
+    this.localKeys = {};
+    this.listeners = {
+      keydown: this.onKeyDown.bind(this),
+      keyup: this.onKeyUp.bind(this),
+      blur: this.onBlur.bind(this)
+    };
+    this.attachEventListeners();
+  },
+
+  /*******************************************************************
+  * Movement
+  */
+
+  isVelocityActive: function () {
+    return this.data.enabled && !!Object.keys(this.getKeys()).length;
+  },
+
+  getVelocityDelta: function () {
+    var data = this.data,
+        keys = this.getKeys();
+
+    this.dVelocity.set(0, 0, 0);
+    if (data.enabled) {
+      if (keys.KeyW || keys.ArrowUp)    { this.dVelocity.z -= 1; }
+      if (keys.KeyA || keys.ArrowLeft)  { this.dVelocity.x -= 1; }
+      if (keys.KeyS || keys.ArrowDown)  { this.dVelocity.z += 1; }
+      if (keys.KeyD || keys.ArrowRight) { this.dVelocity.x += 1; }
+    }
+
+    return this.dVelocity.clone();
+  },
+
+  /*******************************************************************
+  * Events
+  */
+
+  play: function () {
+    this.attachEventListeners();
+  },
+
+  pause: function () {
+    this.removeEventListeners();
+  },
+
+  remove: function () {
+    this.pause();
+  },
+
+  attachEventListeners: function () {
+    window.addEventListener('keydown', this.listeners.keydown, false);
+    window.addEventListener('keyup', this.listeners.keyup, false);
+    window.addEventListener('blur', this.listeners.blur, false);
+  },
+
+  removeEventListeners: function () {
+    window.removeEventListener('keydown', this.listeners.keydown);
+    window.removeEventListener('keyup', this.listeners.keyup);
+    window.removeEventListener('blur', this.listeners.blur);
+  },
+
+  onKeyDown: function (event) {
+    if (AFRAME.utils.shouldCaptureKeyEvent(event)) {
+      this.localKeys[event.code] = true;
+      this.emit(event);
+    }
+  },
+
+  onKeyUp: function (event) {
+    if (AFRAME.utils.shouldCaptureKeyEvent(event)) {
+      delete this.localKeys[event.code];
+      this.emit(event);
+    }
+  },
+
+  onBlur: function () {
+    for (var code in this.localKeys) {
+      if (this.localKeys.hasOwnProperty(code)) {
+        delete this.localKeys[code];
+      }
+    }
+  },
+
+  emit: function (event) {
+    // TODO - keydown only initially?
+    // TODO - where the f is the spacebar
+
+    // Emit original event.
+    if (PROXY_FLAG in event) {
+      // TODO - Method never triggered.
+      this.el.emit(event.type, event);
+    }
+
+    // Emit convenience event, identifying key.
+    this.el.emit(event.type + ':' + event.code, new KeyboardEvent(event.type, event));
+    if (this.data.debug) console.log(event.type + ':' + event.code);
+  },
+
+  /*******************************************************************
+  * Accessors
+  */
+
+  isPressed: function (code) {
+    return code in this.getKeys();
+  },
+
+  getKeys: function () {
+    if (this.isProxied()) {
+      return this.el.sceneEl.components['proxy-controls'].getKeyboard();
+    }
+    return this.localKeys;
+  },
+
+  isProxied: function () {
+    var proxyControls = this.el.sceneEl.components['proxy-controls'];
+    return proxyControls && proxyControls.isConnected();
+  }
+
+};
+
+},{"../../lib/keyboard.polyfill":4}],80:[function(require,module,exports){
+document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
+
+/**
+ * Mouse + Pointerlock controls.
+ *
+ * Based on: https://github.com/aframevr/aframe/pull/1056
+ */
+module.exports = {
+  schema: {
+    enabled: { default: true },
+    pointerlockEnabled: { default: true },
+    sensitivity: { default: 1 / 25 }
+  },
+
+  init: function () {
+    this.mouseDown = false;
+    this.pointerLocked = false;
+    this.lookVector = new THREE.Vector2();
+    this.bindMethods();
+  },
+
+  update: function (previousData) {
+    var data = this.data;
+    if (previousData.pointerlockEnabled && !data.pointerlockEnabled && this.pointerLocked) {
+      document.exitPointerLock();
+    }
+  },
+
+  play: function () {
+    this.addEventListeners();
+  },
+
+  pause: function () {
+    this.removeEventListeners();
+    this.lookVector.set(0, 0);
+  },
+
+  remove: function () {
+    this.pause();
+  },
+
+  bindMethods: function () {
+    this.onMouseDown = this.onMouseDown.bind(this);
+    this.onMouseMove = this.onMouseMove.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+    this.onPointerLockChange = this.onPointerLockChange.bind(this);
+    this.onPointerLockChange = this.onPointerLockChange.bind(this);
+    this.onPointerLockChange = this.onPointerLockChange.bind(this);
+  },
+
+  addEventListeners: function () {
+    var sceneEl = this.el.sceneEl;
+    var canvasEl = sceneEl.canvas;
+    var data = this.data;
+
+    if (!canvasEl) {
+      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
+      return;
+    }
+
+    canvasEl.addEventListener('mousedown', this.onMouseDown, false);
+    canvasEl.addEventListener('mousemove', this.onMouseMove, false);
+    canvasEl.addEventListener('mouseup', this.onMouseUp, false);
+    canvasEl.addEventListener('mouseout', this.onMouseUp, false);
+
+    if (data.pointerlockEnabled) {
+      document.addEventListener('pointerlockchange', this.onPointerLockChange, false);
+      document.addEventListener('mozpointerlockchange', this.onPointerLockChange, false);
+      document.addEventListener('pointerlockerror', this.onPointerLockError, false);
+    }
+  },
+
+  removeEventListeners: function () {
+    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
+    if (canvasEl) {
+      canvasEl.removeEventListener('mousedown', this.onMouseDown, false);
+      canvasEl.removeEventListener('mousemove', this.onMouseMove, false);
+      canvasEl.removeEventListener('mouseup', this.onMouseUp, false);
+      canvasEl.removeEventListener('mouseout', this.onMouseUp, false);
+    }
+    document.removeEventListener('pointerlockchange', this.onPointerLockChange, false);
+    document.removeEventListener('mozpointerlockchange', this.onPointerLockChange, false);
+    document.removeEventListener('pointerlockerror', this.onPointerLockError, false);
+  },
+
+  isRotationActive: function () {
+    return this.data.enabled && (this.mouseDown || this.pointerLocked);
+  },
+
+  /**
+   * Returns the sum of all mouse movement since last call.
+   */
+  getRotationDelta: function () {
+    var dRotation = this.lookVector.clone().multiplyScalar(this.data.sensitivity);
+    this.lookVector.set(0, 0);
+    return dRotation;
+  },
+
+  onMouseMove: function (event) {
+    var previousMouseEvent = this.previousMouseEvent;
+
+    if (!this.data.enabled || !(this.mouseDown || this.pointerLocked)) {
+      return;
+    }
+
+    var movementX = event.movementX || event.mozMovementX || 0;
+    var movementY = event.movementY || event.mozMovementY || 0;
+
+    if (!this.pointerLocked) {
+      movementX = event.screenX - previousMouseEvent.screenX;
+      movementY = event.screenY - previousMouseEvent.screenY;
+    }
+
+    this.lookVector.x += movementX;
+    this.lookVector.y += movementY;
+
+    this.previousMouseEvent = event;
+  },
+
+  onMouseDown: function (event) {
+    var canvasEl = this.el.sceneEl.canvas,
+        isEditing = (AFRAME.INSPECTOR || {}).opened;
+
+    this.mouseDown = true;
+    this.previousMouseEvent = event;
+
+    if (this.data.pointerlockEnabled && !this.pointerLocked && !isEditing) {
+      if (canvasEl.requestPointerLock) {
+        canvasEl.requestPointerLock();
+      } else if (canvasEl.mozRequestPointerLock) {
+        canvasEl.mozRequestPointerLock();
+      }
+    }
+  },
+
+  onMouseUp: function () {
+    this.mouseDown = false;
+  },
+
+  onPointerLockChange: function () {
+    this.pointerLocked = !!(document.pointerLockElement || document.mozPointerLockElement);
+  },
+
+  onPointerLockError: function () {
+    this.pointerLocked = false;
+  }
+};
+
+},{}],81:[function(require,module,exports){
+module.exports = {
+  schema: {
+    enabled: { default: true }
+  },
+
+  init: function () {
+    this.dVelocity = new THREE.Vector3();
+    this.bindMethods();
+  },
+
+  play: function () {
+    this.addEventListeners();
+  },
+
+  pause: function () {
+    this.removeEventListeners();
+    this.dVelocity.set(0, 0, 0);
+  },
+
+  remove: function () {
+    this.pause();
+  },
+
+  addEventListeners: function () {
+    var sceneEl = this.el.sceneEl;
+    var canvasEl = sceneEl.canvas;
+
+    if (!canvasEl) {
+      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
+      return;
+    }
+
+    canvasEl.addEventListener('touchstart', this.onTouchStart);
+    canvasEl.addEventListener('touchend', this.onTouchEnd);
+  },
+
+  removeEventListeners: function () {
+    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
+    if (!canvasEl) { return; }
+
+    canvasEl.removeEventListener('touchstart', this.onTouchStart);
+    canvasEl.removeEventListener('touchend', this.onTouchEnd);
+  },
+
+  isVelocityActive: function () {
+    return this.data.enabled && this.isMoving;
+  },
+
+  getVelocityDelta: function () {
+    this.dVelocity.z = this.isMoving ? -1 : 0;
+    return this.dVelocity.clone();
+  },
+
+  bindMethods: function () {
+    this.onTouchStart = this.onTouchStart.bind(this);
+    this.onTouchEnd = this.onTouchEnd.bind(this);
+  },
+
+  onTouchStart: function (e) {
+    this.isMoving = true;
+    e.preventDefault();
+  },
+
+  onTouchEnd: function (e) {
+    this.isMoving = false;
+    e.preventDefault();
+  }
+};
+
+},{}],82:[function(require,module,exports){
+/**
+ * Universal Controls
+ *
+ * @author Don McCurdy <dm@donmccurdy.com>
+ */
+
+var COMPONENT_SUFFIX = '-controls',
+    MAX_DELTA = 0.2, // ms
+    PI_2 = Math.PI / 2;
+
+module.exports = {
+
+  /*******************************************************************
+   * Schema
+   */
+
+  dependencies: ['velocity', 'rotation'],
+
+  schema: {
+    enabled:              { default: true },
+    movementEnabled:      { default: true },
+    movementControls:     { default: ['gamepad', 'keyboard', 'touch', 'hmd'] },
+    rotationEnabled:      { default: true },
+    rotationControls:     { default: ['hmd', 'gamepad', 'mouse'] },
+    movementSpeed:        { default: 5 }, // m/s
+    movementEasing:       { default: 15 }, // m/s2
+    movementEasingY:      { default: 0  }, // m/s2
+    movementAcceleration: { default: 80 }, // m/s2
+    rotationSensitivity:  { default: 0.05 }, // radians/frame, ish
+    fly:                  { default: false },
+  },
+
+  /*******************************************************************
+   * Lifecycle
+   */
+
+  init: function () {
+    var rotation = this.el.getAttribute('rotation');
+
+    if (this.el.hasAttribute('look-controls') && this.data.rotationEnabled) {
+      console.error('[universal-controls] The `universal-controls` component is a replacement '
+        + 'for `look-controls`, and cannot be used in combination with it.');
+    }
+
+    // Movement
+    this.velocity = new THREE.Vector3();
+
+    // Rotation
+    this.pitch = new THREE.Object3D();
+    this.pitch.rotation.x = THREE.Math.degToRad(rotation.x);
+    this.yaw = new THREE.Object3D();
+    this.yaw.position.y = 10;
+    this.yaw.rotation.y = THREE.Math.degToRad(rotation.y);
+    this.yaw.add(this.pitch);
+    this.heading = new THREE.Euler(0, 0, 0, 'YXZ');
+
+    if (this.el.sceneEl.hasLoaded) {
+      this.injectControls();
+    } else {
+      this.el.sceneEl.addEventListener('loaded', this.injectControls.bind(this));
+    }
+  },
+
+  update: function () {
+    if (this.el.sceneEl.hasLoaded) {
+      this.injectControls();
+    }
+  },
+
+  injectControls: function () {
+    var i, name,
+        data = this.data;
+
+    for (i = 0; i < data.movementControls.length; i++) {
+      name = data.movementControls[i] + COMPONENT_SUFFIX;
+      if (!this.el.components[name]) {
+        this.el.setAttribute(name, '');
+      }
+    }
+
+    for (i = 0; i < data.rotationControls.length; i++) {
+      name = data.rotationControls[i] + COMPONENT_SUFFIX;
+      if (!this.el.components[name]) {
+        this.el.setAttribute(name, '');
+      }
+    }
+  },
+
+  /*******************************************************************
+   * Tick
+   */
+
+  tick: function (t, dt) {
+    if (!dt) { return; }
+
+    // Update rotation.
+    if (this.data.rotationEnabled) this.updateRotation(dt);
+
+    // Update velocity. If FPS is too low, reset.
+    if (this.data.movementEnabled && dt / 1000 > MAX_DELTA) {
+      this.velocity.set(0, 0, 0);
+      this.el.setAttribute('velocity', this.velocity);
+    } else {
+      this.updateVelocity(dt);
+    }
+  },
+
+  /*******************************************************************
+   * Rotation
+   */
+
+  updateRotation: function (dt) {
+    var control, dRotation,
+        data = this.data;
+
+    for (var i = 0, l = data.rotationControls.length; i < l; i++) {
+      control = this.el.components[data.rotationControls[i] + COMPONENT_SUFFIX];
+      if (control && control.isRotationActive()) {
+        if (control.getRotationDelta) {
+          dRotation = control.getRotationDelta(dt);
+          dRotation.multiplyScalar(data.rotationSensitivity);
+          this.yaw.rotation.y -= dRotation.x;
+          this.pitch.rotation.x -= dRotation.y;
+          this.pitch.rotation.x = Math.max(-PI_2, Math.min(PI_2, this.pitch.rotation.x));
+          this.el.setAttribute('rotation', {
+            x: THREE.Math.radToDeg(this.pitch.rotation.x),
+            y: THREE.Math.radToDeg(this.yaw.rotation.y),
+            z: 0
+          });
+        } else if (control.getRotation) {
+          this.el.setAttribute('rotation', control.getRotation());
+        } else {
+          throw new Error('Incompatible rotation controls: %s', data.rotationControls[i]);
+        }
+        break;
+      }
+    }
+  },
+
+  /*******************************************************************
+   * Movement
+   */
+
+  updateVelocity: function (dt) {
+    var control, dVelocity,
+        velocity = this.velocity,
+        data = this.data;
+
+    if (data.movementEnabled) {
+      for (var i = 0, l = data.movementControls.length; i < l; i++) {
+        control = this.el.components[data.movementControls[i] + COMPONENT_SUFFIX];
+        if (control && control.isVelocityActive()) {
+          if (control.getVelocityDelta) {
+            dVelocity = control.getVelocityDelta(dt);
+          } else if (control.getVelocity) {
+            this.el.setAttribute('velocity', control.getVelocity());
+            return;
+          } else if (control.getPositionDelta) {
+            velocity.copy(control.getPositionDelta(dt).multiplyScalar(1000 / dt));
+            this.el.setAttribute('velocity', velocity);
+            return;
+          } else {
+            throw new Error('Incompatible movement controls: ', data.movementControls[i]);
+          }
+          break;
+        }
+      }
+    }
+
+    velocity.copy(this.el.getAttribute('velocity'));
+    velocity.x -= velocity.x * data.movementEasing * dt / 1000;
+    velocity.y -= velocity.y * data.movementEasingY * dt / 1000;
+    velocity.z -= velocity.z * data.movementEasing * dt / 1000;
+
+    if (dVelocity && data.movementEnabled) {
+      // Set acceleration
+      if (dVelocity.length() > 1) {
+        dVelocity.setLength(this.data.movementAcceleration * dt / 1000);
+      } else {
+        dVelocity.multiplyScalar(this.data.movementAcceleration * dt / 1000);
+      }
+
+      // Rotate to heading
+      var rotation = this.el.getAttribute('rotation');
+      if (rotation) {
+        this.heading.set(
+          data.fly ? THREE.Math.degToRad(rotation.x) : 0,
+          THREE.Math.degToRad(rotation.y),
+          0
+        );
+        dVelocity.applyEuler(this.heading);
+      }
+
+      velocity.add(dVelocity);
+
+      // TODO - Several issues here:
+      // (1) Interferes w/ gravity.
+      // (2) Interferes w/ jumping.
+      // (3) Likely to interfere w/ relative position to moving platform.
+      // if (velocity.length() > data.movementSpeed) {
+      //   velocity.setLength(data.movementSpeed);
+      // }
+    }
+
+    this.el.setAttribute('velocity', velocity);
+  }
+};
+
+},{}]},{},[1]);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/addon/aframe-extras.controls.min.js


+ 5 - 226
support/client/lib/vwf/model/aframe/addon/aframe-extras.loaders.js

@@ -1,6 +1,6 @@
 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
 require('./src/loaders').registerAll();
-},{"./src/loaders":9}],2:[function(require,module,exports){
+},{"./src/loaders":8}],2:[function(require,module,exports){
 /**
  * @author Kyle-Larson https://github.com/Kyle-Larson
  * @author Takahiro https://github.com/takahirox
@@ -5899,77 +5899,13 @@ var loadLoader = (function () {
 }());
 
 },{"../../lib/fetch-script":4}],8:[function(require,module,exports){
-var fetchScript = require('../../lib/fetch-script')();
-
-var LOADER_SRC = 'https://rawgit.com/mrdoob/three.js/r87/examples/js/loaders/GLTFLoader.js';
-// Monkeypatch while waiting for three.js r86.
-if (THREE.PropertyBinding.sanitizeNodeName === undefined) {
-
-  THREE.PropertyBinding.sanitizeNodeName = function (s) {
-    return s.replace( /\s/g, '_' ).replace( /[^\w-]/g, '' );
-  };
-
-}
-
-/**
- * Upcoming loader for glTF 2.0 models.
- * Asynchronously loads THREE.GLTF2Loader from rawgit.
- */
-module.exports = {
-  schema: {type: 'model'},
-
-  init: function () {
-    this.model = null;
-    this.loader = null;
-    this.loaderPromise = loadLoader().then(function () {
-      this.loader = new THREE.GLTFLoader();
-      this.loader.setCrossOrigin('Anonymous');
-    }.bind(this));
-  },
-
-  update: function () {
-    var self = this;
-    var el = this.el;
-    var src = this.data;
-
-    if (!src) { return; }
-
-    this.remove();
-
-    this.loaderPromise.then(function () {
-      this.loader.load(src, function gltfLoaded (gltfModel) {
-        self.model = gltfModel.scene;
-        self.model.animations = gltfModel.animations;
-        el.setObject3D('mesh', self.model);
-        el.emit('model-loaded', {format: 'gltf', model: self.model});
-      });
-    }.bind(this));
-  },
-
-  remove: function () {
-    if (!this.model) { return; }
-    this.el.removeObject3D('mesh');
-  }
-};
-
-var loadLoader = (function () {
-  var promise;
-  return function () {
-    promise = promise || fetchScript(LOADER_SRC);
-    return promise;
-  };
-}());
-
-},{"../../lib/fetch-script":4}],9:[function(require,module,exports){
 module.exports = {
   'animation-mixer': require('./animation-mixer'),
   'fbx-model': require('./fbx-model'),
-  'gltf-model-next': require('./gltf-model-next'),
   'gltf-model-legacy': require('./gltf-model-legacy'),
   'json-model': require('./json-model'),
   'object-model': require('./object-model'),
   'ply-model': require('./ply-model'),
-  'three-model': require('./three-model'),
 
   registerAll: function (AFRAME) {
     if (this._registered) return;
@@ -5994,11 +5930,6 @@ module.exports = {
       AFRAME.registerComponent('fbx-model', this['fbx-model']);
     }
 
-    // THREE.GLTF2Loader
-    if (!AFRAME.components['gltf-model-next']) {
-      AFRAME.registerComponent('gltf-model-next', this['gltf-model-next']);
-    }
-
     // THREE.GLTFLoader
     if (!AFRAME.components['gltf-model-legacy']) {
       AFRAME.registerComponent('gltf-model-legacy', this['gltf-model-legacy']);
@@ -6014,16 +5945,11 @@ module.exports = {
       AFRAME.registerComponent('object-model', this['object-model']);
     }
 
-    // (deprecated) THREE.JsonLoader and THREE.ObjectLoader
-    if (!AFRAME.components['three-model']) {
-      AFRAME.registerComponent('three-model', this['three-model']);
-    }
-
     this._registered = true;
   }
 };
 
-},{"./animation-mixer":5,"./fbx-model":6,"./gltf-model-legacy":7,"./gltf-model-next":8,"./json-model":10,"./object-model":11,"./ply-model":12,"./three-model":13}],10:[function(require,module,exports){
+},{"./animation-mixer":5,"./fbx-model":6,"./gltf-model-legacy":7,"./json-model":9,"./object-model":10,"./ply-model":11}],9:[function(require,module,exports){
 /**
  * json-model
  *
@@ -6083,7 +6009,7 @@ module.exports = {
   }
 };
 
-},{}],11:[function(require,module,exports){
+},{}],10:[function(require,module,exports){
 /**
  * object-model
  *
@@ -6138,7 +6064,7 @@ module.exports = {
   }
 };
 
-},{}],12:[function(require,module,exports){
+},{}],11:[function(require,module,exports){
 /**
  * ply-model
  *
@@ -6219,151 +6145,4 @@ function createModel (geometry) {
   }));
 }
 
-},{"../../lib/PLYLoader":3}],13:[function(require,module,exports){
-var DEFAULT_ANIMATION = '__auto__';
-
-/**
- * three-model
- *
- * Loader for THREE.js JSON format. Somewhat confusingly, there are two
- * different THREE.js formats, both having the .json extension. This loader
- * supports both, but requires you to specify the mode as "object" or "json".
- *
- * Typically, you will use "json" for a single mesh, and "object" for a scene
- * or multiple meshes. Check the console for errors, if in doubt.
- *
- * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
- */
-module.exports = {
-  deprecated: true,
-
-  schema: {
-    src:               { type: 'asset' },
-    loader:            { default: 'object', oneOf: ['object', 'json'] },
-    enableAnimation:   { default: true },
-    animation:         { default: DEFAULT_ANIMATION },
-    animationDuration: { default: 0 },
-    crossorigin:       { default: '' }
-  },
-
-  init: function () {
-    this.model = null;
-    this.mixer = null;
-    console.warn('[three-model] Component is deprecated. Use json-model or object-model instead.');
-  },
-
-  update: function (previousData) {
-    previousData = previousData || {};
-
-    var loader,
-        data = this.data;
-
-    if (!data.src) {
-      this.remove();
-      return;
-    }
-
-    // First load.
-    if (!Object.keys(previousData).length) {
-      this.remove();
-      if (data.loader === 'object') {
-        loader = new THREE.ObjectLoader();
-        if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
-        loader.load(data.src, function(loaded) {
-          loaded.traverse( function(object) {
-            if (object instanceof THREE.SkinnedMesh)
-              loaded = object;
-          });
-          if(loaded.material)
-            loaded.material.skinning = !!((loaded.geometry && loaded.geometry.bones) || []).length;
-          this.load(loaded);
-        }.bind(this));
-      } else if (data.loader === 'json') {
-        loader = new THREE.JSONLoader();
-        if (data.crossorigin) loader.crossOrigin = data.crossorigin;
-        loader.load(data.src, function (geometry, materials) {
-
-          // Attempt to automatically detect common material options.
-          materials.forEach(function (mat) {
-            mat.vertexColors = (geometry.faces[0] || {}).color ? THREE.FaceColors : THREE.NoColors;
-            mat.skinning = !!(geometry.bones || []).length;
-            mat.morphTargets = !!(geometry.morphTargets || []).length;
-            mat.morphNormals = !!(geometry.morphNormals || []).length;
-          });
-
-          var mesh = (geometry.bones || []).length
-            ? new THREE.SkinnedMesh(geometry, new THREE.MultiMaterial(materials))
-            : new THREE.Mesh(geometry, new THREE.MultiMaterial(materials));
-
-          this.load(mesh);
-        }.bind(this));
-      } else {
-        throw new Error('[three-model] Invalid mode "%s".', data.mode);
-      }
-      return;
-    }
-
-    var activeAction = this.model && this.model.activeAction;
-
-    if (data.animation !== previousData.animation) {
-      if (activeAction) activeAction.stop();
-      this.playAnimation();
-      return;
-    }
-
-    if (activeAction && data.enableAnimation !== activeAction.isRunning()) {
-      data.enableAnimation ? this.playAnimation() : activeAction.stop();
-    }
-
-    if (activeAction && data.animationDuration) {
-        activeAction.setDuration(data.animationDuration);
-    }
-  },
-
-  load: function (model) {
-    this.model = model;
-    this.mixer = new THREE.AnimationMixer(this.model);
-    this.el.setObject3D('mesh', model);
-    this.el.emit('model-loaded', {format: 'three', model: model});
-
-    if (this.data.enableAnimation) this.playAnimation();
-  },
-
-  playAnimation: function () {
-    var clip,
-        data = this.data,
-        animations = this.model.animations || this.model.geometry.animations || [];
-
-    if (!data.enableAnimation || !data.animation || !animations.length) {
-      return;
-    }
-
-    clip = data.animation === DEFAULT_ANIMATION
-      ? animations[0]
-      : THREE.AnimationClip.findByName(animations, data.animation);
-
-    if (!clip) {
-      console.error('[three-model] Animation "%s" not found.', data.animation);
-      return;
-    }
-
-    this.model.activeAction = this.mixer.clipAction(clip, this.model);
-    if (data.animationDuration) {
-      this.model.activeAction.setDuration(data.animationDuration);
-    }
-    this.model.activeAction.play();
-  },
-
-  remove: function () {
-    if (this.mixer) this.mixer.stopAllAction();
-    if (this.model) this.el.removeObject3D('mesh');
-  },
-
-  tick: function (t, dt) {
-    if (this.mixer && !isNaN(dt)) {
-      this.mixer.update(dt / 1000);
-    }
-  }
-};
-
-},{}]},{},[1]);
+},{"../../lib/PLYLoader":3}]},{},[1]);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/addon/aframe-extras.loaders.min.js


+ 342 - 0
support/client/lib/vwf/model/aframe/addon/aframe-interpolation 2.js

@@ -0,0 +1,342 @@
+/* 
+The MIT License (MIT)
+Copyright (c) 2017 Nikolai Suslov
+Updated for using in LiveCoding.space and A-Frame 0.6.x
+
+Interpolate component for A-Frame VR. https://github.com/scenevr/aframe-interpolate-component.git
+
+The MIT License (MIT)
+
+Copyright (c) 2015 Kevin Ngo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+/* globals AFRAME, performance, THREE */
+
+if (typeof AFRAME === 'undefined') {
+  throw new Error('Component attempted to register before AFRAME was available.');
+}
+
+
+class Interpolator {
+  constructor(comp) {
+    this.component = comp;
+    this.time = this.getMillis();
+  }
+
+  active() {
+    return this.previous && this.next && (this.getTime() < 1);
+  }
+
+  getMillis() {
+    return new Date().getTime();
+  }
+
+  getTime() {
+    return (this.getMillis() - this.time) / this.component.timestep;
+  }
+
+  vecCmp(a, b, delta) {
+
+    let distance = a.distanceTo(b);
+    if (distance > delta) {
+      return false;
+    }
+    return true;
+  }
+
+}
+
+class RotationInterpolator extends Interpolator {
+
+  constructor(comp) {
+    super(comp);
+
+    this.lastRotation = this.component.el.getAttribute('rotation');
+    this.previous = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
+      this.radians(this.lastRotation.x),
+      this.radians(this.lastRotation.y),
+      this.radians(this.lastRotation.z), 'YXZ'
+    ));
+    this.next = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
+      this.radians(this.lastRotation.x),
+      this.radians(this.lastRotation.y),
+      this.radians(this.lastRotation.z), 'YXZ'
+    ));
+
+
+  }
+
+  radians(degrees) {
+    // return degrees * Math.PI / 180.0;
+    return THREE.Math.degToRad(degrees)
+  }
+
+  makeInterpolation() {
+    let q = new THREE.Quaternion();
+    let e = new THREE.Euler();
+
+    THREE.Quaternion.slerp(this.previous, this.next, q, this.getTime());
+    return e.setFromQuaternion(q);
+  }
+
+  testForLerp() {
+
+    if (this.component.deltaRot == 0) {
+      return true
+    }
+
+    let prev = (new THREE.Euler()).setFromQuaternion(this.previous).toVector3();
+    let next = (new THREE.Euler()).setFromQuaternion(this.next).toVector3();
+
+    if (prev && next && this.vecCmp(prev, next, this.component.deltaRot)) {
+      return true
+    }
+    return false
+  }
+
+
+  inTick(currentRotation) {
+
+    if (this.getTime() < 0.5) {
+      // fixme - ignore multiple calls
+      return;
+    }
+
+
+    if (!this.previous) {
+      // this.previous = new THREE.Quaternion();
+      // this.next = new THREE.Quaternion();
+      this.previous = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
+        this.radians(this.lastRotation.x),
+        this.radians(this.lastRotation.y),
+        this.radians(this.lastRotation.z), 'YXZ'
+      ));
+
+      this.next = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
+        this.radians(currentRotation.x),
+        this.radians(currentRotation.y),
+        this.radians(currentRotation.z), 'YXZ'
+      ));
+    }
+
+    this.time = this.getMillis();
+    this.previous.copy(this.next);
+    this.next.setFromEuler(new THREE.Euler(
+      this.radians(currentRotation.x),
+      this.radians(currentRotation.y),
+      this.radians(currentRotation.z), 'YXZ'
+    ));
+
+    this.lastRotation = currentRotation;
+
+  }
+
+}
+
+
+class PositionInterpolator extends Interpolator {
+
+  constructor(comp) {
+    super(comp);
+    //this.lastPosition = new THREE.Vector3().copy(this.component.el.object3D.position);
+   this.lastPosition = new THREE.Vector3().copy(this.component.el.getAttribute('position'));
+    //this.lastPosition = this.component.el.getAttribute('position');
+    this.previous = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
+    this.next = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
+
+  }
+
+
+  testForLerp() {
+
+    if (this.component.deltaPos == 0) {
+      return true
+    }
+
+    if (this.previous && this.next && this.vecCmp(this.previous, this.next, this.component.deltaPos)) {
+      return true
+    }
+    return false
+  }
+
+  makeInterpolation() {
+    return this.previous.lerp(this.next, this.getTime());
+  }
+
+  inTick(currentPosition) {
+    //console.log(this.getTime());
+    if (this.getTime()< 0.5) {
+      // fixme - ignore multiple calls
+      return;
+    }
+
+    if (!this.previous) {
+      this.previous = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
+      this.next = (new THREE.Vector3()).fromArray(Object.values(currentPosition));
+    }
+
+    
+    this.previous.copy(this.next);
+    this.next.copy(currentPosition);
+
+    //this.lastPosition = currentPosition;
+    this.lastPosition.copy(currentPosition);
+    this.time = this.getMillis();
+
+  }
+
+}
+
+/**
+ * Interpolate component for A-Frame.
+ */
+AFRAME.registerComponent('interpolation', {
+  schema: {
+    duration: { default: 50 },
+    enabled: { default: true },
+    deltaPos: { default: 0 },
+    deltaRot: { default: 0 }
+  },
+
+
+
+  /**
+   * Called once when component is attached. Generally for initial setup.
+   */
+  init: function () {
+
+    // Set up the tick throttling.
+    //this.tick = AFRAME.utils.throttleTick(this.throttledTick, 0, this);
+
+    //var el = this.el;
+    //this.lastPosition = el.getAttribute('position');
+
+
+    //this.timestep = 50;
+    //this.time = this.getMillis();
+    //  this.previous = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
+    //  this.next = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
+
+  },
+
+  // throttledTick: function (time, deltaTime) {
+
+  // },
+
+  /**
+   * Called when component is attached and when component data changes.
+   * Generally modifies the entity based on the data.
+   */
+  update: function (oldData) {
+
+    if (!this.interpolation) {
+      this.timestep = parseInt(this.data.duration, 10);
+      this.deltaPos = parseFloat(this.data.deltaPos);
+      this.deltaRot = THREE.Math.degToRad(parseFloat(this.data.deltaRot));
+
+      this.enabled = JSON.parse(this.data.enabled);
+
+      if (this.enabled) {
+        this.posInterpolator = new PositionInterpolator(this);
+        this.rotInterpolator = new RotationInterpolator(this);
+      }
+    }
+
+    // if (!this.interpolation) {
+    //   var timestep = parseInt(this.data.duration, 10);
+
+    //   this.positionInterpolator = new PositionInterpolator(timestep, this);
+    //   this.rotationInterpolator = new RotationInterpolator(timestep, this);
+    // }
+  },
+
+  /**
+   * Called when a component is removed (e.g., via removeAttribute).
+   * Generally undoes all modifications to the entity.
+   */
+  remove: function () { },
+
+  /**
+   * Called on each scene tick.
+   */
+  tick: function () {
+
+    
+   //let currentPosition = this.el.getAttribute('position');
+    //let currentPosition = new THREE.Vector3().copy(this.el.getAttribute('position'));
+    //let currentPosition = new THREE.Vector3().copy(this.el.object3D.position);
+    //console.log(dt);
+    //let currentRotation = this.el.getAttribute('rotation');
+
+    if (this.enabled) {
+
+
+      var el = this.el;
+      var rotationTmp = this.rotationTmp = this.rotationTmp || {x: 0, y: 0, z: 0};
+      var rotation = el.getAttribute('rotation');
+      rotationTmp.x = rotation.x + 0.1;
+      rotationTmp.y = rotation.y + 0.1;
+      rotationTmp.z = rotation.z + 0.1;
+      el.setAttribute('rotation', rotationTmp);
+
+
+      // if (this.posInterpolator.lastPosition.equals(currentPosition) === false) {
+      //   //if (this.posInterpolator.lastPosition != currentPosition) {
+      //   this.posInterpolator.inTick(currentPosition)
+      // }
+      // if (this.posInterpolator.active() && this.posInterpolator.testForLerp()) {
+      //   let newPos = this.posInterpolator.makeInterpolation();
+      //  this.el.setAttribute('position',this.posInterpolator.makeInterpolation())
+        
+      //   //this.el.object3D.position.copy(this.posInterpolator.makeInterpolation());
+      // }
+
+
+      // if (this.rotInterpolator.lastRotation != currentRotation) {
+      //   this.rotInterpolator.inTick(currentRotation)
+      // }
+      // if (this.rotInterpolator.active() && this.rotInterpolator.testForLerp()) {
+      //   this.el.object3D.rotation.copy(this.rotInterpolator.makeInterpolation());
+      // }
+
+    }
+
+    // if (this.positionInterpolator && this.positionInterpolator.active()) {
+    //   this.el.object3D.position.copy(this.positionInterpolator.get());
+    // }
+
+    // if (this.rotationInterpolator && this.rotationInterpolator.active()) {
+    //   this.el.object3D.rotation.copy(this.rotationInterpolator.get());
+    // }
+  },
+
+  /**
+   * Called when entity pauses.
+   * Use to stop or remove any dynamic or background behavior such as events.
+   */
+  pause: function () { },
+
+  /**
+   * Called when entity resumes.
+   * Use to continue or add any dynamic or background behavior such as events.
+   */
+  play: function () { },
+});

+ 96 - 235
support/client/lib/vwf/model/aframe/addon/aframe-interpolation.js

@@ -1,13 +1,7 @@
 /* 
 The MIT License (MIT)
 Copyright (c) 2017 Nikolai Suslov
-Updated for using in LiveCoding.space and A-Frame 0.6.x
-
-Interpolate component for A-Frame VR. https://github.com/scenevr/aframe-interpolate-component.git
-
-The MIT License (MIT)
-
-Copyright (c) 2015 Kevin Ngo
+For using in LiveCoding.space and A-Frame 0.8.x
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -34,287 +28,154 @@ if (typeof AFRAME === 'undefined') {
   throw new Error('Component attempted to register before AFRAME was available.');
 }
 
+AFRAME.registerComponent('interpolation', {
+  schema: {
+    enabled: { default: true },
+    deltaPos: { default: 0.001 },
+    deltaRot: { default: 0.1 }
+  },
 
-class Interpolator {
-  constructor(comp) {
-    this.component = comp;
-    this.time = this.getMillis();
-  }
+  init: function () {
 
-  active() {
-    return this.previous && this.next && (this.getTime() < 1);
-  }
+    this.driver = vwf.views["vwf/view/aframeComponent"];
 
-  getMillis() {
-    return new Date().getTime();
-  }
+  },
 
-  getTime() {
-    return (this.getMillis() - this.time) / this.component.timestep;
-  }
+  update: function (oldData) {
 
-  vecCmp(a, b, delta) {
+    if (!this.interpolation) {
+      this.deltaPos = parseFloat(this.data.deltaPos);
+      this.deltaRot = THREE.Math.degToRad(parseFloat(this.data.deltaRot));
 
-    let distance = a.distanceTo(b);
-    if (distance > delta) {
-      return false;
+      this.enabled = JSON.parse(this.data.enabled);
+      if (this.enabled) {
+      }
     }
-    return true;
-  }
-
-}
-
-class RotationInterpolator extends Interpolator {
-
-  constructor(comp) {
-    super(comp);
-
-    this.lastRotation = this.component.el.getAttribute('rotation');
-    this.previous = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
-      this.radians(this.lastRotation.x),
-      this.radians(this.lastRotation.y),
-      this.radians(this.lastRotation.z), 'YXZ'
-    ));
-    this.next = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
-      this.radians(this.lastRotation.x),
-      this.radians(this.lastRotation.y),
-      this.radians(this.lastRotation.z), 'YXZ'
-    ));
-
-
-  }
-
-  radians(degrees) {
-    // return degrees * Math.PI / 180.0;
-    return THREE.Math.degToRad(degrees)
-  }
-
-  makeInterpolation() {
-    let q = new THREE.Quaternion();
-    let e = new THREE.Euler();
-
-    THREE.Quaternion.slerp(this.previous, this.next, q, this.getTime());
-    return e.setFromQuaternion(q);
-  }
-
-  testForLerp() {
+  },
 
-    if (this.component.deltaRot == 0) {
-      return true
-    }
+  /**
+   * Called when a component is removed (e.g., via removeAttribute).
+   * Generally undoes all modifications to the entity.
+   */
+  remove: function () { },
 
-    let prev = (new THREE.Euler()).setFromQuaternion(this.previous).toVector3();
-    let next = (new THREE.Euler()).setFromQuaternion(this.next).toVector3();
+  /**
+   * Called on each scene tick.
+   */
+  tick: function (t, dt) {
 
-    if (prev && next && this.vecCmp(prev, next, this.component.deltaRot)) {
-      return true
+    if (!this.node) {
+      let interNode = Object.entries(this.driver.state.nodes).find(el => el[1].parentID == this.el.id);
+      this.node = this.driver.nodes[interNode[0]];
+      this.nodeState = interNode[1];
     }
-    return false
-  }
 
+    if (this.enabled && this.node && this.node.interpolate) {
 
-  inTick(currentRotation) {
+      this.setInterpolatedTransforms(dt);
+      //this.restoreTransforms();
 
-    if (this.getTime() < 0.5) {
-      // fixme - ignore multiple calls
-      return;
     }
 
+  },
 
-    if (!this.previous) {
-      // this.previous = new THREE.Quaternion();
-      // this.next = new THREE.Quaternion();
-      this.previous = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
-        this.radians(this.lastRotation.x),
-        this.radians(this.lastRotation.y),
-        this.radians(this.lastRotation.z), 'YXZ'
-      ));
-
-      this.next = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
-        this.radians(currentRotation.x),
-        this.radians(currentRotation.y),
-        this.radians(currentRotation.z), 'YXZ'
-      ));
-    }
-
-    this.time = this.getMillis();
-    this.previous.copy(this.next);
-    this.next.setFromEuler(new THREE.Euler(
-      this.radians(currentRotation.x),
-      this.radians(currentRotation.y),
-      this.radians(currentRotation.z), 'YXZ'
-    ));
-
-    this.lastRotation = currentRotation;
-
-  }
-
-}
-
-
-class PositionInterpolator extends Interpolator {
-
-  constructor(comp) {
-    super(comp);
-    this.lastPosition = this.component.el.getAttribute('position');
-    this.previous = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
-    this.next = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
-
-  }
-
-
-  testForLerp() {
-
-    if (this.component.deltaPos == 0) {
-      return true
-    }
+  vecCmp: function (a, b, delta) {
 
-    if (this.previous && this.next && this.vecCmp(this.previous, this.next, this.component.deltaPos)) {
-      return true
+    let distance = a.distanceTo(b);
+    if (distance > delta) {
+      return false;
     }
-    return false
-  }
 
-  makeInterpolation() {
-    return this.previous.lerp(this.next, this.getTime());
-  }
+    return true;
+  },
 
-  inTick(currentPosition) {
+  restoreTransforms: function () {
 
-    if (this.getTime() < 0.5) {
-      // fixme - ignore multiple calls
-      return;
-    }
+    let r = new THREE.Euler();
+    let rot = r.copy(this.node.interpolate.rotation.selfTick);
 
-    if (!this.previous) {
-      this.previous = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
-      this.next = (new THREE.Vector3()).fromArray(Object.values(currentPosition));
+    if (rot && this.node.needTransformRestore) {
+      this.el.object3D.rotation.set(rot.x, rot.y, rot.z)
+      this.node.needTransformRestore = false;
     }
 
-    this.time = this.getMillis();
-    this.previous.copy(this.next);
-    this.next.copy(currentPosition);
+  },
 
-    this.lastPosition = currentPosition;
+  setInterpolatedTransforms: function (deltaTime) {
 
-  }
+    var step = (this.node.tickTime) / (this.driver.realTickDif);
+    step = Math.min(step, 1);
+    deltaTime = Math.min(deltaTime, this.driver.realTickDif)
+    this.node.tickTime += deltaTime || 0;
 
-}
+    this.interpolatePosition(step);
+    this.interpolateRotation(step);
 
-/**
- * Interpolate component for A-Frame.
- */
-AFRAME.registerComponent('interpolation', {
-  schema: {
-    duration: { default: 50 },
-    enabled: { default: true },
-    deltaPos: { default: 0 },
-    deltaRot: { default: 0 }
+  },
+  radians: function (degrees) {
+    // return degrees * Math.PI / 180.0;
+    return THREE.Math.degToRad(degrees)
   },
 
+  interpolateRotation: function (step) {
 
+    let last = this.node.interpolate.rotation.lastTick;
+    let now = this.node.interpolate.rotation.selfTick;
 
-  /**
-   * Called once when component is attached. Generally for initial setup.
-   */
-  init: function () {
-
-    // Set up the tick throttling.
-    //this.tick = AFRAME.utils.throttleTick(this.throttledTick, 0, this);
-
-    //var el = this.el;
-    //this.lastPosition = el.getAttribute('position');
-
+    if (last && now) {
 
-    //this.timestep = 50;
-    //this.time = this.getMillis();
-    //  this.previous = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
-    //  this.next = (new THREE.Vector3()).fromArray(Object.values(this.lastPosition));
+      let comp = this.vecCmp(last.toVector3(), now.toVector3(), this.deltaRot);
 
-  },
+      if (!comp) {
 
-  // throttledTick: function (time, deltaTime) {
+        // console.log('Last:', last, ' Now: ', now);
 
-  // },
+        let lastV = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
+          (last.x),
+          (last.y),
+          (last.z), 'YXZ'
+        ));
 
-  /**
-   * Called when component is attached and when component data changes.
-   * Generally modifies the entity based on the data.
-   */
-  update: function (oldData) {
+        let nowV = (new THREE.Quaternion()).setFromEuler(new THREE.Euler(
+          (now.x),
+          (now.y),
+          (now.z), 'YXZ'
+        ));
 
-    if (!this.interpolation) {
-      this.timestep = parseInt(this.data.duration, 10);
-      this.deltaPos = parseFloat(this.data.deltaPos);
-      this.deltaRot = THREE.Math.degToRad(parseFloat(this.data.deltaRot));
+        let q = new THREE.Quaternion();
+        let e = new THREE.Euler();
 
-      this.enabled = JSON.parse(this.data.enabled);
+        THREE.Quaternion.slerp(lastV, nowV, q, step || 0);
+        let interp = e.setFromQuaternion(q, 'YXZ');
 
-      if (this.enabled) {
-        this.posInterpolator = new PositionInterpolator(this);
-        this.rotInterpolator = new RotationInterpolator(this);
+        this.el.object3D.rotation.set(interp.x, interp.y, interp.z);
+        this.node.needTransformRestore = true;
       }
     }
-
-    // if (!this.interpolation) {
-    //   var timestep = parseInt(this.data.duration, 10);
-
-    //   this.positionInterpolator = new PositionInterpolator(timestep, this);
-    //   this.rotationInterpolator = new RotationInterpolator(timestep, this);
-    // }
   },
 
-  /**
-   * Called when a component is removed (e.g., via removeAttribute).
-   * Generally undoes all modifications to the entity.
-   */
-  remove: function () { },
+  interpolatePosition: function (step) {
 
-  /**
-   * Called on each scene tick.
-   */
-  tick: function (t, dt) {
+    var last = this.node.interpolate.position.lastTick; //this.node.lastTickTransform;
+    var now = this.node.interpolate.position.selfTick; //Transform;
 
-    let currentPosition = this.el.getAttribute('position');
-    let currentRotation = this.el.getAttribute('rotation');
+    if (last && now) {
 
-    if (this.enabled) {
+      let comp = this.vecCmp(last, now, this.deltaPos);
 
-      if (this.posInterpolator.lastPosition != currentPosition) {
-        this.posInterpolator.inTick(currentPosition)
-      }
-      if (this.posInterpolator.active() && this.posInterpolator.testForLerp()) {
-        this.el.object3D.position.copy(this.posInterpolator.makeInterpolation());
-      }
+      if (!comp) {
 
+        var lastV = (new THREE.Vector3()).copy(last);
+        var nowV = (new THREE.Vector3()).copy(now);
 
-      if (this.rotInterpolator.lastRotation != currentRotation) {
-        this.rotInterpolator.inTick(currentRotation)
-      }
-      if (this.rotInterpolator.active() && this.rotInterpolator.testForLerp()) {
-        this.el.object3D.rotation.copy(this.rotInterpolator.makeInterpolation());
-      }
+        var interp = lastV.lerp(nowV, step || 0);
+        //this.el.setAttribute('position',interp);
 
+        this.el.object3D.position.set(interp.x, interp.y, interp.z);
+        this.node.needTransformRestore = true;
+      }
     }
-
-    // if (this.positionInterpolator && this.positionInterpolator.active()) {
-    //   this.el.object3D.position.copy(this.positionInterpolator.get());
-    // }
-
-    // if (this.rotationInterpolator && this.rotationInterpolator.active()) {
-    //   this.el.object3D.rotation.copy(this.rotationInterpolator.get());
-    // }
   },
-
-  /**
-   * Called when entity pauses.
-   * Use to stop or remove any dynamic or background behavior such as events.
-   */
   pause: function () { },
-
-  /**
-   * Called when entity resumes.
-   * Use to continue or add any dynamic or background behavior such as events.
-   */
-  play: function () { },
+  play: function () { }
 });

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 616 - 729
support/client/lib/vwf/model/aframe/aframe-master.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 2
support/client/lib/vwf/model/aframe/aframe-master.js.map


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/aframe-master.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/aframe-master.min.js.map


+ 11 - 4
support/client/lib/vwf/model/aframe/extras/aframe-extras.controls.js

@@ -16449,7 +16449,7 @@ function getGeometry (object) {
     var position = new THREE.Vector3(),
         quaternion = new THREE.Quaternion(),
         scale = new THREE.Vector3();
-    if (meshes[0].geometry instanceof THREE.BufferGeometry) {
+    if (meshes[0].geometry.isBufferGeometry) {
       if (meshes[0].geometry.attributes.position) {
         tmp.fromBufferGeometry(meshes[0].geometry);
       }
@@ -16465,7 +16465,7 @@ function getGeometry (object) {
   // Recursively merge geometry, preserving local transforms.
   while ((mesh = meshes.pop())) {
     mesh.updateMatrixWorld();
-    if (mesh.geometry instanceof THREE.BufferGeometry) {
+    if (mesh.geometry.isBufferGeometry) {
       tmp.fromBufferGeometry(mesh.geometry);
       combined.merge(tmp, mesh.matrixWorld);
     } else {
@@ -16989,14 +16989,21 @@ module.exports = {
     if (this.checkpoint === checkpoint) return;
 
     if (this.checkpoint) {
-      el.emit('navigation-end', {checkpoint: checkpoint});
+      el.emit('navigation-end', {checkpoint: this.checkpoint});
     }
 
     this.checkpoint = checkpoint;
+    this.sync();
+
+    // Ignore new checkpoint if we're already there.
+    if (this.position.distanceTo(this.targetPosition) < EPS) {
+      this.checkpoint = null;
+      return;
+    }
+
     el.emit('navigation-start', {checkpoint: checkpoint});
 
     if (this.data.mode === 'teleport') {
-      this.sync();
       this.el.setAttribute('position', this.targetPosition);
       this.checkpoint = null;
       el.emit('navigation-end', {checkpoint: checkpoint});

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/aframe-extras.controls.min.js


+ 3578 - 3828
support/client/lib/vwf/model/aframe/extras/aframe-extras.js

@@ -19,7 +19,7 @@ module.exports = {
   }
 };
 
-},{"./src/controls":84,"./src/loaders":93,"./src/misc":101,"./src/pathfinding":107,"./src/primitives":115,"aframe-physics-system":11}],3:[function(require,module,exports){
+},{"./src/controls":89,"./src/loaders":97,"./src/misc":104,"./src/pathfinding":110,"./src/primitives":118,"aframe-physics-system":11}],3:[function(require,module,exports){
 /**
  * @author Kyle-Larson https://github.com/Kyle-Larson
  * @author Takahiro https://github.com/takahirox
@@ -6986,7 +6986,7 @@ module.exports = {
   }())
 };
 
-},{"../../../lib/CANNON-shape2mesh":12,"cannon":23,"three-to-cannon":79}],14:[function(require,module,exports){
+},{"../../../lib/CANNON-shape2mesh":12,"cannon":23,"three-to-cannon":84}],14:[function(require,module,exports){
 var Body = require('./body');
 
 /**
@@ -21920,4733 +21920,4483 @@ World.prototype.clearForces = function(){
 };
 
 },{"../collision/AABB":24,"../collision/ArrayCollisionMatrix":25,"../collision/NaiveBroadphase":28,"../collision/OverlapKeeper":30,"../collision/Ray":31,"../collision/RaycastResult":32,"../equations/ContactEquation":41,"../equations/FrictionEquation":43,"../material/ContactMaterial":46,"../material/Material":47,"../math/Quaternion":50,"../math/Vec3":52,"../objects/Body":53,"../shapes/Shape":65,"../solver/GSSolver":68,"../utils/EventTarget":71,"../utils/TupleDictionary":74,"./Narrowphase":77}],79:[function(require,module,exports){
-var CANNON = require('cannon'),
-    quickhull = require('./lib/THREE.quickhull');
+const BinaryHeap = require('./BinaryHeap');
+const utils = require('./utils.js');
 
-var PI_2 = Math.PI / 2;
+class AStar {
+  static init (graph) {
+    for (let x = 0; x < graph.length; x++) {
+      //for(var x in graph) {
+      const node = graph[x];
+      node.f = 0;
+      node.g = 0;
+      node.h = 0;
+      node.cost = 1.0;
+      node.visited = false;
+      node.closed = false;
+      node.parent = null;
+    }
+  }
 
-var Type = {
-  BOX: 'Box',
-  CYLINDER: 'Cylinder',
-  SPHERE: 'Sphere',
-  HULL: 'ConvexPolyhedron',
-  MESH: 'Trimesh'
-};
+  static cleanUp (graph) {
+    for (let x = 0; x < graph.length; x++) {
+      const node = graph[x];
+      delete node.f;
+      delete node.g;
+      delete node.h;
+      delete node.cost;
+      delete node.visited;
+      delete node.closed;
+      delete node.parent;
+    }
+  }
 
-/**
- * Given a THREE.Object3D instance, creates a corresponding CANNON shape.
- * @param  {THREE.Object3D} object
- * @return {CANNON.Shape}
- */
-module.exports = CANNON.mesh2shape = function (object, options) {
-  options = options || {};
+  static heap () {
+    return new BinaryHeap(function (node) {
+      return node.f;
+    });
+  }
 
-  var geometry;
+  static search (graph, start, end) {
+    this.init(graph);
+    //heuristic = heuristic || astar.manhattan;
 
-  if (options.type === Type.BOX) {
-    return createBoundingBoxShape(object);
-  } else if (options.type === Type.CYLINDER) {
-    return createBoundingCylinderShape(object, options);
-  } else if (options.type === Type.SPHERE) {
-    return createBoundingSphereShape(object, options);
-  } else if (options.type === Type.HULL) {
-    return createConvexPolyhedron(object);
-  } else if (options.type === Type.MESH) {
-    geometry = getGeometry(object);
-    return geometry ? createTrimeshShape(geometry) : null;
-  } else if (options.type) {
-    throw new Error('[CANNON.mesh2shape] Invalid type "%s".', options.type);
-  }
 
-  geometry = getGeometry(object);
-  if (!geometry) return null;
+    const openHeap = this.heap();
 
-  var type = geometry.metadata
-    ? geometry.metadata.type
-    : geometry.type;
+    openHeap.push(start);
 
-  switch (type) {
-    case 'BoxGeometry':
-    case 'BoxBufferGeometry':
-      return createBoxShape(geometry);
-    case 'CylinderGeometry':
-    case 'CylinderBufferGeometry':
-      return createCylinderShape(geometry);
-    case 'PlaneGeometry':
-    case 'PlaneBufferGeometry':
-      return createPlaneShape(geometry);
-    case 'SphereGeometry':
-    case 'SphereBufferGeometry':
-      return createSphereShape(geometry);
-    case 'TubeGeometry':
-    case 'Geometry':
-    case 'BufferGeometry':
-      return createBoundingBoxShape(object);
-    default:
-      console.warn('Unrecognized geometry: "%s". Using bounding box as shape.', geometry.type);
-      return createBoxShape(geometry);
-  }
-};
+    while (openHeap.size() > 0) {
 
-CANNON.mesh2shape.Type = Type;
+      // Grab the lowest f(x) to process next.  Heap keeps this sorted for us.
+      const currentNode = openHeap.pop();
 
-/******************************************************************************
- * Shape construction
- */
+      // End case -- result has been found, return the traced path.
+      if (currentNode === end) {
+        let curr = currentNode;
+        const ret = [];
+        while (curr.parent) {
+          ret.push(curr);
+          curr = curr.parent;
+        }
+        this.cleanUp(ret);
+        return ret.reverse();
+      }
 
- /**
-  * @param  {THREE.Geometry} geometry
-  * @return {CANNON.Shape}
-  */
- function createBoxShape (geometry) {
-   var vertices = getVertices(geometry);
+      // Normal case -- move currentNode from open to closed, process each of its neighbours.
+      currentNode.closed = true;
 
-   if (!vertices.length) return null;
+      // Find all neighbours for the current node. Optionally find diagonal neighbours as well (false by default).
+      const neighbours = this.neighbours(graph, currentNode);
 
-   geometry.computeBoundingBox();
-   var box = geometry.boundingBox;
-   return new CANNON.Box(new CANNON.Vec3(
-     (box.max.x - box.min.x) / 2,
-     (box.max.y - box.min.y) / 2,
-     (box.max.z - box.min.z) / 2
-   ));
- }
+      for (let i = 0, il = neighbours.length; i < il; i++) {
+        const neighbour = neighbours[i];
 
-/**
- * Bounding box needs to be computed with the entire mesh, not just geometry.
- * @param  {THREE.Object3D} mesh
- * @return {CANNON.Shape}
- */
-function createBoundingBoxShape (object) {
-  var shape, localPosition, worldPosition,
-      box = new THREE.Box3();
+        if (neighbour.closed) {
+          // Not a valid node to process, skip to next neighbour.
+          continue;
+        }
 
-  box.setFromObject(object);
+        // The g score is the shortest distance from start to current node.
+        // We need to check if the path we have arrived at this neighbour is the shortest one we have seen yet.
+        const gScore = currentNode.g + neighbour.cost;
+        const beenVisited = neighbour.visited;
 
-  if (!isFinite(box.min.lengthSq())) return null;
+        if (!beenVisited || gScore < neighbour.g) {
 
-  shape = new CANNON.Box(new CANNON.Vec3(
-    (box.max.x - box.min.x) / 2,
-    (box.max.y - box.min.y) / 2,
-    (box.max.z - box.min.z) / 2
-  ));
+          // Found an optimal (so far) path to this node.  Take score for node to see how good it is.
+          neighbour.visited = true;
+          neighbour.parent = currentNode;
+          if (!neighbour.centroid || !end.centroid) throw new Error('Unexpected state');
+          neighbour.h = neighbour.h || this.heuristic(neighbour.centroid, end.centroid);
+          neighbour.g = gScore;
+          neighbour.f = neighbour.g + neighbour.h;
 
-  object.updateMatrixWorld();
-  worldPosition = new THREE.Vector3();
-  worldPosition.setFromMatrixPosition(object.matrixWorld);
-  localPosition = box.translate(worldPosition.negate()).getCenter();
-  if (localPosition.lengthSq()) {
-    shape.offset = localPosition;
+          if (!beenVisited) {
+            // Pushing to heap will put it in proper place based on the 'f' value.
+            openHeap.push(neighbour);
+          } else {
+            // Already seen the node, but since it has been rescored we need to reorder it in the heap
+            openHeap.rescoreElement(neighbour);
+          }
+        }
+      }
+    }
+
+    // No result was found - empty array signifies failure to find path.
+    return [];
   }
 
-  return shape;
+  static heuristic (pos1, pos2) {
+    return utils.distanceToSquared(pos1, pos2);
+  }
+
+  static neighbours (graph, node) {
+    const ret = [];
+
+    for (let e = 0; e < node.neighbours.length; e++) {
+      ret.push(graph[node.neighbours[e]]);
+    }
+
+    return ret;
+  }
 }
 
-/**
- * Computes 3D convex hull as a CANNON.ConvexPolyhedron.
- * @param  {THREE.Object3D} mesh
- * @return {CANNON.Shape}
- */
-function createConvexPolyhedron (object) {
-  var i, vertices, faces, hull,
-      eps = 1e-4,
-      geometry = getGeometry(object);
+module.exports = AStar;
 
-  if (!geometry || !geometry.vertices.length) return null;
+},{"./BinaryHeap":80,"./utils.js":83}],80:[function(require,module,exports){
+// javascript-astar
+// http://github.com/bgrins/javascript-astar
+// Freely distributable under the MIT License.
+// Implements the astar search algorithm in javascript using a binary heap.
 
-  // Perturb.
-  for (i = 0; i < geometry.vertices.length; i++) {
-    geometry.vertices[i].x += (Math.random() - 0.5) * eps;
-    geometry.vertices[i].y += (Math.random() - 0.5) * eps;
-    geometry.vertices[i].z += (Math.random() - 0.5) * eps;
+class BinaryHeap {
+  constructor (scoreFunction) {
+    this.content = [];
+    this.scoreFunction = scoreFunction;
   }
 
-  // Compute the 3D convex hull.
-  hull = quickhull(geometry);
+  push (element) {
+    // Add the new element to the end of the array.
+    this.content.push(element);
 
-  // Convert from THREE.Vector3 to CANNON.Vec3.
-  vertices = new Array(hull.vertices.length);
-  for (i = 0; i < hull.vertices.length; i++) {
-    vertices[i] = new CANNON.Vec3(hull.vertices[i].x, hull.vertices[i].y, hull.vertices[i].z);
+    // Allow it to sink down.
+    this.sinkDown(this.content.length - 1);
   }
 
-  // Convert from THREE.Face to Array<number>.
-  faces = new Array(hull.faces.length);
-  for (i = 0; i < hull.faces.length; i++) {
-    faces[i] = [hull.faces[i].a, hull.faces[i].b, hull.faces[i].c];
+  pop () {
+    // Store the first element so we can return it later.
+    const result = this.content[0];
+    // Get the element at the end of the array.
+    const end = this.content.pop();
+    // If there are any elements left, put the end element at the
+    // start, and let it bubble up.
+    if (this.content.length > 0) {
+      this.content[0] = end;
+      this.bubbleUp(0);
+    }
+    return result;
   }
 
-  return new CANNON.ConvexPolyhedron(vertices, faces);
-}
+  remove (node) {
+    const i = this.content.indexOf(node);
 
-/**
- * @param  {THREE.Geometry} geometry
- * @return {CANNON.Shape}
- */
-function createCylinderShape (geometry) {
-  var shape,
-      params = geometry.metadata
-        ? geometry.metadata.parameters
-        : geometry.parameters;
-  shape = new CANNON.Cylinder(
-    params.radiusTop,
-    params.radiusBottom,
-    params.height,
-    params.radialSegments
-  );
-
-  // Include metadata for serialization.
-  shape._type = CANNON.Shape.types.CYLINDER; // Patch schteppe/cannon.js#329.
-  shape.radiusTop = params.radiusTop;
-  shape.radiusBottom = params.radiusBottom;
-  shape.height = params.height;
-  shape.numSegments = params.radialSegments;
-
-  shape.orientation = new CANNON.Quaternion();
-  shape.orientation.setFromEuler(THREE.Math.degToRad(-90), 0, 0, 'XYZ').normalize();
-  return shape;
-}
-
-/**
- * @param  {THREE.Object3D} object
- * @return {CANNON.Shape}
- */
-function createBoundingCylinderShape (object, options) {
-  var shape, height, radius,
-      box = new THREE.Box3(),
-      axes = ['x', 'y', 'z'],
-      majorAxis = options.cylinderAxis || 'y',
-      minorAxes = axes.splice(axes.indexOf(majorAxis), 1) && axes;
-
-  box.setFromObject(object);
-
-  if (!isFinite(box.min.lengthSq())) return null;
-
-  // Compute cylinder dimensions.
-  height = box.max[majorAxis] - box.min[majorAxis];
-  radius = 0.5 * Math.max(
-    box.max[minorAxes[0]] - box.min[minorAxes[0]],
-    box.max[minorAxes[1]] - box.min[minorAxes[1]]
-  );
-
-  // Create shape.
-  shape = new CANNON.Cylinder(radius, radius, height, 12);
-
-  // Include metadata for serialization.
-  shape._type = CANNON.Shape.types.CYLINDER; // Patch schteppe/cannon.js#329.
-  shape.radiusTop = radius;
-  shape.radiusBottom = radius;
-  shape.height = height;
-  shape.numSegments = 12;
-
-  shape.orientation = new CANNON.Quaternion();
-  shape.orientation.setFromEuler(
-    majorAxis === 'y' ? PI_2 : 0,
-    majorAxis === 'z' ? PI_2 : 0,
-    0,
-    'XYZ'
-  ).normalize();
-  return shape;
-}
-
-/**
- * @param  {THREE.Geometry} geometry
- * @return {CANNON.Shape}
- */
-function createPlaneShape (geometry) {
-  geometry.computeBoundingBox();
-  var box = geometry.boundingBox;
-  return new CANNON.Box(new CANNON.Vec3(
-    (box.max.x - box.min.x) / 2 || 0.1,
-    (box.max.y - box.min.y) / 2 || 0.1,
-    (box.max.z - box.min.z) / 2 || 0.1
-  ));
-}
-
-/**
- * @param  {THREE.Geometry} geometry
- * @return {CANNON.Shape}
- */
-function createSphereShape (geometry) {
-  var params = geometry.metadata
-    ? geometry.metadata.parameters
-    : geometry.parameters;
-  return new CANNON.Sphere(params.radius);
-}
-
-/**
- * @param  {THREE.Object3D} object
- * @return {CANNON.Shape}
- */
-function createBoundingSphereShape (object, options) {
-  if (options.sphereRadius) {
-    return new CANNON.Sphere(options.sphereRadius);
-  }
-  var geometry = getGeometry(object);
-  if (!geometry) return null;
-  geometry.computeBoundingSphere();
-  return new CANNON.Sphere(geometry.boundingSphere.radius);
-}
-
-/**
- * @param  {THREE.Geometry} geometry
- * @return {CANNON.Shape}
- */
-function createTrimeshShape (geometry) {
-  var indices,
-      vertices = getVertices(geometry);
-
-  if (!vertices.length) return null;
-
-  indices = Object.keys(vertices).map(Number);
-  return new CANNON.Trimesh(vertices, indices);
-}
-
-/******************************************************************************
- * Utils
- */
-
-/**
- * Returns a single geometry for the given object. If the object is compound,
- * its geometries are automatically merged.
- * @param {THREE.Object3D} object
- * @return {THREE.Geometry}
- */
-function getGeometry (object) {
-  var matrix, mesh,
-      meshes = getMeshes(object),
-      tmp = new THREE.Geometry(),
-      combined = new THREE.Geometry();
-
-  if (meshes.length === 0) return null;
-
-  // Apply scale  – it can't easily be applied to a CANNON.Shape later.
-  if (meshes.length === 1) {
-    var position = new THREE.Vector3(),
-        quaternion = new THREE.Quaternion(),
-        scale = new THREE.Vector3();
-    if (meshes[0].geometry instanceof THREE.BufferGeometry) {
-      if (meshes[0].geometry.attributes.position) {
-        tmp.fromBufferGeometry(meshes[0].geometry);
-      }
-    } else {
-      tmp = meshes[0].geometry.clone();
-    }
-    tmp.metadata = meshes[0].geometry.metadata;
-    meshes[0].updateMatrixWorld();
-    meshes[0].matrixWorld.decompose(position, quaternion, scale);
-    return tmp.scale(scale.x, scale.y, scale.z);
-  }
-
-  // Recursively merge geometry, preserving local transforms.
-  while ((mesh = meshes.pop())) {
-    mesh.updateMatrixWorld();
-    if (mesh.geometry instanceof THREE.BufferGeometry) {
-      tmp.fromBufferGeometry(mesh.geometry);
-      combined.merge(tmp, mesh.matrixWorld);
-    } else {
-      combined.merge(mesh.geometry, mesh.matrixWorld);
-    }
-  }
-
-  matrix = new THREE.Matrix4();
-  matrix.scale(object.scale);
-  combined.applyMatrix(matrix);
-  return combined;
-}
-
-/**
- * @param  {THREE.Geometry} geometry
- * @return {Array<number>}
- */
-function getVertices (geometry) {
-  if (!geometry.attributes) {
-    geometry = new THREE.BufferGeometry().fromGeometry(geometry);
-  }
-  return (geometry.attributes.position || {}).array || [];
-}
-
-/**
- * Returns a flat array of THREE.Mesh instances from the given object. If
- * nested transformations are found, they are applied to child meshes
- * as mesh.userData.matrix, so that each mesh has its position/rotation/scale
- * independently of all of its parents except the top-level object.
- * @param  {THREE.Object3D} object
- * @return {Array<THREE.Mesh>}
- */
-function getMeshes (object) {
-  var meshes = [];
-  object.traverse(function (o) {
-    if (o.type === 'Mesh') {
-      meshes.push(o);
-    }
-  });
-  return meshes;
-}
-
-},{"./lib/THREE.quickhull":80,"cannon":23}],80:[function(require,module,exports){
-/**
-
-  QuickHull
-  ---------
-
-  The MIT License
-
-  Copyright &copy; 2010-2014 three.js authors
-
-  Permission is hereby granted, free of charge, to any person obtaining a copy
-  of this software and associated documentation files (the "Software"), to deal
-  in the Software without restriction, including without limitation the rights
-  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-  copies of the Software, and to permit persons to whom the Software is
-  furnished to do so, subject to the following conditions:
-
-  The above copyright notice and this permission notice shall be included in
-  all copies or substantial portions of the Software.
-
-  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
-  THE SOFTWARE.
-
-
-    @author mark lundin / http://mark-lundin.com
-
-    This is a 3D implementation of the Quick Hull algorithm.
-    It is a fast way of computing a convex hull with average complexity
-    of O(n log(n)).
-    It uses depends on three.js and is supposed to create THREE.Geometry.
-
-    It's also very messy
-
- */
-
-module.exports = (function(){
-
-
-  var faces     = [],
-    faceStack   = [],
-    i, NUM_POINTS, extremes,
-    max     = 0,
-    dcur, current, j, v0, v1, v2, v3,
-    N, D;
-
-  var ab, ac, ax,
-    suba, subb, normal,
-    diff, subaA, subaB, subC;
-
-  function reset(){
-
-    ab    = new THREE.Vector3(),
-    ac    = new THREE.Vector3(),
-    ax    = new THREE.Vector3(),
-    suba  = new THREE.Vector3(),
-    subb  = new THREE.Vector3(),
-    normal  = new THREE.Vector3(),
-    diff  = new THREE.Vector3(),
-    subaA = new THREE.Vector3(),
-    subaB = new THREE.Vector3(),
-    subC  = new THREE.Vector3();
-
-  }
-
-  //temporary vectors
-
-  function process( points ){
-
-    // Iterate through all the faces and remove
-    while( faceStack.length > 0  ){
-      cull( faceStack.shift(), points );
-    }
-  }
-
-
-  var norm = function(){
-
-    var ca = new THREE.Vector3(),
-      ba = new THREE.Vector3(),
-      N = new THREE.Vector3();
-
-    return function( a, b, c ){
-
-      ca.subVectors( c, a );
-      ba.subVectors( b, a );
-
-      N.crossVectors( ca, ba );
-
-      return N.normalize();
-    }
-
-  }();
-
-
-  function getNormal( face, points ){
-
-    if( face.normal !== undefined ) return face.normal;
-
-    var p0 = points[face[0]],
-      p1 = points[face[1]],
-      p2 = points[face[2]];
-
-    ab.subVectors( p1, p0 );
-    ac.subVectors( p2, p0 );
-    normal.crossVectors( ac, ab );
-    normal.normalize();
-
-    return face.normal = normal.clone();
-
-  }
-
-
-  function assignPoints( face, pointset, points ){
-
-    // ASSIGNING POINTS TO FACE
-    var p0 = points[face[0]],
-      dots = [], apex,
-      norm = getNormal( face, points );
-
-
-    // Sory all the points by there distance from the plane
-    pointset.sort( function( aItem, bItem ){
-
-
-      dots[aItem.x/3] = dots[aItem.x/3] !== undefined ? dots[aItem.x/3] : norm.dot( suba.subVectors( aItem, p0 ));
-      dots[bItem.x/3] = dots[bItem.x/3] !== undefined ? dots[bItem.x/3] : norm.dot( subb.subVectors( bItem, p0 ));
-
-      return dots[aItem.x/3] - dots[bItem.x/3] ;
-    });
-
-    //TODO :: Must be a faster way of finding and index in this array
-    var index = pointset.length;
-
-    if( index === 1 ) dots[pointset[0].x/3] = norm.dot( suba.subVectors( pointset[0], p0 ));
-    while( index-- > 0 && dots[pointset[index].x/3] > 0 )
+    // When it is found, the process seen in 'pop' is repeated
+    // to fill up the hole.
+    const end = this.content.pop();
 
-    var point;
-    if( index + 1 < pointset.length && dots[pointset[index+1].x/3] > 0 ){
+    if (i !== this.content.length - 1) {
+      this.content[i] = end;
 
-      face.visiblePoints  = pointset.splice( index + 1 );
+      if (this.scoreFunction(end) < this.scoreFunction(node)) {
+        this.sinkDown(i);
+      } else {
+        this.bubbleUp(i);
+      }
     }
   }
 
+  size () {
+    return this.content.length;
+  }
 
+  rescoreElement (node) {
+    this.sinkDown(this.content.indexOf(node));
+  }
 
+  sinkDown (n) {
+    // Fetch the element that has to be sunk.
+    const element = this.content[n];
 
-  function cull( face, points ){
-
-    var i = faces.length,
-      dot, visibleFace, currentFace,
-      visibleFaces = [face];
-
-    var apex = points.indexOf( face.visiblePoints.pop() );
+    // When at 0, an element can not sink any further.
+    while (n > 0) {
+      // Compute the parent element's index, and fetch it.
+      const parentN = ((n + 1) >> 1) - 1;
+      const parent = this.content[parentN];
 
-    // Iterate through all other faces...
-    while( i-- > 0 ){
-      currentFace = faces[i];
-      if( currentFace !== face ){
-        // ...and check if they're pointing in the same direction
-        dot = getNormal( currentFace, points ).dot( diff.subVectors( points[apex], points[currentFace[0]] ));
-        if( dot > 0 ){
-          visibleFaces.push( currentFace );
-        }
+      if (this.scoreFunction(element) < this.scoreFunction(parent)) {
+        // Swap the elements if the parent is greater.
+        this.content[parentN] = element;
+        this.content[n] = parent;
+        // Update 'n' to continue at the new position.
+        n = parentN;
+      } else {
+        // Found a parent that is less, no need to sink any further.
+        break;
       }
     }
+  }
 
-    var index, neighbouringIndex, vertex;
-
-    // Determine Perimeter - Creates a bounded horizon
-
-    // 1. Pick an edge A out of all possible edges
-    // 2. Check if A is shared by any other face. a->b === b->a
-      // 2.1 for each edge in each triangle, isShared = ( f1.a == f2.a && f1.b == f2.b ) || ( f1.a == f2.b && f1.b == f2.a )
-    // 3. If not shared, then add to convex horizon set,
-        //pick an end point (N) of the current edge A and choose a new edge NA connected to A.
-        //Restart from 1.
-    // 4. If A is shared, it is not an horizon edge, therefore flag both faces that share this edge as candidates for culling
-    // 5. If candidate geometry is a degenrate triangle (ie. the tangent space normal cannot be computed) then remove that triangle from all further processing
-
-
-    var j = i = visibleFaces.length;
-    var isDistinct = false,
-      hasOneVisibleFace = i === 1,
-      cull = [],
-      perimeter = [],
-      edgeIndex = 0, compareFace, nextIndex,
-      a, b;
-
-    var allPoints = [];
-    var originFace = [visibleFaces[0][0], visibleFaces[0][1], visibleFaces[0][1], visibleFaces[0][2], visibleFaces[0][2], visibleFaces[0][0]];
-
+  bubbleUp (n) {
+    // Look up the target element and its score.
+    const length = this.content.length,
+      element = this.content[n],
+      elemScore = this.scoreFunction(element);
 
-    if( visibleFaces.length === 1 ){
-      currentFace = visibleFaces[0];
+    while (true) {
+      // Compute the indices of the child elements.
+      const child2N = (n + 1) << 1,
+        child1N = child2N - 1;
+      // This is used to store the new position of the element,
+      // if any.
+      let swap = null;
+      let child1Score;
+      // If the first child exists (is inside the array)...
+      if (child1N < length) {
+        // Look it up and compute its score.
+        const child1 = this.content[child1N];
+        child1Score = this.scoreFunction(child1);
 
-      perimeter = [currentFace[0], currentFace[1], currentFace[1], currentFace[2], currentFace[2], currentFace[0]];
-      // remove visible face from list of faces
-      if( faceStack.indexOf( currentFace ) > -1 ){
-        faceStack.splice( faceStack.indexOf( currentFace ), 1 );
+        // If the score is less than our element's, we need to swap.
+        if (child1Score < elemScore) {
+          swap = child1N;
+        }
       }
 
-
-      if( currentFace.visiblePoints ) allPoints = allPoints.concat( currentFace.visiblePoints );
-      faces.splice( faces.indexOf( currentFace ), 1 );
-
-    }else{
-
-      while( i-- > 0  ){  // for each visible face
-
-        currentFace = visibleFaces[i];
-
-        // remove visible face from list of faces
-        if( faceStack.indexOf( currentFace ) > -1 ){
-          faceStack.splice( faceStack.indexOf( currentFace ), 1 );
+      // Do the same checks for the other child.
+      if (child2N < length) {
+        const child2 = this.content[child2N],
+          child2Score = this.scoreFunction(child2);
+        if (child2Score < (swap === null ? elemScore : child1Score)) {
+          swap = child2N;
         }
+      }
 
-        if( currentFace.visiblePoints ) allPoints = allPoints.concat( currentFace.visiblePoints );
-        faces.splice( faces.indexOf( currentFace ), 1 );
-
+      // If the element needs to be moved, swap it, and continue.
+      if (swap !== null) {
+        this.content[n] = this.content[swap];
+        this.content[swap] = element;
+        n = swap;
+      }
 
-        var isSharedEdge;
-        cEdgeIndex = 0;
+      // Otherwise, we are done.
+      else {
+        break;
+      }
+    }
+  }
 
-        while( cEdgeIndex < 3 ){ // Iterate through it's edges
+}
 
-          isSharedEdge = false;
-          j = visibleFaces.length;
-          a = currentFace[cEdgeIndex]
-          b = currentFace[(cEdgeIndex+1)%3];
+module.exports = BinaryHeap;
 
+},{}],81:[function(require,module,exports){
+const utils = require('./utils');
 
-          while( j-- > 0 && !isSharedEdge ){ // find another visible faces
+class Channel {
+  constructor () {
+    this.portals = [];
+  }
 
-            compareFace = visibleFaces[j];
-            edgeIndex = 0;
+  push (p1, p2) {
+    if (p2 === undefined) p2 = p1;
+    this.portals.push({
+      left: p1,
+      right: p2
+    });
+  }
 
-            // isSharedEdge = compareFace == currentFace;
-            if( compareFace !== currentFace ){
+  stringPull () {
+    const portals = this.portals;
+    const pts = [];
+    // Init scan state
+    let portalApex, portalLeft, portalRight;
+    let apexIndex = 0,
+      leftIndex = 0,
+      rightIndex = 0;
 
-              while( edgeIndex < 3 && !isSharedEdge ){ //Check all it's indices
+    portalApex = portals[0].left;
+    portalLeft = portals[0].left;
+    portalRight = portals[0].right;
 
-                nextIndex = ( edgeIndex + 1 );
-                isSharedEdge = ( compareFace[edgeIndex] === a && compareFace[nextIndex%3] === b ) ||
-                         ( compareFace[edgeIndex] === b && compareFace[nextIndex%3] === a );
+    // Add start point.
+    pts.push(portalApex);
 
-                edgeIndex++;
-              }
-            }
-          }
+    for (let i = 1; i < portals.length; i++) {
+      const left = portals[i].left;
+      const right = portals[i].right;
 
-          if( !isSharedEdge || hasOneVisibleFace ){
-            perimeter.push( a );
-            perimeter.push( b );
-          }
+      // Update right vertex.
+      if (utils.triarea2(portalApex, portalRight, right) <= 0.0) {
+        if (utils.vequal(portalApex, portalRight) || utils.triarea2(portalApex, portalLeft, right) > 0.0) {
+          // Tighten the funnel.
+          portalRight = right;
+          rightIndex = i;
+        } else {
+          // Right over left, insert left to path and restart scan from portal left point.
+          pts.push(portalLeft);
+          // Make current left the new apex.
+          portalApex = portalLeft;
+          apexIndex = leftIndex;
+          // Reset portal
+          portalLeft = portalApex;
+          portalRight = portalApex;
+          leftIndex = apexIndex;
+          rightIndex = apexIndex;
+          // Restart scan
+          i = apexIndex;
+          continue;
+        }
+      }
 
-          cEdgeIndex++;
+      // Update left vertex.
+      if (utils.triarea2(portalApex, portalLeft, left) >= 0.0) {
+        if (utils.vequal(portalApex, portalLeft) || utils.triarea2(portalApex, portalRight, left) < 0.0) {
+          // Tighten the funnel.
+          portalLeft = left;
+          leftIndex = i;
+        } else {
+          // Left over right, insert right to path and restart scan from portal right point.
+          pts.push(portalRight);
+          // Make current right the new apex.
+          portalApex = portalRight;
+          apexIndex = rightIndex;
+          // Reset portal
+          portalLeft = portalApex;
+          portalRight = portalApex;
+          leftIndex = apexIndex;
+          rightIndex = apexIndex;
+          // Restart scan
+          i = apexIndex;
+          continue;
         }
       }
     }
 
-    // create new face for all pairs around edge
-    i = 0;
-    var l = perimeter.length/2;
-    var f;
-
-    while( i < l ){
-      f = [ perimeter[i*2+1], apex, perimeter[i*2] ];
-      assignPoints( f, allPoints, points );
-      faces.push( f )
-      if( f.visiblePoints !== undefined  )faceStack.push( f );
-      i++;
+    if ((pts.length === 0) || (!utils.vequal(pts[pts.length - 1], portals[portals.length - 1].left))) {
+      // Append last point to path.
+      pts.push(portals[portals.length - 1].left);
     }
 
+    this.path = pts;
+    return pts;
   }
+}
 
-  var distSqPointSegment = function(){
-
-    var ab = new THREE.Vector3(),
-      ac = new THREE.Vector3(),
-      bc = new THREE.Vector3();
-
-    return function( a, b, c ){
-
-        ab.subVectors( b, a );
-        ac.subVectors( c, a );
-        bc.subVectors( c, b );
-
-        var e = ac.dot(ab);
-        if (e < 0.0) return ac.dot( ac );
-        var f = ab.dot( ab );
-        if (e >= f) return bc.dot(  bc );
-        return ac.dot( ac ) - e * e / f;
-
-      }
-
-  }();
+module.exports = Channel;
 
+},{"./utils":83}],82:[function(require,module,exports){
+const utils = require('./utils');
+const AStar = require('./AStar');
+const Channel = require('./Channel');
 
+var polygonId = 1;
 
+var buildPolygonGroups = function (navigationMesh) {
 
+	var polygons = navigationMesh.polygons;
 
-  return function( geometry ){
+	var polygonGroups = [];
+	var groupCount = 0;
 
-    reset();
+	var spreadGroupId = function (polygon) {
+		polygon.neighbours.forEach((neighbour) => {
+			if (neighbour.group === undefined) {
+				neighbour.group = polygon.group;
+				spreadGroupId(neighbour);
+			}
+		});
+	};
 
+	polygons.forEach((polygon) => {
 
-    points    = geometry.vertices;
-    faces     = [],
-    faceStack   = [],
-    i       = NUM_POINTS = points.length,
-    extremes  = points.slice( 0, 6 ),
-    max     = 0;
+		if (polygon.group === undefined) {
+			polygon.group = groupCount++;
+			// Spread it
+			spreadGroupId(polygon);
+		}
 
+		if (!polygonGroups[polygon.group]) polygonGroups[polygon.group] = [];
 
+		polygonGroups[polygon.group].push(polygon);
+	});
 
-    /*
-     *  FIND EXTREMETIES
-     */
-    while( i-- > 0 ){
-      if( points[i].x < extremes[0].x ) extremes[0] = points[i];
-      if( points[i].x > extremes[1].x ) extremes[1] = points[i];
+	console.log('Groups built: ', polygonGroups.length);
 
-      if( points[i].y < extremes[2].y ) extremes[2] = points[i];
-      if( points[i].y < extremes[3].y ) extremes[3] = points[i];
+	return polygonGroups;
+};
 
-      if( points[i].z < extremes[4].z ) extremes[4] = points[i];
-      if( points[i].z < extremes[5].z ) extremes[5] = points[i];
-    }
+var buildPolygonNeighbours = function (polygon, navigationMesh) {
+	polygon.neighbours = [];
 
+	// All other nodes that contain at least two of our vertices are our neighbours
+	for (var i = 0, len = navigationMesh.polygons.length; i < len; i++) {
+		if (polygon === navigationMesh.polygons[i]) continue;
 
-    /*
-     *  Find the longest line between the extremeties
-     */
+		// Don't check polygons that are too far, since the intersection tests take a long time
+		if (polygon.centroid.distanceToSquared(navigationMesh.polygons[i].centroid) > 100 * 100) continue;
 
-    j = i = 6;
-    while( i-- > 0 ){
-      j = i - 1;
-      while( j-- > 0 ){
-          if( max < (dcur = extremes[i].distanceToSquared( extremes[j] )) ){
-        max = dcur;
-        v0 = extremes[ i ];
-        v1 = extremes[ j ];
+		var matches = utils.array_intersect(polygon.vertexIds, navigationMesh.polygons[i].vertexIds);
 
-          }
-        }
-      }
+		if (matches.length >= 2) {
+			polygon.neighbours.push(navigationMesh.polygons[i]);
+		}
+	}
+};
 
+var buildPolygonsFromGeometry = function (geometry) {
 
-      // 3. Find the most distant point to the line segment, this creates a plane
-      i = 6;
-      max = 0;
-    while( i-- > 0 ){
-      dcur = distSqPointSegment( v0, v1, extremes[i]);
-      if( max < dcur ){
-        max = dcur;
-            v2 = extremes[ i ];
-          }
-    }
+	console.log('Vertices:', geometry.vertices.length, 'polygons:', geometry.faces.length);
 
+	var polygons = [];
+	var vertices = geometry.vertices;
+	var faceVertexUvs = geometry.faceVertexUvs;
 
-      // 4. Find the most distant point to the plane.
+	// Convert the faces into a custom format that supports more than 3 vertices
+	geometry.faces.forEach((face) => {
+		polygons.push({
+			id: polygonId++,
+			vertexIds: [face.a, face.b, face.c],
+			centroid: face.centroid,
+			normal: face.normal,
+			neighbours: []
+		});
+	});
 
-      N = norm(v0, v1, v2);
-      D = N.dot( v0 );
+	var navigationMesh = {
+		polygons: polygons,
+		vertices: vertices,
+		faceVertexUvs: faceVertexUvs
+	};
 
+	// Build a list of adjacent polygons
+	polygons.forEach((polygon) => {
+		buildPolygonNeighbours(polygon, navigationMesh);
+	});
 
-      max = 0;
-      i = NUM_POINTS;
-      while( i-- > 0 ){
-        dcur = Math.abs( points[i].dot( N ) - D );
-          if( max < dcur ){
-            max = dcur;
-            v3 = points[i];
-      }
-      }
+	return navigationMesh;
+};
 
+var buildNavigationMesh = function (geometry) {
+	// Prepare geometry
+	utils.computeCentroids(geometry);
+	geometry.mergeVertices();
+	return buildPolygonsFromGeometry(geometry);
+};
 
+var getSharedVerticesInOrder = function (a, b) {
 
-      var v0Index = points.indexOf( v0 ),
-      v1Index = points.indexOf( v1 ),
-      v2Index = points.indexOf( v2 ),
-      v3Index = points.indexOf( v3 );
+	var aList = a.vertexIds;
+	var bList = b.vertexIds;
 
+	var sharedVertices = [];
 
-    //  We now have a tetrahedron as the base geometry.
-    //  Now we must subdivide the
+	aList.forEach((vId) => {
+		if (bList.includes(vId)) {
+			sharedVertices.push(vId);
+		}
+	});
 
-      var tetrahedron =[
-        [ v2Index, v1Index, v0Index ],
-        [ v1Index, v3Index, v0Index ],
-        [ v2Index, v3Index, v1Index ],
-        [ v0Index, v3Index, v2Index ],
-    ];
+	if (sharedVertices.length < 2) return [];
 
+	// console.log("TRYING aList:", aList, ", bList:", bList, ", sharedVertices:", sharedVertices);
 
+	if (sharedVertices.includes(aList[0]) && sharedVertices.includes(aList[aList.length - 1])) {
+		// Vertices on both edges are bad, so shift them once to the left
+		aList.push(aList.shift());
+	}
 
-    subaA.subVectors( v1, v0 ).normalize();
-    subaB.subVectors( v2, v0 ).normalize();
-    subC.subVectors ( v3, v0 ).normalize();
-    var sign  = subC.dot( new THREE.Vector3().crossVectors( subaB, subaA ));
+	if (sharedVertices.includes(bList[0]) && sharedVertices.includes(bList[bList.length - 1])) {
+		// Vertices on both edges are bad, so shift them once to the left
+		bList.push(bList.shift());
+	}
 
+	// Again!
+	sharedVertices = [];
 
-    // Reverse the winding if negative sign
-    if( sign < 0 ){
-      tetrahedron[0].reverse();
-      tetrahedron[1].reverse();
-      tetrahedron[2].reverse();
-      tetrahedron[3].reverse();
-    }
+	aList.forEach((vId) => {
+		if (bList.includes(vId)) {
+			sharedVertices.push(vId);
+		}
+	});
 
+	return sharedVertices;
+};
 
-    //One for each face of the pyramid
-    var pointsCloned = points.slice();
-    pointsCloned.splice( pointsCloned.indexOf( v0 ), 1 );
-    pointsCloned.splice( pointsCloned.indexOf( v1 ), 1 );
-    pointsCloned.splice( pointsCloned.indexOf( v2 ), 1 );
-    pointsCloned.splice( pointsCloned.indexOf( v3 ), 1 );
+var groupNavMesh = function (navigationMesh) {
 
+	var saveObj = {};
 
-    var i = tetrahedron.length;
-    while( i-- > 0 ){
-      assignPoints( tetrahedron[i], pointsCloned, points );
-      if( tetrahedron[i].visiblePoints !== undefined ){
-        faceStack.push( tetrahedron[i] );
-      }
-      faces.push( tetrahedron[i] );
-    }
+	navigationMesh.vertices.forEach((v) => {
+		v.x = utils.roundNumber(v.x, 2);
+		v.y = utils.roundNumber(v.y, 2);
+		v.z = utils.roundNumber(v.z, 2);
+	});
 
-    process( points );
+	saveObj.vertices = navigationMesh.vertices;
 
+	var groups = buildPolygonGroups(navigationMesh);
 
-    //  Assign to our geometry object
+	saveObj.groups = [];
 
-    var ll = faces.length;
-    while( ll-- > 0 ){
-      geometry.faces[ll] = new THREE.Face3( faces[ll][2], faces[ll][1], faces[ll][0], faces[ll].normal )
-    }
+	var findPolygonIndex = function (group, p) {
+		for (var i = 0; i < group.length; i++) {
+			if (p === group[i]) return i;
+		}
+	};
 
-    geometry.normalsNeedUpdate = true;
+	groups.forEach((group) => {
 
-    return geometry;
+		var newGroup = [];
 
-  }
+		group.forEach((p) => {
 
-}())
+			var neighbours = [];
 
-},{}],81:[function(require,module,exports){
-var EPS = 0.1;
+			p.neighbours.forEach((n) => {
+				neighbours.push(findPolygonIndex(group, n));
+			});
 
-module.exports = {
-  schema: {
-    enabled: {default: true},
-    mode: {default: 'teleport', oneOf: ['teleport', 'animate']},
-    animateSpeed: {default: 3.0}
-  },
 
-  init: function () {
-    this.active = true;
-    this.checkpoint = null;
+			// Build a portal list to each neighbour
+			var portals = [];
+			p.neighbours.forEach((n) => {
+				portals.push(getSharedVerticesInOrder(p, n));
+			});
 
-    this.offset = new THREE.Vector3();
-    this.position = new THREE.Vector3();
-    this.targetPosition = new THREE.Vector3();
-  },
 
-  play: function () { this.active = true; },
-  pause: function () { this.active = false; },
+			p.centroid.x = utils.roundNumber(p.centroid.x, 2);
+			p.centroid.y = utils.roundNumber(p.centroid.y, 2);
+			p.centroid.z = utils.roundNumber(p.centroid.z, 2);
 
-  setCheckpoint: function (checkpoint) {
-    var el = this.el;
+			newGroup.push({
+				id: findPolygonIndex(group, p),
+				neighbours: neighbours,
+				vertexIds: p.vertexIds,
+				centroid: p.centroid,
+				portals: portals
+			});
 
-    if (!this.active) return;
-    if (this.checkpoint === checkpoint) return;
+		});
 
-    if (this.checkpoint) {
-      el.emit('navigation-end', {checkpoint: checkpoint});
-    }
+		saveObj.groups.push(newGroup);
+	});
 
-    this.checkpoint = checkpoint;
-    el.emit('navigation-start', {checkpoint: checkpoint});
+	return saveObj;
+};
 
-    if (this.data.mode === 'teleport') {
-      this.sync();
-      this.el.setAttribute('position', this.targetPosition);
-      this.checkpoint = null;
-      el.emit('navigation-end', {checkpoint: checkpoint});
-    }
-  },
+var zoneNodes = {};
 
-  isVelocityActive: function () {
-    return !!(this.active && this.checkpoint);
-  },
+module.exports = {
+	buildNodes: function (geometry) {
+		var navigationMesh = buildNavigationMesh(geometry);
 
-  getVelocity: function () {
-    if (!this.active) return;
+		var zoneNodes = groupNavMesh(navigationMesh);
 
-    var data = this.data,
-        offset = this.offset,
-        position = this.position,
-        targetPosition = this.targetPosition,
-        checkpoint = this.checkpoint;
+		return zoneNodes;
+	},
+	setZoneData: function (zone, data) {
+		zoneNodes[zone] = data;
+	},
+	getGroup: function (zone, position) {
 
-    this.sync();
-    if (position.distanceTo(targetPosition) < EPS) {
-      this.checkpoint = null;
-      this.el.emit('navigation-end', {checkpoint: checkpoint});
-      return offset.set(0, 0, 0);
-    }
-    offset.setLength(data.animateSpeed);
-    return offset;
-  },
+		if (!zoneNodes[zone]) return null;
 
-  sync: function () {
-    var offset = this.offset,
-        position = this.position,
-        targetPosition = this.targetPosition;
+		var closestNodeGroup = null;
 
-    position.copy(this.el.getAttribute('position'));
-    targetPosition.copy(this.checkpoint.object3D.getWorldPosition());
-    targetPosition.add(this.checkpoint.components.checkpoint.getOffset());
-    offset.copy(targetPosition).sub(position);
-  }
-};
+		var distance = Math.pow(50, 2);
 
-},{}],82:[function(require,module,exports){
-/**
- * Gamepad controls for A-Frame.
- *
- * Stripped-down version of: https://github.com/donmccurdy/aframe-gamepad-controls
- *
- * For more information about the Gamepad API, see:
- * https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
- */
+		zoneNodes[zone].groups.forEach((group, index) => {
+			group.forEach((node) => {
+				var measuredDistance = utils.distanceToSquared(node.centroid, position);
+				if (measuredDistance < distance) {
+					closestNodeGroup = index;
+					distance = measuredDistance;
+				}
+			});
+		});
 
-var GamepadButton = require('../../lib/GamepadButton'),
-    GamepadButtonEvent = require('../../lib/GamepadButtonEvent');
+		return closestNodeGroup;
+	},
+	getRandomNode: function (zone, group, nearPosition, nearRange) {
 
-var JOYSTICK_EPS = 0.2;
+		if (!zoneNodes[zone]) return new THREE.Vector3();
 
-module.exports = {
+		nearPosition = nearPosition || null;
+		nearRange = nearRange || 0;
 
-  /*******************************************************************
-   * Statics
-   */
+		var candidates = [];
 
-  GamepadButton: GamepadButton,
+		var polygons = zoneNodes[zone].groups[group];
 
-  /*******************************************************************
-   * Schema
-   */
+		polygons.forEach((p) => {
+			if (nearPosition && nearRange) {
+				if (utils.distanceToSquared(nearPosition, p.centroid) < nearRange * nearRange) {
+					candidates.push(p.centroid);
+				}
+			} else {
+				candidates.push(p.centroid);
+			}
+		});
 
-  schema: {
-    // Controller 0-3
-    controller:        { default: 0, oneOf: [0, 1, 2, 3] },
+		return utils.sample(candidates) || new THREE.Vector3();
+	},
+	getClosestNode: function (position, zone, group, checkPolygon = false) {
+		const nodes = zoneNodes[zone].groups[group];
+		const vertices = zoneNodes[zone].vertices;
+		let closestNode = null;
+		let closestDistance = Infinity;
 
-    // Enable/disable features
-    enabled:           { default: true },
+		nodes.forEach((node) => {
+			const distance = utils.distanceToSquared(node.centroid, position);
+			if (distance < closestDistance
+					&& (!checkPolygon || utils.isVectorInPolygon(position, node, vertices))) {
+				closestNode = node;
+				closestDistance = distance;
+			}
+		});
 
-    // Debugging
-    debug:             { default: false }
-  },
+		return closestNode;
+	},
+	findPath: function (startPosition, targetPosition, zone, group) {
+		const nodes = zoneNodes[zone].groups[group];
+		const vertices = zoneNodes[zone].vertices;
 
-  /*******************************************************************
-   * Core
-   */
+		const closestNode = this.getClosestNode(startPosition, zone, group);
+		const farthestNode = this.getClosestNode(targetPosition, zone, group, true);
 
-  /**
-   * Called once when component is attached. Generally for initial setup.
-   */
-  init: function () {
-    var scene = this.el.sceneEl;
-    this.prevTime = window.performance.now();
+		// If we can't find any node, just go straight to the target
+		if (!closestNode || !farthestNode) {
+			return null;
+		}
 
-    // Button state
-    this.buttons = {};
+		const paths = AStar.search(nodes, closestNode, farthestNode);
 
-    scene.addBehavior(this);
-  },
+		const getPortalFromTo = function (a, b) {
+			for (var i = 0; i < a.neighbours.length; i++) {
+				if (a.neighbours[i] === b.id) {
+					return a.portals[i];
+				}
+			}
+		};
 
-  /**
-   * Called when component is attached and when component data changes.
-   * Generally modifies the entity based on the data.
-   */
-  update: function () { this.tick(); },
+		// We have the corridor, now pull the rope.
+		const channel = new Channel();
+		channel.push(startPosition);
+		for (let i = 0; i < paths.length; i++) {
+			const polygon = paths[i];
+			const nextPolygon = paths[i + 1];
 
-  /**
-   * Called on each iteration of main render loop.
-   */
-  tick: function () {
-    this.updateButtonState();
-  },
+			if (nextPolygon) {
+				const portals = getPortalFromTo(polygon, nextPolygon);
+				channel.push(
+					vertices[portals[0]],
+					vertices[portals[1]]
+				);
+			}
+		}
+		channel.push(targetPosition);
+		channel.stringPull();
 
-  /**
-   * Called when a component is removed (e.g., via removeAttribute).
-   * Generally undoes all modifications to the entity.
-   */
-  remove: function () { },
+		// Return the path, omitting first position (which is already known).
+		const path = channel.path.map((c) => new THREE.Vector3(c.x, c.y, c.z));
+		path.shift();
+		return path;
+	}
+};
 
-  /*******************************************************************
-   * Universal controls - movement
-   */
+},{"./AStar":79,"./Channel":81,"./utils":83}],83:[function(require,module,exports){
+class Utils {
 
-  isVelocityActive: function () {
-    if (!this.data.enabled || !this.isConnected()) return false;
+  static computeCentroids (geometry) {
+    var f, fl, face;
 
-    var dpad = this.getDpad(),
-        joystick0 = this.getJoystick(0),
-        inputX = dpad.x || joystick0.x,
-        inputY = dpad.y || joystick0.y;
+    for ( f = 0, fl = geometry.faces.length; f < fl; f ++ ) {
 
-    return Math.abs(inputX) > JOYSTICK_EPS || Math.abs(inputY) > JOYSTICK_EPS;
-  },
+      face = geometry.faces[ f ];
+      face.centroid = new THREE.Vector3( 0, 0, 0 );
 
-  getVelocityDelta: function () {
-    var dpad = this.getDpad(),
-        joystick0 = this.getJoystick(0),
-        inputX = dpad.x || joystick0.x,
-        inputY = dpad.y || joystick0.y,
-        dVelocity = new THREE.Vector3();
+      face.centroid.add( geometry.vertices[ face.a ] );
+      face.centroid.add( geometry.vertices[ face.b ] );
+      face.centroid.add( geometry.vertices[ face.c ] );
+      face.centroid.divideScalar( 3 );
 
-    if (Math.abs(inputX) > JOYSTICK_EPS) {
-      dVelocity.x += inputX;
-    }
-    if (Math.abs(inputY) > JOYSTICK_EPS) {
-      dVelocity.z += inputY;
     }
+  }
 
-    return dVelocity;
-  },
+  static roundNumber (number, decimals) {
+    var newnumber = Number(number + '').toFixed(parseInt(decimals));
+    return parseFloat(newnumber);
+  }
 
-  /*******************************************************************
-   * Universal controls - rotation
-   */
+  static sample (list) {
+    return list[Math.floor(Math.random() * list.length)];
+  }
 
-  isRotationActive: function () {
-    if (!this.data.enabled || !this.isConnected()) return false;
+  static mergeVertexIds (aList, bList) {
 
-    var joystick1 = this.getJoystick(1);
+    var sharedVertices = [];
 
-    return Math.abs(joystick1.x) > JOYSTICK_EPS || Math.abs(joystick1.y) > JOYSTICK_EPS;
-  },
+    aList.forEach((vID) => {
+      if (bList.indexOf(vID) >= 0) {
+        sharedVertices.push(vID);
+      }
+    });
 
-  getRotationDelta: function () {
-    var lookVector = this.getJoystick(1);
-    if (Math.abs(lookVector.x) <= JOYSTICK_EPS) lookVector.x = 0;
-    if (Math.abs(lookVector.y) <= JOYSTICK_EPS) lookVector.y = 0;
-    return lookVector;
-  },
+    if (sharedVertices.length < 2) return [];
 
-  /*******************************************************************
-   * Button events
-   */
+    if (sharedVertices.includes(aList[0]) && sharedVertices.includes(aList[aList.length - 1])) {
+      // Vertices on both edges are bad, so shift them once to the left
+      aList.push(aList.shift());
+    }
 
-  updateButtonState: function () {
-    var gamepad = this.getGamepad();
-    if (this.data.enabled && gamepad) {
+    if (sharedVertices.includes(bList[0]) && sharedVertices.includes(bList[bList.length - 1])) {
+      // Vertices on both edges are bad, so shift them once to the left
+      bList.push(bList.shift());
+    }
 
-      // Fire DOM events for button state changes.
-      for (var i = 0; i < gamepad.buttons.length; i++) {
-        if (gamepad.buttons[i].pressed && !this.buttons[i]) {
-          this.emit(new GamepadButtonEvent('gamepadbuttondown', i, gamepad.buttons[i]));
-        } else if (!gamepad.buttons[i].pressed && this.buttons[i]) {
-          this.emit(new GamepadButtonEvent('gamepadbuttonup', i, gamepad.buttons[i]));
-        }
-        this.buttons[i] = gamepad.buttons[i].pressed;
+    // Again!
+    sharedVertices = [];
+
+    aList.forEach((vId) => {
+      if (bList.includes(vId)) {
+        sharedVertices.push(vId);
       }
+    });
 
-    } else if (Object.keys(this.buttons)) {
-      // Reset state if controls are disabled or controller is lost.
-      this.buttons = {};
+    var clockwiseMostSharedVertex = sharedVertices[1];
+    var counterClockwiseMostSharedVertex = sharedVertices[0];
+
+
+    var cList = aList.slice();
+    while (cList[0] !== clockwiseMostSharedVertex) {
+      cList.push(cList.shift());
     }
-  },
 
-  emit: function (event) {
-    // Emit original event.
-    this.el.emit(event.type, event);
+    var c = 0;
 
-    // Emit convenience event, identifying button index.
-    this.el.emit(
-      event.type + ':' + event.index,
-      new GamepadButtonEvent(event.type, event.index, event)
-    );
-  },
+    var temp = bList.slice();
+    while (temp[0] !== counterClockwiseMostSharedVertex) {
+      temp.push(temp.shift());
 
-  /*******************************************************************
-   * Gamepad state
-   */
+      if (c++ > 10) throw new Error('Unexpected state');
+    }
 
-  /**
-   * Returns the Gamepad instance attached to the component. If connected,
-   * a proxy-controls component may provide access to Gamepad input from a
-   * remote device.
-   *
-   * @return {Gamepad}
-   */
-  getGamepad: function () {
-    var localGamepad = navigator.getGamepads
-          && navigator.getGamepads()[this.data.controller],
-        proxyControls = this.el.sceneEl.components['proxy-controls'],
-        proxyGamepad = proxyControls && proxyControls.isConnected()
-          && proxyControls.getGamepad(this.data.controller);
-    return proxyGamepad || localGamepad;
-  },
+    // Shave
+    temp.shift();
+    temp.pop();
 
-  /**
-   * Returns the state of the given button.
-   * @param  {number} index The button (0-N) for which to find state.
-   * @return {GamepadButton}
-   */
-  getButton: function (index) {
-    return this.getGamepad().buttons[index];
-  },
+    cList = cList.concat(temp);
 
-  /**
-   * Returns state of the given axis. Axes are labelled 0-N, where 0-1 will
-   * represent X/Y on the first joystick, and 2-3 X/Y on the second.
-   * @param  {number} index The axis (0-N) for which to find state.
-   * @return {number} On the interval [-1,1].
-   */
-  getAxis: function (index) {
-    return this.getGamepad().axes[index];
-  },
+    return cList;
+  }
 
-  /**
-   * Returns the state of the given joystick (0 or 1) as a THREE.Vector2.
-   * @param  {number} id The joystick (0, 1) for which to find state.
-   * @return {THREE.Vector2}
-   */
-  getJoystick: function (index) {
-    var gamepad = this.getGamepad();
-    switch (index) {
-      case 0: return new THREE.Vector2(gamepad.axes[0], gamepad.axes[1]);
-      case 1: return new THREE.Vector2(gamepad.axes[2], gamepad.axes[3]);
-      default: throw new Error('Unexpected joystick index "%d".', index);
-    }
-  },
+  static setPolygonCentroid (polygon, navigationMesh) {
+    var sum = new THREE.Vector3();
 
-  /**
-   * Returns the state of the dpad as a THREE.Vector2.
-   * @return {THREE.Vector2}
-   */
-  getDpad: function () {
-    var gamepad = this.getGamepad();
-    if (!gamepad.buttons[GamepadButton.DPAD_RIGHT]) {
-      return new THREE.Vector2();
-    }
-    return new THREE.Vector2(
-      (gamepad.buttons[GamepadButton.DPAD_RIGHT].pressed ? 1 : 0)
-      + (gamepad.buttons[GamepadButton.DPAD_LEFT].pressed ? -1 : 0),
-      (gamepad.buttons[GamepadButton.DPAD_UP].pressed ? -1 : 0)
-      + (gamepad.buttons[GamepadButton.DPAD_DOWN].pressed ? 1 : 0)
-    );
-  },
+    var vertices = navigationMesh.vertices;
 
-  /**
-   * Returns true if the gamepad is currently connected to the system.
-   * @return {boolean}
-   */
-  isConnected: function () {
-    var gamepad = this.getGamepad();
-    return !!(gamepad && gamepad.connected);
-  },
+    polygon.vertexIds.forEach((vId) => {
+      sum.add(vertices[vId]);
+    });
 
-  /**
-   * Returns a string containing some information about the controller. Result
-   * may vary across browsers, for a given controller.
-   * @return {string}
-   */
-  getID: function () {
-    return this.getGamepad().id;
+    sum.divideScalar(polygon.vertexIds.length);
+
+    polygon.centroid.copy(sum);
   }
-};
 
-},{"../../lib/GamepadButton":4,"../../lib/GamepadButtonEvent":5}],83:[function(require,module,exports){
-var radToDeg = THREE.Math.radToDeg,
-    isMobile = AFRAME.utils.device.isMobile();
+  static cleanPolygon (polygon, navigationMesh) {
 
-module.exports = {
-  schema: {
-    enabled: {default: true},
-    standing: {default: true}
-  },
+    var newVertexIds = [];
 
-  init: function () {
-    this.isPositionCalibrated = false;
-    this.dolly = new THREE.Object3D();
-    this.hmdEuler = new THREE.Euler();
-    this.previousHMDPosition = new THREE.Vector3();
-    this.deltaHMDPosition = new THREE.Vector3();
-    this.vrControls = new THREE.VRControls(this.dolly);
-    this.rotation = new THREE.Vector3();
-  },
+    var vertices = navigationMesh.vertices;
 
-  update: function () {
-    var data = this.data;
-    var vrControls = this.vrControls;
-    vrControls.standing = data.standing;
-    vrControls.update();
-  },
+    for (var i = 0; i < polygon.vertexIds.length; i++) {
+
+      var vertex = vertices[polygon.vertexIds[i]];
+
+      var nextVertexId, previousVertexId;
+      var nextVertex, previousVertex;
+
+      // console.log("nextVertex: ", nextVertex);
+
+      if (i === 0) {
+        nextVertexId = polygon.vertexIds[1];
+        previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 1];
+      } else if (i === polygon.vertexIds.length - 1) {
+        nextVertexId = polygon.vertexIds[0];
+        previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 2];
+      } else {
+        nextVertexId = polygon.vertexIds[i + 1];
+        previousVertexId = polygon.vertexIds[i - 1];
+      }
 
-  tick: function () {
-    this.vrControls.update();
-  },
+      nextVertex = vertices[nextVertexId];
+      previousVertex = vertices[previousVertexId];
 
-  remove: function () {
-    this.vrControls.dispose();
-  },
+      var a = nextVertex.clone().sub(vertex);
+      var b = previousVertex.clone().sub(vertex);
 
-  isRotationActive: function () {
-    var hmdEuler = this.hmdEuler;
-    if (!this.data.enabled || !(this.el.sceneEl.is('vr-mode') || isMobile)) {
-      return false;
-    }
-    hmdEuler.setFromQuaternion(this.dolly.quaternion, 'YXZ');
-    return !isNullVector(hmdEuler);
-  },
+      var angle = a.angleTo(b);
 
-  getRotation: function () {
-    var hmdEuler = this.hmdEuler;
-    return this.rotation.set(
-      radToDeg(hmdEuler.x),
-      radToDeg(hmdEuler.y),
-      radToDeg(hmdEuler.z)
-    );
-  },
+      // console.log(angle);
 
-  isVelocityActive: function () {
-    var deltaHMDPosition = this.deltaHMDPosition;
-    var previousHMDPosition = this.previousHMDPosition;
-    var currentHMDPosition = this.calculateHMDPosition();
-    this.isPositionCalibrated = this.isPositionCalibrated || !isNullVector(previousHMDPosition);
-    if (!this.data.enabled || !this.el.sceneEl.is('vr-mode') || isMobile) {
-      return false;
-    }
-    deltaHMDPosition.copy(currentHMDPosition).sub(previousHMDPosition);
-    previousHMDPosition.copy(currentHMDPosition);
-    return this.isPositionCalibrated && !isNullVector(deltaHMDPosition);
-  },
+      if (angle > Math.PI - 0.01 && angle < Math.PI + 0.01) {
+        // Unneccesary vertex
+        // console.log("Unneccesary vertex: ", polygon.vertexIds[i]);
+        // console.log("Angle between "+previousVertexId+", "+polygon.vertexIds[i]+" "+nextVertexId+" was: ", angle);
 
-  getPositionDelta: function () {
-    return this.deltaHMDPosition;
-  },
 
-  calculateHMDPosition: function () {
-    var dolly = this.dolly;
-    var position = new THREE.Vector3();
-    dolly.updateMatrix();
-    position.setFromMatrixPosition(dolly.matrix);
-    return position;
-  }
-};
+        // Remove the neighbours who had this vertex
+        var goodNeighbours = [];
+        polygon.neighbours.forEach((neighbour) => {
+          if (!neighbour.vertexIds.includes(polygon.vertexIds[i])) {
+            goodNeighbours.push(neighbour);
+          }
+        });
+        polygon.neighbours = goodNeighbours;
 
-function isNullVector (vector) {
-  return vector.x === 0 && vector.y === 0 && vector.z === 0;
-}
 
-},{}],84:[function(require,module,exports){
-var physics = require('aframe-physics-system');
+        // TODO cleanup the list of vertices and rebuild vertexIds for all polygons
+      } else {
+        newVertexIds.push(polygon.vertexIds[i]);
+      }
 
-module.exports = {
-  'checkpoint-controls': require('./checkpoint-controls'),
-  'gamepad-controls':    require('./gamepad-controls'),
-  'hmd-controls':        require('./hmd-controls'),
-  'keyboard-controls':   require('./keyboard-controls'),
-  'mouse-controls':      require('./mouse-controls'),
-  'touch-controls':      require('./touch-controls'),
-  'universal-controls':  require('./universal-controls'),
+    }
 
-  registerAll: function (AFRAME) {
-    if (this._registered) return;
+    // console.log("New vertexIds: ", newVertexIds);
 
-    AFRAME = AFRAME || window.AFRAME;
+    polygon.vertexIds = newVertexIds;
 
-    physics.registerAll();
-    if (!AFRAME.components['checkpoint-controls'])  AFRAME.registerComponent('checkpoint-controls', this['checkpoint-controls']);
-    if (!AFRAME.components['gamepad-controls'])     AFRAME.registerComponent('gamepad-controls',    this['gamepad-controls']);
-    if (!AFRAME.components['hmd-controls'])         AFRAME.registerComponent('hmd-controls',        this['hmd-controls']);
-    if (!AFRAME.components['keyboard-controls'])    AFRAME.registerComponent('keyboard-controls',   this['keyboard-controls']);
-    if (!AFRAME.components['mouse-controls'])       AFRAME.registerComponent('mouse-controls',      this['mouse-controls']);
-    if (!AFRAME.components['touch-controls'])       AFRAME.registerComponent('touch-controls',      this['touch-controls']);
-    if (!AFRAME.components['universal-controls'])   AFRAME.registerComponent('universal-controls',  this['universal-controls']);
+    setPolygonCentroid(polygon, navigationMesh);
 
-    this._registered = true;
   }
-};
 
-},{"./checkpoint-controls":81,"./gamepad-controls":82,"./hmd-controls":83,"./keyboard-controls":85,"./mouse-controls":86,"./touch-controls":87,"./universal-controls":88,"aframe-physics-system":11}],85:[function(require,module,exports){
-require('../../lib/keyboard.polyfill');
+  static isConvex (polygon, navigationMesh) {
 
-var MAX_DELTA = 0.2,
-    PROXY_FLAG = '__keyboard-controls-proxy';
+    var vertices = navigationMesh.vertices;
 
-var KeyboardEvent = window.KeyboardEvent;
+    if (polygon.vertexIds.length < 3) return false;
 
-/**
- * Keyboard Controls component.
- *
- * Stripped-down version of: https://github.com/donmccurdy/aframe-keyboard-controls
- *
- * Bind keyboard events to components, or control your entities with the WASD keys.
- *
- * Why use KeyboardEvent.code? "This is set to a string representing the key that was pressed to
- * generate the KeyboardEvent, without taking the current keyboard layout (e.g., QWERTY vs.
- * Dvorak), locale (e.g., English vs. French), or any modifier keys into account. This is useful
- * when you care about which physical key was pressed, rather thanwhich character it corresponds
- * to. For example, if you’re a writing a game, you might want a certain set of keys to move the
- * player in different directions, and that mapping should ideally be independent of keyboard
- * layout. See: https://developers.google.com/web/updates/2016/04/keyboardevent-keys-codes
- *
- * @namespace wasd-controls
- * keys the entity moves and if you release it will stop. Easing simulates friction.
- * to the entity when pressing the keys.
- * @param {bool} [enabled=true] - To completely enable or disable the controls
- */
-module.exports = {
-  schema: {
-    enabled:           { default: true },
-    debug:             { default: false }
-  },
+    var convex = true;
 
-  init: function () {
-    this.dVelocity = new THREE.Vector3();
-    this.localKeys = {};
-    this.listeners = {
-      keydown: this.onKeyDown.bind(this),
-      keyup: this.onKeyUp.bind(this),
-      blur: this.onBlur.bind(this)
-    };
-    this.attachEventListeners();
-  },
+    var total = 0;
 
-  /*******************************************************************
-  * Movement
-  */
+    var results = [];
 
-  isVelocityActive: function () {
-    return this.data.enabled && !!Object.keys(this.getKeys()).length;
-  },
+    for (var i = 0; i < polygon.vertexIds.length; i++) {
 
-  getVelocityDelta: function () {
-    var data = this.data,
-        keys = this.getKeys();
+      var vertex = vertices[polygon.vertexIds[i]];
 
-    this.dVelocity.set(0, 0, 0);
-    if (data.enabled) {
-      if (keys.KeyW || keys.ArrowUp)    { this.dVelocity.z -= 1; }
-      if (keys.KeyA || keys.ArrowLeft)  { this.dVelocity.x -= 1; }
-      if (keys.KeyS || keys.ArrowDown)  { this.dVelocity.z += 1; }
-      if (keys.KeyD || keys.ArrowRight) { this.dVelocity.x += 1; }
-    }
+      var nextVertex, previousVertex;
 
-    return this.dVelocity.clone();
-  },
+      if (i === 0) {
+        nextVertex = vertices[polygon.vertexIds[1]];
+        previousVertex = vertices[polygon.vertexIds[polygon.vertexIds.length - 1]];
+      } else if (i === polygon.vertexIds.length - 1) {
+        nextVertex = vertices[polygon.vertexIds[0]];
+        previousVertex = vertices[polygon.vertexIds[polygon.vertexIds.length - 2]];
+      } else {
+        nextVertex = vertices[polygon.vertexIds[i + 1]];
+        previousVertex = vertices[polygon.vertexIds[i - 1]];
+      }
 
-  /*******************************************************************
-  * Events
-  */
+      var a = nextVertex.clone().sub(vertex);
+      var b = previousVertex.clone().sub(vertex);
 
-  play: function () {
-    this.attachEventListeners();
-  },
+      var angle = a.angleTo(b);
+      total += angle;
 
-  pause: function () {
-    this.removeEventListeners();
-  },
+      if (angle === Math.PI || angle === 0) return false;
 
-  remove: function () {
-    this.pause();
-  },
+      var r = a.cross(b).y;
+      results.push(r);
+    }
 
-  attachEventListeners: function () {
-    window.addEventListener('keydown', this.listeners.keydown, false);
-    window.addEventListener('keyup', this.listeners.keyup, false);
-    window.addEventListener('blur', this.listeners.blur, false);
-  },
+    // if ( total > (polygon.vertexIds.length-2)*Math.PI ) return false;
 
-  removeEventListeners: function () {
-    window.removeEventListener('keydown', this.listeners.keydown);
-    window.removeEventListener('keyup', this.listeners.keyup);
-    window.removeEventListener('blur', this.listeners.blur);
-  },
+    results.forEach((r) => {
+      if (r === 0) convex = false;
+    });
 
-  onKeyDown: function (event) {
-    if (AFRAME.utils.shouldCaptureKeyEvent(event)) {
-      this.localKeys[event.code] = true;
-      this.emit(event);
+    if (results[0] > 0) {
+      results.forEach((r) => {
+        if (r < 0) convex = false;
+      });
+    } else {
+      results.forEach((r) => {
+        if (r > 0) convex = false;
+      });
     }
-  },
 
-  onKeyUp: function (event) {
-    if (AFRAME.utils.shouldCaptureKeyEvent(event)) {
-      delete this.localKeys[event.code];
-      this.emit(event);
-    }
-  },
+    return convex;
+  }
 
-  onBlur: function () {
-    for (var code in this.localKeys) {
-      if (this.localKeys.hasOwnProperty(code)) {
-        delete this.localKeys[code];
-      }
-    }
-  },
+  static distanceToSquared (a, b) {
 
-  emit: function (event) {
-    // TODO - keydown only initially?
-    // TODO - where the f is the spacebar
+    var dx = a.x - b.x;
+    var dy = a.y - b.y;
+    var dz = a.z - b.z;
+
+    return dx * dx + dy * dy + dz * dz;
+
+  }
+
+  //+ Jonas Raoni Soares Silva
+  //@ http://jsfromhell.com/math/is-point-in-poly [rev. #0]
+  static isPointInPoly (poly, pt) {
+    for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
+      ((poly[i].z <= pt.z && pt.z < poly[j].z) || (poly[j].z <= pt.z && pt.z < poly[i].z)) && (pt.x < (poly[j].x - poly[i].x) * (pt.z - poly[i].z) / (poly[j].z - poly[i].z) + poly[i].x) && (c = !c);
+    return c;
+  }
 
-    // Emit original event.
-    if (PROXY_FLAG in event) {
-      // TODO - Method never triggered.
-      this.el.emit(event.type, event);
-    }
+  static isVectorInPolygon (vector, polygon, vertices) {
 
-    // Emit convenience event, identifying key.
-    this.el.emit(event.type + ':' + event.code, new KeyboardEvent(event.type, event));
-    if (this.data.debug) console.log(event.type + ':' + event.code);
-  },
+    // reference point will be the centroid of the polygon
+    // We need to rotate the vector as well as all the points which the polygon uses
 
-  /*******************************************************************
-  * Accessors
-  */
+    var lowestPoint = 100000;
+    var highestPoint = -100000;
 
-  isPressed: function (code) {
-    return code in this.getKeys();
-  },
+    var polygonVertices = [];
 
-  getKeys: function () {
-    if (this.isProxied()) {
-      return this.el.sceneEl.components['proxy-controls'].getKeyboard();
+    polygon.vertexIds.forEach((vId) => {
+      lowestPoint = Math.min(vertices[vId].y, lowestPoint);
+      highestPoint = Math.max(vertices[vId].y, highestPoint);
+      polygonVertices.push(vertices[vId]);
+    });
+
+    if (vector.y < highestPoint + 0.5 && vector.y > lowestPoint - 0.5 &&
+      this.isPointInPoly(polygonVertices, vector)) {
+      return true;
     }
-    return this.localKeys;
-  },
+    return false;
+  }
 
-  isProxied: function () {
-    var proxyControls = this.el.sceneEl.components['proxy-controls'];
-    return proxyControls && proxyControls.isConnected();
+  static triarea2 (a, b, c) {
+    var ax = b.x - a.x;
+    var az = b.z - a.z;
+    var bx = c.x - a.x;
+    var bz = c.z - a.z;
+    return bx * az - ax * bz;
   }
 
-};
+  static vequal (a, b) {
+    return this.distanceToSquared(a, b) < 0.00001;
+  }
 
-},{"../../lib/keyboard.polyfill":10}],86:[function(require,module,exports){
-document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
+  static array_intersect () {
+    let i, shortest, nShortest, n, len, ret = [],
+      obj = {},
+      nOthers;
+    nOthers = arguments.length - 1;
+    nShortest = arguments[0].length;
+    shortest = 0;
+    for (i = 0; i <= nOthers; i++) {
+      n = arguments[i].length;
+      if (n < nShortest) {
+        shortest = i;
+        nShortest = n;
+      }
+    }
 
-/**
- * Mouse + Pointerlock controls.
- *
- * Based on: https://github.com/aframevr/aframe/pull/1056
- */
-module.exports = {
-  schema: {
-    enabled: { default: true },
-    pointerlockEnabled: { default: true },
-    sensitivity: { default: 1 / 25 }
-  },
+    for (i = 0; i <= nOthers; i++) {
+      n = (i === shortest) ? 0 : (i || shortest); //Read the shortest array first. Read the first array instead of the shortest
+      len = arguments[n].length;
+      for (var j = 0; j < len; j++) {
+        var elem = arguments[n][j];
+        if (obj[elem] === i - 1) {
+          if (i === nOthers) {
+            ret.push(elem);
+            obj[elem] = 0;
+          } else {
+            obj[elem] = i;
+          }
+        } else if (i === 0) {
+          obj[elem] = 0;
+        }
+      }
+    }
+    return ret;
+  }
+}
 
-  init: function () {
-    this.mouseDown = false;
-    this.pointerLocked = false;
-    this.lookVector = new THREE.Vector2();
-    this.bindMethods();
-  },
 
-  update: function (previousData) {
-    var data = this.data;
-    if (previousData.pointerlockEnabled && !data.pointerlockEnabled && this.pointerLocked) {
-      document.exitPointerLock();
-    }
-  },
 
-  play: function () {
-    this.addEventListeners();
-  },
+module.exports = Utils;
 
-  pause: function () {
-    this.removeEventListeners();
-    this.lookVector.set(0, 0);
-  },
+},{}],84:[function(require,module,exports){
+var CANNON = require('cannon'),
+    quickhull = require('./lib/THREE.quickhull');
 
-  remove: function () {
-    this.pause();
-  },
+var PI_2 = Math.PI / 2;
 
-  bindMethods: function () {
-    this.onMouseDown = this.onMouseDown.bind(this);
-    this.onMouseMove = this.onMouseMove.bind(this);
-    this.onMouseUp = this.onMouseUp.bind(this);
-    this.onMouseUp = this.onMouseUp.bind(this);
-    this.onPointerLockChange = this.onPointerLockChange.bind(this);
-    this.onPointerLockChange = this.onPointerLockChange.bind(this);
-    this.onPointerLockChange = this.onPointerLockChange.bind(this);
-  },
+var Type = {
+  BOX: 'Box',
+  CYLINDER: 'Cylinder',
+  SPHERE: 'Sphere',
+  HULL: 'ConvexPolyhedron',
+  MESH: 'Trimesh'
+};
 
-  addEventListeners: function () {
-    var sceneEl = this.el.sceneEl;
-    var canvasEl = sceneEl.canvas;
-    var data = this.data;
+/**
+ * Given a THREE.Object3D instance, creates a corresponding CANNON shape.
+ * @param  {THREE.Object3D} object
+ * @return {CANNON.Shape}
+ */
+module.exports = CANNON.mesh2shape = function (object, options) {
+  options = options || {};
 
-    if (!canvasEl) {
-      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
-      return;
-    }
+  var geometry;
 
-    canvasEl.addEventListener('mousedown', this.onMouseDown, false);
-    canvasEl.addEventListener('mousemove', this.onMouseMove, false);
-    canvasEl.addEventListener('mouseup', this.onMouseUp, false);
-    canvasEl.addEventListener('mouseout', this.onMouseUp, false);
+  if (options.type === Type.BOX) {
+    return createBoundingBoxShape(object);
+  } else if (options.type === Type.CYLINDER) {
+    return createBoundingCylinderShape(object, options);
+  } else if (options.type === Type.SPHERE) {
+    return createBoundingSphereShape(object, options);
+  } else if (options.type === Type.HULL) {
+    return createConvexPolyhedron(object);
+  } else if (options.type === Type.MESH) {
+    geometry = getGeometry(object);
+    return geometry ? createTrimeshShape(geometry) : null;
+  } else if (options.type) {
+    throw new Error('[CANNON.mesh2shape] Invalid type "%s".', options.type);
+  }
 
-    if (data.pointerlockEnabled) {
-      document.addEventListener('pointerlockchange', this.onPointerLockChange, false);
-      document.addEventListener('mozpointerlockchange', this.onPointerLockChange, false);
-      document.addEventListener('pointerlockerror', this.onPointerLockError, false);
-    }
-  },
+  geometry = getGeometry(object);
+  if (!geometry) return null;
 
-  removeEventListeners: function () {
-    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
-    if (canvasEl) {
-      canvasEl.removeEventListener('mousedown', this.onMouseDown, false);
-      canvasEl.removeEventListener('mousemove', this.onMouseMove, false);
-      canvasEl.removeEventListener('mouseup', this.onMouseUp, false);
-      canvasEl.removeEventListener('mouseout', this.onMouseUp, false);
-    }
-    document.removeEventListener('pointerlockchange', this.onPointerLockChange, false);
-    document.removeEventListener('mozpointerlockchange', this.onPointerLockChange, false);
-    document.removeEventListener('pointerlockerror', this.onPointerLockError, false);
-  },
+  var type = geometry.metadata
+    ? geometry.metadata.type
+    : geometry.type;
 
-  isRotationActive: function () {
-    return this.data.enabled && (this.mouseDown || this.pointerLocked);
-  },
+  switch (type) {
+    case 'BoxGeometry':
+    case 'BoxBufferGeometry':
+      return createBoxShape(geometry);
+    case 'CylinderGeometry':
+    case 'CylinderBufferGeometry':
+      return createCylinderShape(geometry);
+    case 'PlaneGeometry':
+    case 'PlaneBufferGeometry':
+      return createPlaneShape(geometry);
+    case 'SphereGeometry':
+    case 'SphereBufferGeometry':
+      return createSphereShape(geometry);
+    case 'TubeGeometry':
+    case 'Geometry':
+    case 'BufferGeometry':
+      return createBoundingBoxShape(object);
+    default:
+      console.warn('Unrecognized geometry: "%s". Using bounding box as shape.', geometry.type);
+      return createBoxShape(geometry);
+  }
+};
 
-  /**
-   * Returns the sum of all mouse movement since last call.
-   */
-  getRotationDelta: function () {
-    var dRotation = this.lookVector.clone().multiplyScalar(this.data.sensitivity);
-    this.lookVector.set(0, 0);
-    return dRotation;
-  },
+CANNON.mesh2shape.Type = Type;
 
-  onMouseMove: function (event) {
-    var previousMouseEvent = this.previousMouseEvent;
+/******************************************************************************
+ * Shape construction
+ */
 
-    if (!this.data.enabled || !(this.mouseDown || this.pointerLocked)) {
-      return;
-    }
+ /**
+  * @param  {THREE.Geometry} geometry
+  * @return {CANNON.Shape}
+  */
+ function createBoxShape (geometry) {
+   var vertices = getVertices(geometry);
 
-    var movementX = event.movementX || event.mozMovementX || 0;
-    var movementY = event.movementY || event.mozMovementY || 0;
+   if (!vertices.length) return null;
+
+   geometry.computeBoundingBox();
+   var box = geometry.boundingBox;
+   return new CANNON.Box(new CANNON.Vec3(
+     (box.max.x - box.min.x) / 2,
+     (box.max.y - box.min.y) / 2,
+     (box.max.z - box.min.z) / 2
+   ));
+ }
 
-    if (!this.pointerLocked) {
-      movementX = event.screenX - previousMouseEvent.screenX;
-      movementY = event.screenY - previousMouseEvent.screenY;
-    }
+/**
+ * Bounding box needs to be computed with the entire mesh, not just geometry.
+ * @param  {THREE.Object3D} mesh
+ * @return {CANNON.Shape}
+ */
+function createBoundingBoxShape (object) {
+  var shape, localPosition, worldPosition,
+      box = new THREE.Box3();
 
-    this.lookVector.x += movementX;
-    this.lookVector.y += movementY;
+  box.setFromObject(object);
 
-    this.previousMouseEvent = event;
-  },
+  if (!isFinite(box.min.lengthSq())) return null;
 
-  onMouseDown: function (event) {
-    var canvasEl = this.el.sceneEl.canvas,
-        isEditing = (AFRAME.INSPECTOR || {}).opened;
+  shape = new CANNON.Box(new CANNON.Vec3(
+    (box.max.x - box.min.x) / 2,
+    (box.max.y - box.min.y) / 2,
+    (box.max.z - box.min.z) / 2
+  ));
 
-    this.mouseDown = true;
-    this.previousMouseEvent = event;
+  object.updateMatrixWorld();
+  worldPosition = new THREE.Vector3();
+  worldPosition.setFromMatrixPosition(object.matrixWorld);
+  localPosition = box.translate(worldPosition.negate()).getCenter();
+  if (localPosition.lengthSq()) {
+    shape.offset = localPosition;
+  }
 
-    if (this.data.pointerlockEnabled && !this.pointerLocked && !isEditing) {
-      if (canvasEl.requestPointerLock) {
-        canvasEl.requestPointerLock();
-      } else if (canvasEl.mozRequestPointerLock) {
-        canvasEl.mozRequestPointerLock();
-      }
-    }
-  },
+  return shape;
+}
 
-  onMouseUp: function () {
-    this.mouseDown = false;
-  },
+/**
+ * Computes 3D convex hull as a CANNON.ConvexPolyhedron.
+ * @param  {THREE.Object3D} mesh
+ * @return {CANNON.Shape}
+ */
+function createConvexPolyhedron (object) {
+  var i, vertices, faces, hull,
+      eps = 1e-4,
+      geometry = getGeometry(object);
 
-  onPointerLockChange: function () {
-    this.pointerLocked = !!(document.pointerLockElement || document.mozPointerLockElement);
-  },
+  if (!geometry || !geometry.vertices.length) return null;
 
-  onPointerLockError: function () {
-    this.pointerLocked = false;
+  // Perturb.
+  for (i = 0; i < geometry.vertices.length; i++) {
+    geometry.vertices[i].x += (Math.random() - 0.5) * eps;
+    geometry.vertices[i].y += (Math.random() - 0.5) * eps;
+    geometry.vertices[i].z += (Math.random() - 0.5) * eps;
   }
-};
-
-},{}],87:[function(require,module,exports){
-module.exports = {
-  schema: {
-    enabled: { default: true }
-  },
 
-  init: function () {
-    this.dVelocity = new THREE.Vector3();
-    this.bindMethods();
-  },
+  // Compute the 3D convex hull.
+  hull = quickhull(geometry);
 
-  play: function () {
-    this.addEventListeners();
-  },
+  // Convert from THREE.Vector3 to CANNON.Vec3.
+  vertices = new Array(hull.vertices.length);
+  for (i = 0; i < hull.vertices.length; i++) {
+    vertices[i] = new CANNON.Vec3(hull.vertices[i].x, hull.vertices[i].y, hull.vertices[i].z);
+  }
 
-  pause: function () {
-    this.removeEventListeners();
-    this.dVelocity.set(0, 0, 0);
-  },
+  // Convert from THREE.Face to Array<number>.
+  faces = new Array(hull.faces.length);
+  for (i = 0; i < hull.faces.length; i++) {
+    faces[i] = [hull.faces[i].a, hull.faces[i].b, hull.faces[i].c];
+  }
 
-  remove: function () {
-    this.pause();
-  },
+  return new CANNON.ConvexPolyhedron(vertices, faces);
+}
 
-  addEventListeners: function () {
-    var sceneEl = this.el.sceneEl;
-    var canvasEl = sceneEl.canvas;
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
+ */
+function createCylinderShape (geometry) {
+  var shape,
+      params = geometry.metadata
+        ? geometry.metadata.parameters
+        : geometry.parameters;
+  shape = new CANNON.Cylinder(
+    params.radiusTop,
+    params.radiusBottom,
+    params.height,
+    params.radialSegments
+  );
 
-    if (!canvasEl) {
-      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
-      return;
-    }
+  // Include metadata for serialization.
+  shape._type = CANNON.Shape.types.CYLINDER; // Patch schteppe/cannon.js#329.
+  shape.radiusTop = params.radiusTop;
+  shape.radiusBottom = params.radiusBottom;
+  shape.height = params.height;
+  shape.numSegments = params.radialSegments;
 
-    canvasEl.addEventListener('touchstart', this.onTouchStart);
-    canvasEl.addEventListener('touchend', this.onTouchEnd);
-  },
+  shape.orientation = new CANNON.Quaternion();
+  shape.orientation.setFromEuler(THREE.Math.degToRad(-90), 0, 0, 'XYZ').normalize();
+  return shape;
+}
 
-  removeEventListeners: function () {
-    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
-    if (!canvasEl) { return; }
+/**
+ * @param  {THREE.Object3D} object
+ * @return {CANNON.Shape}
+ */
+function createBoundingCylinderShape (object, options) {
+  var shape, height, radius,
+      box = new THREE.Box3(),
+      axes = ['x', 'y', 'z'],
+      majorAxis = options.cylinderAxis || 'y',
+      minorAxes = axes.splice(axes.indexOf(majorAxis), 1) && axes;
 
-    canvasEl.removeEventListener('touchstart', this.onTouchStart);
-    canvasEl.removeEventListener('touchend', this.onTouchEnd);
-  },
+  box.setFromObject(object);
 
-  isVelocityActive: function () {
-    return this.data.enabled && this.isMoving;
-  },
+  if (!isFinite(box.min.lengthSq())) return null;
 
-  getVelocityDelta: function () {
-    this.dVelocity.z = this.isMoving ? -1 : 0;
-    return this.dVelocity.clone();
-  },
+  // Compute cylinder dimensions.
+  height = box.max[majorAxis] - box.min[majorAxis];
+  radius = 0.5 * Math.max(
+    box.max[minorAxes[0]] - box.min[minorAxes[0]],
+    box.max[minorAxes[1]] - box.min[minorAxes[1]]
+  );
 
-  bindMethods: function () {
-    this.onTouchStart = this.onTouchStart.bind(this);
-    this.onTouchEnd = this.onTouchEnd.bind(this);
-  },
+  // Create shape.
+  shape = new CANNON.Cylinder(radius, radius, height, 12);
 
-  onTouchStart: function (e) {
-    this.isMoving = true;
-    e.preventDefault();
-  },
+  // Include metadata for serialization.
+  shape._type = CANNON.Shape.types.CYLINDER; // Patch schteppe/cannon.js#329.
+  shape.radiusTop = radius;
+  shape.radiusBottom = radius;
+  shape.height = height;
+  shape.numSegments = 12;
 
-  onTouchEnd: function (e) {
-    this.isMoving = false;
-    e.preventDefault();
-  }
-};
+  shape.orientation = new CANNON.Quaternion();
+  shape.orientation.setFromEuler(
+    majorAxis === 'y' ? PI_2 : 0,
+    majorAxis === 'z' ? PI_2 : 0,
+    0,
+    'XYZ'
+  ).normalize();
+  return shape;
+}
 
-},{}],88:[function(require,module,exports){
 /**
- * Universal Controls
- *
- * @author Don McCurdy <dm@donmccurdy.com>
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
  */
+function createPlaneShape (geometry) {
+  geometry.computeBoundingBox();
+  var box = geometry.boundingBox;
+  return new CANNON.Box(new CANNON.Vec3(
+    (box.max.x - box.min.x) / 2 || 0.1,
+    (box.max.y - box.min.y) / 2 || 0.1,
+    (box.max.z - box.min.z) / 2 || 0.1
+  ));
+}
 
-var COMPONENT_SUFFIX = '-controls',
-    MAX_DELTA = 0.2, // ms
-    PI_2 = Math.PI / 2;
-
-module.exports = {
-
-  /*******************************************************************
-   * Schema
-   */
-
-  dependencies: ['velocity', 'rotation'],
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
+ */
+function createSphereShape (geometry) {
+  var params = geometry.metadata
+    ? geometry.metadata.parameters
+    : geometry.parameters;
+  return new CANNON.Sphere(params.radius);
+}
 
-  schema: {
-    enabled:              { default: true },
-    movementEnabled:      { default: true },
-    movementControls:     { default: ['gamepad', 'keyboard', 'touch', 'hmd'] },
-    rotationEnabled:      { default: true },
-    rotationControls:     { default: ['hmd', 'gamepad', 'mouse'] },
-    movementSpeed:        { default: 5 }, // m/s
-    movementEasing:       { default: 15 }, // m/s2
-    movementEasingY:      { default: 0  }, // m/s2
-    movementAcceleration: { default: 80 }, // m/s2
-    rotationSensitivity:  { default: 0.05 }, // radians/frame, ish
-    fly:                  { default: false },
-  },
+/**
+ * @param  {THREE.Object3D} object
+ * @return {CANNON.Shape}
+ */
+function createBoundingSphereShape (object, options) {
+  if (options.sphereRadius) {
+    return new CANNON.Sphere(options.sphereRadius);
+  }
+  var geometry = getGeometry(object);
+  if (!geometry) return null;
+  geometry.computeBoundingSphere();
+  return new CANNON.Sphere(geometry.boundingSphere.radius);
+}
 
-  /*******************************************************************
-   * Lifecycle
-   */
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {CANNON.Shape}
+ */
+function createTrimeshShape (geometry) {
+  var indices,
+      vertices = getVertices(geometry);
 
-  init: function () {
-    var rotation = this.el.getAttribute('rotation');
+  if (!vertices.length) return null;
 
-    if (this.el.hasAttribute('look-controls') && this.data.rotationEnabled) {
-      console.error('[universal-controls] The `universal-controls` component is a replacement '
-        + 'for `look-controls`, and cannot be used in combination with it.');
-    }
+  indices = Object.keys(vertices).map(Number);
+  return new CANNON.Trimesh(vertices, indices);
+}
 
-    // Movement
-    this.velocity = new THREE.Vector3();
+/******************************************************************************
+ * Utils
+ */
 
-    // Rotation
-    this.pitch = new THREE.Object3D();
-    this.pitch.rotation.x = THREE.Math.degToRad(rotation.x);
-    this.yaw = new THREE.Object3D();
-    this.yaw.position.y = 10;
-    this.yaw.rotation.y = THREE.Math.degToRad(rotation.y);
-    this.yaw.add(this.pitch);
-    this.heading = new THREE.Euler(0, 0, 0, 'YXZ');
+/**
+ * Returns a single geometry for the given object. If the object is compound,
+ * its geometries are automatically merged.
+ * @param {THREE.Object3D} object
+ * @return {THREE.Geometry}
+ */
+function getGeometry (object) {
+  var matrix, mesh,
+      meshes = getMeshes(object),
+      tmp = new THREE.Geometry(),
+      combined = new THREE.Geometry();
 
-    if (this.el.sceneEl.hasLoaded) {
-      this.injectControls();
+  if (meshes.length === 0) return null;
+
+  // Apply scale  – it can't easily be applied to a CANNON.Shape later.
+  if (meshes.length === 1) {
+    var position = new THREE.Vector3(),
+        quaternion = new THREE.Quaternion(),
+        scale = new THREE.Vector3();
+    if (meshes[0].geometry.isBufferGeometry) {
+      if (meshes[0].geometry.attributes.position) {
+        tmp.fromBufferGeometry(meshes[0].geometry);
+      }
     } else {
-      this.el.sceneEl.addEventListener('loaded', this.injectControls.bind(this));
+      tmp = meshes[0].geometry.clone();
     }
-  },
+    tmp.metadata = meshes[0].geometry.metadata;
+    meshes[0].updateMatrixWorld();
+    meshes[0].matrixWorld.decompose(position, quaternion, scale);
+    return tmp.scale(scale.x, scale.y, scale.z);
+  }
 
-  update: function () {
-    if (this.el.sceneEl.hasLoaded) {
-      this.injectControls();
+  // Recursively merge geometry, preserving local transforms.
+  while ((mesh = meshes.pop())) {
+    mesh.updateMatrixWorld();
+    if (mesh.geometry.isBufferGeometry) {
+      tmp.fromBufferGeometry(mesh.geometry);
+      combined.merge(tmp, mesh.matrixWorld);
+    } else {
+      combined.merge(mesh.geometry, mesh.matrixWorld);
     }
-  },
+  }
 
-  injectControls: function () {
-    var i, name,
-        data = this.data;
+  matrix = new THREE.Matrix4();
+  matrix.scale(object.scale);
+  combined.applyMatrix(matrix);
+  return combined;
+}
 
-    for (i = 0; i < data.movementControls.length; i++) {
-      name = data.movementControls[i] + COMPONENT_SUFFIX;
-      if (!this.el.components[name]) {
-        this.el.setAttribute(name, '');
-      }
-    }
+/**
+ * @param  {THREE.Geometry} geometry
+ * @return {Array<number>}
+ */
+function getVertices (geometry) {
+  if (!geometry.attributes) {
+    geometry = new THREE.BufferGeometry().fromGeometry(geometry);
+  }
+  return (geometry.attributes.position || {}).array || [];
+}
 
-    for (i = 0; i < data.rotationControls.length; i++) {
-      name = data.rotationControls[i] + COMPONENT_SUFFIX;
-      if (!this.el.components[name]) {
-        this.el.setAttribute(name, '');
-      }
+/**
+ * Returns a flat array of THREE.Mesh instances from the given object. If
+ * nested transformations are found, they are applied to child meshes
+ * as mesh.userData.matrix, so that each mesh has its position/rotation/scale
+ * independently of all of its parents except the top-level object.
+ * @param  {THREE.Object3D} object
+ * @return {Array<THREE.Mesh>}
+ */
+function getMeshes (object) {
+  var meshes = [];
+  object.traverse(function (o) {
+    if (o.type === 'Mesh') {
+      meshes.push(o);
     }
-  },
+  });
+  return meshes;
+}
 
-  /*******************************************************************
-   * Tick
-   */
+},{"./lib/THREE.quickhull":85,"cannon":23}],85:[function(require,module,exports){
+/**
 
-  tick: function (t, dt) {
-    if (!dt) { return; }
+  QuickHull
+  ---------
 
-    // Update rotation.
-    if (this.data.rotationEnabled) this.updateRotation(dt);
+  The MIT License
 
-    // Update velocity. If FPS is too low, reset.
-    if (this.data.movementEnabled && dt / 1000 > MAX_DELTA) {
-      this.velocity.set(0, 0, 0);
-      this.el.setAttribute('velocity', this.velocity);
-    } else {
-      this.updateVelocity(dt);
-    }
-  },
+  Copyright &copy; 2010-2014 three.js authors
 
-  /*******************************************************************
-   * Rotation
-   */
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
 
-  updateRotation: function (dt) {
-    var control, dRotation,
-        data = this.data;
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
 
-    for (var i = 0, l = data.rotationControls.length; i < l; i++) {
-      control = this.el.components[data.rotationControls[i] + COMPONENT_SUFFIX];
-      if (control && control.isRotationActive()) {
-        if (control.getRotationDelta) {
-          dRotation = control.getRotationDelta(dt);
-          dRotation.multiplyScalar(data.rotationSensitivity);
-          this.yaw.rotation.y -= dRotation.x;
-          this.pitch.rotation.x -= dRotation.y;
-          this.pitch.rotation.x = Math.max(-PI_2, Math.min(PI_2, this.pitch.rotation.x));
-          this.el.setAttribute('rotation', {
-            x: THREE.Math.radToDeg(this.pitch.rotation.x),
-            y: THREE.Math.radToDeg(this.yaw.rotation.y),
-            z: 0
-          });
-        } else if (control.getRotation) {
-          this.el.setAttribute('rotation', control.getRotation());
-        } else {
-          throw new Error('Incompatible rotation controls: %s', data.rotationControls[i]);
-        }
-        break;
-      }
-    }
-  },
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 
-  /*******************************************************************
-   * Movement
-   */
+  THE SOFTWARE.
 
-  updateVelocity: function (dt) {
-    var control, dVelocity,
-        velocity = this.velocity,
-        data = this.data;
 
-    if (data.movementEnabled) {
-      for (var i = 0, l = data.movementControls.length; i < l; i++) {
-        control = this.el.components[data.movementControls[i] + COMPONENT_SUFFIX];
-        if (control && control.isVelocityActive()) {
-          if (control.getVelocityDelta) {
-            dVelocity = control.getVelocityDelta(dt);
-          } else if (control.getVelocity) {
-            this.el.setAttribute('velocity', control.getVelocity());
-            return;
-          } else if (control.getPositionDelta) {
-            velocity.copy(control.getPositionDelta(dt).multiplyScalar(1000 / dt));
-            this.el.setAttribute('velocity', velocity);
-            return;
-          } else {
-            throw new Error('Incompatible movement controls: ', data.movementControls[i]);
-          }
-          break;
-        }
-      }
-    }
+    @author mark lundin / http://mark-lundin.com
 
-    velocity.copy(this.el.getAttribute('velocity'));
-    velocity.x -= velocity.x * data.movementEasing * dt / 1000;
-    velocity.y -= velocity.y * data.movementEasingY * dt / 1000;
-    velocity.z -= velocity.z * data.movementEasing * dt / 1000;
+    This is a 3D implementation of the Quick Hull algorithm.
+    It is a fast way of computing a convex hull with average complexity
+    of O(n log(n)).
+    It uses depends on three.js and is supposed to create THREE.Geometry.
 
-    if (dVelocity && data.movementEnabled) {
-      // Set acceleration
-      if (dVelocity.length() > 1) {
-        dVelocity.setLength(this.data.movementAcceleration * dt / 1000);
-      } else {
-        dVelocity.multiplyScalar(this.data.movementAcceleration * dt / 1000);
-      }
+    It's also very messy
 
-      // Rotate to heading
-      var rotation = this.el.getAttribute('rotation');
-      if (rotation) {
-        this.heading.set(
-          data.fly ? THREE.Math.degToRad(rotation.x) : 0,
-          THREE.Math.degToRad(rotation.y),
-          0
-        );
-        dVelocity.applyEuler(this.heading);
-      }
+ */
+
+module.exports = (function(){
+
+
+  var faces     = [],
+    faceStack   = [],
+    i, NUM_POINTS, extremes,
+    max     = 0,
+    dcur, current, j, v0, v1, v2, v3,
+    N, D;
 
-      velocity.add(dVelocity);
+  var ab, ac, ax,
+    suba, subb, normal,
+    diff, subaA, subaB, subC;
 
-      // TODO - Several issues here:
-      // (1) Interferes w/ gravity.
-      // (2) Interferes w/ jumping.
-      // (3) Likely to interfere w/ relative position to moving platform.
-      // if (velocity.length() > data.movementSpeed) {
-      //   velocity.setLength(data.movementSpeed);
-      // }
-    }
+  function reset(){
+
+    ab    = new THREE.Vector3(),
+    ac    = new THREE.Vector3(),
+    ax    = new THREE.Vector3(),
+    suba  = new THREE.Vector3(),
+    subb  = new THREE.Vector3(),
+    normal  = new THREE.Vector3(),
+    diff  = new THREE.Vector3(),
+    subaA = new THREE.Vector3(),
+    subaB = new THREE.Vector3(),
+    subC  = new THREE.Vector3();
 
-    this.el.setAttribute('velocity', velocity);
   }
-};
 
-},{}],89:[function(require,module,exports){
-var LoopMode = {
-  once: THREE.LoopOnce,
-  repeat: THREE.LoopRepeat,
-  pingpong: THREE.LoopPingPong
-};
+  //temporary vectors
 
-/**
- * animation-mixer
- *
- * Player for animation clips. Intended to be compatible with any model format that supports
- * skeletal or morph animations through THREE.AnimationMixer.
- * See: https://threejs.org/docs/?q=animation#Reference/Animation/AnimationMixer
- */
-module.exports = {
-  schema: {
-    clip:  {default: '*'},
-    duration: {default: 0},
-    crossFadeDuration: {default: 0},
-    loop: {default: 'repeat', oneOf: Object.keys(LoopMode)},
-    repetitions: {default: Infinity, min: 0}
-  },
+  function process( points ){
 
-  init: function () {
-    /** @type {THREE.Mesh} */
-    this.model = null;
-    /** @type {THREE.AnimationMixer} */
-    this.mixer = null;
-    /** @type {Array<THREE.AnimationAction>} */
-    this.activeActions = [];
+    // Iterate through all the faces and remove
+    while( faceStack.length > 0  ){
+      cull( faceStack.shift(), points );
+    }
+  }
 
-    var model = this.el.getObject3D('mesh');
 
-    if (model) {
-      this.load(model);
-    } else {
-      this.el.addEventListener('model-loaded', function(e) {
-        this.load(e.detail.model);
-      }.bind(this));
-    }
-  },
+  var norm = function(){
 
-  load: function (model) {
-    var el = this.el;
-    this.model = model;
-    this.mixer = new THREE.AnimationMixer(model);
-    this.mixer.addEventListener('loop', function (e) {
-      el.emit('animation-loop', {action: e.action, loopDelta: e.loopDelta});
-    }.bind(this));
-    this.mixer.addEventListener('finished', function (e) {
-      el.emit('animation-finished', {action: e.action, direction: e.direction});
-    }.bind(this));
-    if (this.data.clip) this.update({});
-  },
+    var ca = new THREE.Vector3(),
+      ba = new THREE.Vector3(),
+      N = new THREE.Vector3();
 
-  remove: function () {
-    if (this.mixer) this.mixer.stopAllAction();
-  },
+    return function( a, b, c ){
 
-  update: function (previousData) {
-    if (!previousData) return;
+      ca.subVectors( c, a );
+      ba.subVectors( b, a );
 
-    this.stopAction();
+      N.crossVectors( ca, ba );
 
-    if (this.data.clip) {
-      this.playAction();
+      return N.normalize();
     }
-  },
 
-  stopAction: function () {
-    var data = this.data;
-    for (var i = 0; i < this.activeActions.length; i++) {
-      data.crossFadeDuration
-        ? this.activeActions[i].fadeOut(data.crossFadeDuration)
-        : this.activeActions[i].stop();
-    }
-    this.activeActions.length = 0;
-  },
+  }();
 
-  playAction: function () {
-    if (!this.mixer) return;
 
-    var model = this.model,
-        data = this.data,
-        clips = model.animations || (model.geometry || {}).animations || [];
+  function getNormal( face, points ){
 
-    if (!clips.length) return;
+    if( face.normal !== undefined ) return face.normal;
 
-    var re = wildcardToRegExp(data.clip);
+    var p0 = points[face[0]],
+      p1 = points[face[1]],
+      p2 = points[face[2]];
 
-    for (var clip, i = 0; (clip = clips[i]); i++) {
-      if (clip.name.match(re)) {
-        var action = this.mixer.clipAction(clip, model);
-        action.enabled = true;
-        if (data.duration) action.setDuration(data.duration);
-        action
-          .setLoop(LoopMode[data.loop], data.repetitions)
-          .fadeIn(data.crossFadeDuration)
-          .play();
-        this.activeActions.push(action);
-      }
-    }
-  },
+    ab.subVectors( p1, p0 );
+    ac.subVectors( p2, p0 );
+    normal.crossVectors( ac, ab );
+    normal.normalize();
+
+    return face.normal = normal.clone();
 
-  tick: function (t, dt) {
-    if (this.mixer && !isNaN(dt)) this.mixer.update(dt / 1000);
   }
-};
 
-/**
- * Creates a RegExp from the given string, converting asterisks to .* expressions,
- * and escaping all other characters.
- */
-function wildcardToRegExp (s) {
-  return new RegExp('^' + s.split(/\*+/).map(regExpEscape).join('.*') + '$');
-}
 
-/**
- * RegExp-escapes all characters in the given string.
- */
-function regExpEscape (s) {
-  return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
-}
+  function assignPoints( face, pointset, points ){
 
-},{}],90:[function(require,module,exports){
-THREE.FBXLoader = require('../../lib/FBXLoader');
+    // ASSIGNING POINTS TO FACE
+    var p0 = points[face[0]],
+      dots = [], apex,
+      norm = getNormal( face, points );
 
-/**
- * fbx-model
- *
- * Loader for FBX format. Supports ASCII, but *not* binary, models.
- */
-module.exports = {
-  schema: {
-    src:         { type: 'asset' },
-    crossorigin: { default: '' }
-  },
 
-  init: function () {
-    this.model = null;
-  },
+    // Sory all the points by there distance from the plane
+    pointset.sort( function( aItem, bItem ){
 
-  update: function () {
-    var loader,
-        data = this.data;
-    if (!data.src) return;
 
-    this.remove();
-    loader = new THREE.FBXLoader();
-    if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
-    loader.load(data.src, this.load.bind(this));
-  },
+      dots[aItem.x/3] = dots[aItem.x/3] !== undefined ? dots[aItem.x/3] : norm.dot( suba.subVectors( aItem, p0 ));
+      dots[bItem.x/3] = dots[bItem.x/3] !== undefined ? dots[bItem.x/3] : norm.dot( subb.subVectors( bItem, p0 ));
 
-  load: function (model) {
-    this.model = model;
-    this.el.setObject3D('mesh', model);
-    this.el.emit('model-loaded', {format: 'fbx', model: model});
-  },
+      return dots[aItem.x/3] - dots[bItem.x/3] ;
+    });
 
-  remove: function () {
-    if (this.model) this.el.removeObject3D('mesh');
+    //TODO :: Must be a faster way of finding and index in this array
+    var index = pointset.length;
+
+    if( index === 1 ) dots[pointset[0].x/3] = norm.dot( suba.subVectors( pointset[0], p0 ));
+    while( index-- > 0 && dots[pointset[index].x/3] > 0 )
+
+    var point;
+    if( index + 1 < pointset.length && dots[pointset[index+1].x/3] > 0 ){
+
+      face.visiblePoints  = pointset.splice( index + 1 );
+    }
   }
-};
 
-},{"../../lib/FBXLoader":3}],91:[function(require,module,exports){
-var fetchScript = require('../../lib/fetch-script')();
 
-var LOADER_SRC = 'https://rawgit.com/mrdoob/three.js/r86/examples/js/loaders/GLTFLoader.js';
 
-/**
- * Legacy loader for glTF 1.0 models.
- * Asynchronously loads THREE.GLTFLoader from rawgit.
- */
-module.exports.Component = {
-  schema: {type: 'model'},
 
-  init: function () {
-    this.model = null;
-    this.loader = null;
-    this.loaderPromise = loadLoader().then(function () {
-      this.loader = new THREE.GLTFLoader();
-      this.loader.setCrossOrigin('Anonymous');
-    }.bind(this));
-  },
+  function cull( face, points ){
+
+    var i = faces.length,
+      dot, visibleFace, currentFace,
+      visibleFaces = [face];
+
+    var apex = points.indexOf( face.visiblePoints.pop() );
+
+    // Iterate through all other faces...
+    while( i-- > 0 ){
+      currentFace = faces[i];
+      if( currentFace !== face ){
+        // ...and check if they're pointing in the same direction
+        dot = getNormal( currentFace, points ).dot( diff.subVectors( points[apex], points[currentFace[0]] ));
+        if( dot > 0 ){
+          visibleFaces.push( currentFace );
+        }
+      }
+    }
+
+    var index, neighbouringIndex, vertex;
+
+    // Determine Perimeter - Creates a bounded horizon
+
+    // 1. Pick an edge A out of all possible edges
+    // 2. Check if A is shared by any other face. a->b === b->a
+      // 2.1 for each edge in each triangle, isShared = ( f1.a == f2.a && f1.b == f2.b ) || ( f1.a == f2.b && f1.b == f2.a )
+    // 3. If not shared, then add to convex horizon set,
+        //pick an end point (N) of the current edge A and choose a new edge NA connected to A.
+        //Restart from 1.
+    // 4. If A is shared, it is not an horizon edge, therefore flag both faces that share this edge as candidates for culling
+    // 5. If candidate geometry is a degenrate triangle (ie. the tangent space normal cannot be computed) then remove that triangle from all further processing
 
-  update: function () {
-    var self = this;
-    var el = this.el;
-    var src = this.data;
 
-    if (!src) { return; }
+    var j = i = visibleFaces.length;
+    var isDistinct = false,
+      hasOneVisibleFace = i === 1,
+      cull = [],
+      perimeter = [],
+      edgeIndex = 0, compareFace, nextIndex,
+      a, b;
 
-    this.remove();
+    var allPoints = [];
+    var originFace = [visibleFaces[0][0], visibleFaces[0][1], visibleFaces[0][1], visibleFaces[0][2], visibleFaces[0][2], visibleFaces[0][0]];
 
-    this.loaderPromise.then(function () {
-      this.loader.load(src, function gltfLoaded (gltfModel) {
-        self.model = gltfModel.scene;
-        self.model.animations = gltfModel.animations;
-        self.system.registerModel(self.model);
-        el.setObject3D('mesh', self.model);
-        el.emit('model-loaded', {format: 'gltf', model: self.model});
-      });
-    }.bind(this));
-  },
 
-  remove: function () {
-    if (!this.model) { return; }
-    this.el.removeObject3D('mesh');
-    this.system.unregisterModel(this.model);
-  }
-};
+    if( visibleFaces.length === 1 ){
+      currentFace = visibleFaces[0];
 
-/**
- * glTF model system.
- */
-module.exports.System = {
-  init: function () {
-    this.models = [];
-  },
+      perimeter = [currentFace[0], currentFace[1], currentFace[1], currentFace[2], currentFace[2], currentFace[0]];
+      // remove visible face from list of faces
+      if( faceStack.indexOf( currentFace ) > -1 ){
+        faceStack.splice( faceStack.indexOf( currentFace ), 1 );
+      }
 
-  /**
-   * Updates shaders for all glTF models in the system.
-   */
-  tick: function () {
-    var sceneEl = this.sceneEl;
-    if (sceneEl.hasLoaded && this.models.length) {
-      THREE.GLTFLoader.Shaders.update(sceneEl.object3D, sceneEl.camera);
-    }
-  },
 
-  /**
-   * Registers a glTF asset.
-   * @param {object} gltf Asset containing a scene and (optional) animations and cameras.
-   */
-  registerModel: function (gltf) {
-    this.models.push(gltf);
-  },
+      if( currentFace.visiblePoints ) allPoints = allPoints.concat( currentFace.visiblePoints );
+      faces.splice( faces.indexOf( currentFace ), 1 );
 
-  /**
-   * Unregisters a glTF asset.
-   * @param  {object} gltf Asset containing a scene and (optional) animations and cameras.
-   */
-  unregisterModel: function (gltf) {
-    var models = this.models;
-    var index = models.indexOf(gltf);
-    if (index >= 0) {
-      models.splice(index, 1);
-    }
-  }
-};
+    }else{
 
-var loadLoader = (function () {
-  var promise;
-  return function () {
-    promise = promise || fetchScript(LOADER_SRC);
-    return promise;
-  };
-}());
+      while( i-- > 0  ){  // for each visible face
 
-},{"../../lib/fetch-script":8}],92:[function(require,module,exports){
-var fetchScript = require('../../lib/fetch-script')();
+        currentFace = visibleFaces[i];
 
-var LOADER_SRC = 'https://rawgit.com/mrdoob/three.js/r87/examples/js/loaders/GLTFLoader.js';
-// Monkeypatch while waiting for three.js r86.
-if (THREE.PropertyBinding.sanitizeNodeName === undefined) {
+        // remove visible face from list of faces
+        if( faceStack.indexOf( currentFace ) > -1 ){
+          faceStack.splice( faceStack.indexOf( currentFace ), 1 );
+        }
 
-  THREE.PropertyBinding.sanitizeNodeName = function (s) {
-    return s.replace( /\s/g, '_' ).replace( /[^\w-]/g, '' );
-  };
+        if( currentFace.visiblePoints ) allPoints = allPoints.concat( currentFace.visiblePoints );
+        faces.splice( faces.indexOf( currentFace ), 1 );
 
-}
 
-/**
- * Upcoming loader for glTF 2.0 models.
- * Asynchronously loads THREE.GLTF2Loader from rawgit.
- */
-module.exports = {
-  schema: {type: 'model'},
+        var isSharedEdge;
+        cEdgeIndex = 0;
 
-  init: function () {
-    this.model = null;
-    this.loader = null;
-    this.loaderPromise = loadLoader().then(function () {
-      this.loader = new THREE.GLTFLoader();
-      this.loader.setCrossOrigin('Anonymous');
-    }.bind(this));
-  },
+        while( cEdgeIndex < 3 ){ // Iterate through it's edges
 
-  update: function () {
-    var self = this;
-    var el = this.el;
-    var src = this.data;
+          isSharedEdge = false;
+          j = visibleFaces.length;
+          a = currentFace[cEdgeIndex]
+          b = currentFace[(cEdgeIndex+1)%3];
 
-    if (!src) { return; }
 
-    this.remove();
+          while( j-- > 0 && !isSharedEdge ){ // find another visible faces
 
-    this.loaderPromise.then(function () {
-      this.loader.load(src, function gltfLoaded (gltfModel) {
-        self.model = gltfModel.scene;
-        self.model.animations = gltfModel.animations;
-        el.setObject3D('mesh', self.model);
-        el.emit('model-loaded', {format: 'gltf', model: self.model});
-      });
-    }.bind(this));
-  },
+            compareFace = visibleFaces[j];
+            edgeIndex = 0;
 
-  remove: function () {
-    if (!this.model) { return; }
-    this.el.removeObject3D('mesh');
-  }
-};
+            // isSharedEdge = compareFace == currentFace;
+            if( compareFace !== currentFace ){
 
-var loadLoader = (function () {
-  var promise;
-  return function () {
-    promise = promise || fetchScript(LOADER_SRC);
-    return promise;
-  };
-}());
+              while( edgeIndex < 3 && !isSharedEdge ){ //Check all it's indices
 
-},{"../../lib/fetch-script":8}],93:[function(require,module,exports){
-module.exports = {
-  'animation-mixer': require('./animation-mixer'),
-  'fbx-model': require('./fbx-model'),
-  'gltf-model-next': require('./gltf-model-next'),
-  'gltf-model-legacy': require('./gltf-model-legacy'),
-  'json-model': require('./json-model'),
-  'object-model': require('./object-model'),
-  'ply-model': require('./ply-model'),
-  'three-model': require('./three-model'),
+                nextIndex = ( edgeIndex + 1 );
+                isSharedEdge = ( compareFace[edgeIndex] === a && compareFace[nextIndex%3] === b ) ||
+                         ( compareFace[edgeIndex] === b && compareFace[nextIndex%3] === a );
 
-  registerAll: function (AFRAME) {
-    if (this._registered) return;
+                edgeIndex++;
+              }
+            }
+          }
 
-    AFRAME = AFRAME || window.AFRAME;
+          if( !isSharedEdge || hasOneVisibleFace ){
+            perimeter.push( a );
+            perimeter.push( b );
+          }
 
-    // THREE.AnimationMixer
-    if (!AFRAME.components['animation-mixer']) {
-      AFRAME.registerComponent('animation-mixer', this['animation-mixer']);
+          cEdgeIndex++;
+        }
+      }
     }
 
-    // THREE.PlyLoader
-    if (!AFRAME.systems['ply-model']) {
-      AFRAME.registerSystem('ply-model', this['ply-model'].System);
-    }
-    if (!AFRAME.components['ply-model']) {
-      AFRAME.registerComponent('ply-model', this['ply-model'].Component);
-    }
+    // create new face for all pairs around edge
+    i = 0;
+    var l = perimeter.length/2;
+    var f;
 
-    // THREE.FBXLoader
-    if (!AFRAME.components['fbx-model']) {
-      AFRAME.registerComponent('fbx-model', this['fbx-model']);
+    while( i < l ){
+      f = [ perimeter[i*2+1], apex, perimeter[i*2] ];
+      assignPoints( f, allPoints, points );
+      faces.push( f )
+      if( f.visiblePoints !== undefined  )faceStack.push( f );
+      i++;
     }
 
-    // THREE.GLTF2Loader
-    if (!AFRAME.components['gltf-model-next']) {
-      AFRAME.registerComponent('gltf-model-next', this['gltf-model-next']);
-    }
+  }
 
-    // THREE.GLTFLoader
-    if (!AFRAME.components['gltf-model-legacy']) {
-      AFRAME.registerComponent('gltf-model-legacy', this['gltf-model-legacy'].Component);
-      AFRAME.registerSystem('gltf-model-legacy', this['gltf-model-legacy'].System);
-    }
+  var distSqPointSegment = function(){
 
-    // THREE.JsonLoader
-    if (!AFRAME.components['json-model']) {
-      AFRAME.registerComponent('json-model', this['json-model']);
-    }
+    var ab = new THREE.Vector3(),
+      ac = new THREE.Vector3(),
+      bc = new THREE.Vector3();
 
-    // THREE.ObjectLoader
-    if (!AFRAME.components['object-model']) {
-      AFRAME.registerComponent('object-model', this['object-model']);
-    }
+    return function( a, b, c ){
 
-    // (deprecated) THREE.JsonLoader and THREE.ObjectLoader
-    if (!AFRAME.components['three-model']) {
-      AFRAME.registerComponent('three-model', this['three-model']);
-    }
+        ab.subVectors( b, a );
+        ac.subVectors( c, a );
+        bc.subVectors( c, b );
 
-    this._registered = true;
-  }
-};
+        var e = ac.dot(ab);
+        if (e < 0.0) return ac.dot( ac );
+        var f = ab.dot( ab );
+        if (e >= f) return bc.dot(  bc );
+        return ac.dot( ac ) - e * e / f;
 
-},{"./animation-mixer":89,"./fbx-model":90,"./gltf-model-legacy":91,"./gltf-model-next":92,"./json-model":94,"./object-model":95,"./ply-model":96,"./three-model":97}],94:[function(require,module,exports){
-/**
- * json-model
- *
- * Loader for THREE.js JSON format. Somewhat confusingly, there are two different THREE.js formats,
- * both having the .json extension. This loader supports only THREE.JsonLoader, which typically
- * includes only a single mesh.
- *
- * Check the console for errors, if in doubt. You may need to use `object-model` or
- * `blend-character-model` for some .js and .json files.
- *
- * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
- */
-module.exports = {
-  schema: {
-    src:         { type: 'asset' },
-    crossorigin: { default: '' }
-  },
+      }
 
-  init: function () {
-    this.model = null;
-  },
+  }();
 
-  update: function () {
-    var loader,
-        data = this.data;
-    if (!data.src) return;
 
-    this.remove();
-    loader = new THREE.JSONLoader();
-    if (data.crossorigin) loader.crossOrigin = data.crossorigin;
-    loader.load(data.src, function (geometry, materials) {
 
-      // Attempt to automatically detect common material options.
-      materials.forEach(function (mat) {
-        mat.vertexColors = (geometry.faces[0] || {}).color ? THREE.FaceColors : THREE.NoColors;
-        mat.skinning = !!(geometry.bones || []).length;
-        mat.morphTargets = !!(geometry.morphTargets || []).length;
-        mat.morphNormals = !!(geometry.morphNormals || []).length;
-      });
 
-      var model = (geometry.bones || []).length
-        ? new THREE.SkinnedMesh(geometry, new THREE.MultiMaterial(materials))
-        : new THREE.Mesh(geometry, new THREE.MultiMaterial(materials));
 
-      this.load(model);
-    }.bind(this));
-  },
+  return function( geometry ){
+
+    reset();
+
+
+    points    = geometry.vertices;
+    faces     = [],
+    faceStack   = [],
+    i       = NUM_POINTS = points.length,
+    extremes  = points.slice( 0, 6 ),
+    max     = 0;
+
+
 
-  load: function (model) {
-    this.model = model;
-    this.el.setObject3D('mesh', model);
-    this.el.emit('model-loaded', {format: 'json', model: model});
-  },
+    /*
+     *  FIND EXTREMETIES
+     */
+    while( i-- > 0 ){
+      if( points[i].x < extremes[0].x ) extremes[0] = points[i];
+      if( points[i].x > extremes[1].x ) extremes[1] = points[i];
 
-  remove: function () {
-    if (this.model) this.el.removeObject3D('mesh');
-  }
-};
+      if( points[i].y < extremes[2].y ) extremes[2] = points[i];
+      if( points[i].y < extremes[3].y ) extremes[3] = points[i];
 
-},{}],95:[function(require,module,exports){
-/**
- * object-model
- *
- * Loader for THREE.js JSON format. Somewhat confusingly, there are two different THREE.js formats,
- * both having the .json extension. This loader supports only THREE.ObjectLoader, which typically
- * includes multiple meshes or an entire scene.
- *
- * Check the console for errors, if in doubt. You may need to use `json-model` or
- * `blend-character-model` for some .js and .json files.
- *
- * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
- */
-module.exports = {
-  schema: {
-    src:         { type: 'asset' },
-    crossorigin: { default: '' }
-  },
+      if( points[i].z < extremes[4].z ) extremes[4] = points[i];
+      if( points[i].z < extremes[5].z ) extremes[5] = points[i];
+    }
 
-  init: function () {
-    this.model = null;
-  },
 
-  update: function () {
-    var loader,
-        data = this.data;
-    if (!data.src) return;
+    /*
+     *  Find the longest line between the extremeties
+     */
 
-    this.remove();
-    loader = new THREE.ObjectLoader();
-    if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
-    loader.load(data.src, function(object) {
+    j = i = 6;
+    while( i-- > 0 ){
+      j = i - 1;
+      while( j-- > 0 ){
+          if( max < (dcur = extremes[i].distanceToSquared( extremes[j] )) ){
+        max = dcur;
+        v0 = extremes[ i ];
+        v1 = extremes[ j ];
 
-      // Enable skinning, if applicable.
-      object.traverse(function(o) {
-        if (o instanceof THREE.SkinnedMesh && o.material) {
-          o.material.skinning = !!((o.geometry && o.geometry.bones) || []).length;
+          }
         }
-      });
+      }
 
-      this.load(object);
-    }.bind(this));
-  },
 
-  load: function (model) {
-    this.model = model;
-    this.el.setObject3D('mesh', model);
-    this.el.emit('model-loaded', {format: 'json', model: model});
-  },
+      // 3. Find the most distant point to the line segment, this creates a plane
+      i = 6;
+      max = 0;
+    while( i-- > 0 ){
+      dcur = distSqPointSegment( v0, v1, extremes[i]);
+      if( max < dcur ){
+        max = dcur;
+            v2 = extremes[ i ];
+          }
+    }
 
-  remove: function () {
-    if (this.model) this.el.removeObject3D('mesh');
-  }
-};
 
-},{}],96:[function(require,module,exports){
-/**
- * ply-model
- *
- * Wraps THREE.PLYLoader.
- */
-THREE.PLYLoader = require('../../lib/PLYLoader');
+      // 4. Find the most distant point to the plane.
 
-/**
- * Loads, caches, resolves geometries.
- *
- * @member cache - Promises that resolve geometries keyed by `src`.
- */
-module.exports.System = {
-  init: function () {
-    this.cache = {};
-  },
+      N = norm(v0, v1, v2);
+      D = N.dot( v0 );
 
-  /**
-   * @returns {Promise}
-   */
-  getOrLoadGeometry: function (src, skipCache) {
-    var cache = this.cache;
-    var cacheItem = cache[src];
 
-    if (!skipCache && cacheItem) {
-      return cacheItem;
-    }
+      max = 0;
+      i = NUM_POINTS;
+      while( i-- > 0 ){
+        dcur = Math.abs( points[i].dot( N ) - D );
+          if( max < dcur ){
+            max = dcur;
+            v3 = points[i];
+      }
+      }
 
-    cache[src] = new Promise(function (resolve) {
-      var loader = new THREE.PLYLoader();
-      loader.load(src, function (geometry) {
-        resolve(geometry);
-      });
-    });
-    return cache[src];
-  },
-};
 
-module.exports.Component = {
-  schema: {
-    skipCache: {type: 'boolean', default: false},
-    src: {type: 'asset'}
-  },
 
-  init: function () {
-    this.model = null;
-  },
+      var v0Index = points.indexOf( v0 ),
+      v1Index = points.indexOf( v1 ),
+      v2Index = points.indexOf( v2 ),
+      v3Index = points.indexOf( v3 );
 
-  update: function () {
-    var data = this.data;
-    var el = this.el;
-    var loader;
 
-    if (!data.src) {
-      console.warn('[%s] `src` property is required.', this.name);
-      return;
-    }
+    //  We now have a tetrahedron as the base geometry.
+    //  Now we must subdivide the
 
-    // Get geometry from system, create and set mesh.
-    this.system.getOrLoadGeometry(data.src, data.skipCache).then(function (geometry) {
-      var model = createModel(geometry);
-      el.setObject3D('mesh', model);
-      el.emit('model-loaded', {format: 'ply', model: model});
-    });
-  },
+      var tetrahedron =[
+        [ v2Index, v1Index, v0Index ],
+        [ v1Index, v3Index, v0Index ],
+        [ v2Index, v3Index, v1Index ],
+        [ v0Index, v3Index, v2Index ],
+    ];
 
-  remove: function () {
-    if (this.model) { this.el.removeObject3D('mesh'); }
-  }
-};
 
-function createModel (geometry) {
-  return new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({
-    color: 0xFFFFFF,
-    shading: THREE.FlatShading,
-    vertexColors: THREE.VertexColors,
-    shininess: 0
-  }));
-}
 
-},{"../../lib/PLYLoader":6}],97:[function(require,module,exports){
-var DEFAULT_ANIMATION = '__auto__';
+    subaA.subVectors( v1, v0 ).normalize();
+    subaB.subVectors( v2, v0 ).normalize();
+    subC.subVectors ( v3, v0 ).normalize();
+    var sign  = subC.dot( new THREE.Vector3().crossVectors( subaB, subaA ));
 
-/**
- * three-model
- *
- * Loader for THREE.js JSON format. Somewhat confusingly, there are two
- * different THREE.js formats, both having the .json extension. This loader
- * supports both, but requires you to specify the mode as "object" or "json".
- *
- * Typically, you will use "json" for a single mesh, and "object" for a scene
- * or multiple meshes. Check the console for errors, if in doubt.
- *
- * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
- */
-module.exports = {
-  deprecated: true,
 
-  schema: {
-    src:               { type: 'asset' },
-    loader:            { default: 'object', oneOf: ['object', 'json'] },
-    enableAnimation:   { default: true },
-    animation:         { default: DEFAULT_ANIMATION },
-    animationDuration: { default: 0 },
-    crossorigin:       { default: '' }
-  },
+    // Reverse the winding if negative sign
+    if( sign < 0 ){
+      tetrahedron[0].reverse();
+      tetrahedron[1].reverse();
+      tetrahedron[2].reverse();
+      tetrahedron[3].reverse();
+    }
 
-  init: function () {
-    this.model = null;
-    this.mixer = null;
-    console.warn('[three-model] Component is deprecated. Use json-model or object-model instead.');
-  },
 
-  update: function (previousData) {
-    previousData = previousData || {};
+    //One for each face of the pyramid
+    var pointsCloned = points.slice();
+    pointsCloned.splice( pointsCloned.indexOf( v0 ), 1 );
+    pointsCloned.splice( pointsCloned.indexOf( v1 ), 1 );
+    pointsCloned.splice( pointsCloned.indexOf( v2 ), 1 );
+    pointsCloned.splice( pointsCloned.indexOf( v3 ), 1 );
 
-    var loader,
-        data = this.data;
 
-    if (!data.src) {
-      this.remove();
-      return;
+    var i = tetrahedron.length;
+    while( i-- > 0 ){
+      assignPoints( tetrahedron[i], pointsCloned, points );
+      if( tetrahedron[i].visiblePoints !== undefined ){
+        faceStack.push( tetrahedron[i] );
+      }
+      faces.push( tetrahedron[i] );
     }
 
-    // First load.
-    if (!Object.keys(previousData).length) {
-      this.remove();
-      if (data.loader === 'object') {
-        loader = new THREE.ObjectLoader();
-        if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
-        loader.load(data.src, function(loaded) {
-          loaded.traverse( function(object) {
-            if (object instanceof THREE.SkinnedMesh)
-              loaded = object;
-          });
-          if(loaded.material)
-            loaded.material.skinning = !!((loaded.geometry && loaded.geometry.bones) || []).length;
-          this.load(loaded);
-        }.bind(this));
-      } else if (data.loader === 'json') {
-        loader = new THREE.JSONLoader();
-        if (data.crossorigin) loader.crossOrigin = data.crossorigin;
-        loader.load(data.src, function (geometry, materials) {
-
-          // Attempt to automatically detect common material options.
-          materials.forEach(function (mat) {
-            mat.vertexColors = (geometry.faces[0] || {}).color ? THREE.FaceColors : THREE.NoColors;
-            mat.skinning = !!(geometry.bones || []).length;
-            mat.morphTargets = !!(geometry.morphTargets || []).length;
-            mat.morphNormals = !!(geometry.morphNormals || []).length;
-          });
+    process( points );
 
-          var mesh = (geometry.bones || []).length
-            ? new THREE.SkinnedMesh(geometry, new THREE.MultiMaterial(materials))
-            : new THREE.Mesh(geometry, new THREE.MultiMaterial(materials));
 
-          this.load(mesh);
-        }.bind(this));
-      } else {
-        throw new Error('[three-model] Invalid mode "%s".', data.mode);
-      }
-      return;
+    //  Assign to our geometry object
+
+    var ll = faces.length;
+    while( ll-- > 0 ){
+      geometry.faces[ll] = new THREE.Face3( faces[ll][2], faces[ll][1], faces[ll][0], faces[ll].normal )
     }
 
-    var activeAction = this.model && this.model.activeAction;
+    geometry.normalsNeedUpdate = true;
 
-    if (data.animation !== previousData.animation) {
-      if (activeAction) activeAction.stop();
-      this.playAnimation();
-      return;
-    }
+    return geometry;
+
+  }
+
+}())
+
+},{}],86:[function(require,module,exports){
+var EPS = 0.1;
+
+module.exports = {
+  schema: {
+    enabled: {default: true},
+    mode: {default: 'teleport', oneOf: ['teleport', 'animate']},
+    animateSpeed: {default: 3.0}
+  },
 
-    if (activeAction && data.enableAnimation !== activeAction.isRunning()) {
-      data.enableAnimation ? this.playAnimation() : activeAction.stop();
-    }
+  init: function () {
+    this.active = true;
+    this.checkpoint = null;
 
-    if (activeAction && data.animationDuration) {
-        activeAction.setDuration(data.animationDuration);
-    }
+    this.offset = new THREE.Vector3();
+    this.position = new THREE.Vector3();
+    this.targetPosition = new THREE.Vector3();
   },
 
-  load: function (model) {
-    this.model = model;
-    this.mixer = new THREE.AnimationMixer(this.model);
-    this.el.setObject3D('mesh', model);
-    this.el.emit('model-loaded', {format: 'three', model: model});
+  play: function () { this.active = true; },
+  pause: function () { this.active = false; },
 
-    if (this.data.enableAnimation) this.playAnimation();
-  },
+  setCheckpoint: function (checkpoint) {
+    var el = this.el;
 
-  playAnimation: function () {
-    var clip,
-        data = this.data,
-        animations = this.model.animations || this.model.geometry.animations || [];
+    if (!this.active) return;
+    if (this.checkpoint === checkpoint) return;
 
-    if (!data.enableAnimation || !data.animation || !animations.length) {
-      return;
+    if (this.checkpoint) {
+      el.emit('navigation-end', {checkpoint: this.checkpoint});
     }
 
-    clip = data.animation === DEFAULT_ANIMATION
-      ? animations[0]
-      : THREE.AnimationClip.findByName(animations, data.animation);
+    this.checkpoint = checkpoint;
+    this.sync();
 
-    if (!clip) {
-      console.error('[three-model] Animation "%s" not found.', data.animation);
+    // Ignore new checkpoint if we're already there.
+    if (this.position.distanceTo(this.targetPosition) < EPS) {
+      this.checkpoint = null;
       return;
     }
 
-    this.model.activeAction = this.mixer.clipAction(clip, this.model);
-    if (data.animationDuration) {
-      this.model.activeAction.setDuration(data.animationDuration);
-    }
-    this.model.activeAction.play();
-  },
-
-  remove: function () {
-    if (this.mixer) this.mixer.stopAllAction();
-    if (this.model) this.el.removeObject3D('mesh');
-  },
+    el.emit('navigation-start', {checkpoint: checkpoint});
 
-  tick: function (t, dt) {
-    if (this.mixer && !isNaN(dt)) {
-      this.mixer.update(dt / 1000);
+    if (this.data.mode === 'teleport') {
+      this.el.setAttribute('position', this.targetPosition);
+      this.checkpoint = null;
+      el.emit('navigation-end', {checkpoint: checkpoint});
     }
-  }
-};
-
-},{}],98:[function(require,module,exports){
-module.exports = {
-  schema: {
-    offset: {default: {x: 0, y: 0, z: 0}, type: 'vec3'}
   },
 
-  init: function () {
-    this.active = false;
-    this.targetEl = null;
-    this.fire = this.fire.bind(this);
-    this.offset = new THREE.Vector3();
+  isVelocityActive: function () {
+    return !!(this.active && this.checkpoint);
   },
 
-  update: function () {
-    this.offset.copy(this.data.offset);
-  },
+  getVelocity: function () {
+    if (!this.active) return;
 
-  play: function () { this.el.addEventListener('click', this.fire); },
-  pause: function () { this.el.removeEventListener('click', this.fire); },
-  remove: function () { this.pause(); },
+    var data = this.data,
+        offset = this.offset,
+        position = this.position,
+        targetPosition = this.targetPosition,
+        checkpoint = this.checkpoint;
 
-  fire: function () {
-    var targetEl = this.el.sceneEl.querySelector('[checkpoint-controls]');
-    if (!targetEl) {
-      throw new Error('No `checkpoint-controls` component found.');
+    this.sync();
+    if (position.distanceTo(targetPosition) < EPS) {
+      this.checkpoint = null;
+      this.el.emit('navigation-end', {checkpoint: checkpoint});
+      return offset.set(0, 0, 0);
     }
-    targetEl.components['checkpoint-controls'].setCheckpoint(this.el);
+    offset.setLength(data.animateSpeed);
+    return offset;
   },
 
-  getOffset: function () {
-    return this.offset.copy(this.data.offset);
+  sync: function () {
+    var offset = this.offset,
+        position = this.position,
+        targetPosition = this.targetPosition;
+
+    position.copy(this.el.getAttribute('position'));
+    targetPosition.copy(this.checkpoint.object3D.getWorldPosition());
+    targetPosition.add(this.checkpoint.components.checkpoint.getOffset());
+    offset.copy(targetPosition).sub(position);
   }
 };
 
-},{}],99:[function(require,module,exports){
+},{}],87:[function(require,module,exports){
 /**
- * Specifies an envMap on an entity, without replacing any existing material
- * properties.
+ * Gamepad controls for A-Frame.
+ *
+ * Stripped-down version of: https://github.com/donmccurdy/aframe-gamepad-controls
+ *
+ * For more information about the Gamepad API, see:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
  */
+
+var GamepadButton = require('../../lib/GamepadButton'),
+    GamepadButtonEvent = require('../../lib/GamepadButtonEvent');
+
+var JOYSTICK_EPS = 0.2;
+
 module.exports = {
-  schema: {
-    path: {default: ''},
-    extension: {default: 'jpg'},
-    format: {default: 'RGBFormat'},
-    enableBackground: {default: false}
-  },
 
-  init: function () {
-    var data = this.data;
+  /*******************************************************************
+   * Statics
+   */
 
-    this.texture = new THREE.CubeTextureLoader().load([
-      data.path + 'posx.' + data.extension, data.path + 'negx.' + data.extension,
-      data.path + 'posy.' + data.extension, data.path + 'negy.' + data.extension,
-      data.path + 'posz.' + data.extension, data.path + 'negz.' + data.extension
-    ]);
-    this.texture.format = THREE[data.format];
+  GamepadButton: GamepadButton,
 
-    if (data.enableBackground) {
-      this.el.sceneEl.object3D.background = this.texture;
-    }
+  /*******************************************************************
+   * Schema
+   */
 
-    this.applyEnvMap();
-    this.el.addEventListener('object3dset', this.applyEnvMap.bind(this));
-  },
+  schema: {
+    // Controller 0-3
+    controller:        { default: 0, oneOf: [0, 1, 2, 3] },
 
-  applyEnvMap: function () {
-    var mesh = this.el.getObject3D('mesh');
-    var envMap = this.texture;
+    // Enable/disable features
+    enabled:           { default: true },
 
-    if (!mesh) return;
+    // Debugging
+    debug:             { default: false }
+  },
 
-    mesh.traverse(function (node) {
-      if (node.material && 'envMap' in node.material) {
-        node.material.envMap = envMap;
-        node.material.needsUpdate = true;
-      }
-    });
-  }
-};
+  /*******************************************************************
+   * Core
+   */
 
-},{}],100:[function(require,module,exports){
-/**
- * Based on aframe/examples/showcase/tracked-controls.
- *
- * Handles events coming from the hand-controls.
- * Determines if the entity is grabbed or released.
- * Updates its position to move along the controller.
- */
-module.exports = {
+  /**
+   * Called once when component is attached. Generally for initial setup.
+   */
   init: function () {
-    this.GRABBED_STATE = 'grabbed';
-
-    this.grabbing = false;
-    this.hitEl =      /** @type {AFRAME.Element}    */ null;
-    this.physics =    /** @type {AFRAME.System}     */ this.el.sceneEl.systems.physics;
-    this.constraint = /** @type {CANNON.Constraint} */ null;
+    var scene = this.el.sceneEl;
+    this.prevTime = window.performance.now();
 
-    // Bind event handlers
-    this.onHit = this.onHit.bind(this);
-    this.onGripOpen = this.onGripOpen.bind(this);
-    this.onGripClose = this.onGripClose.bind(this);
-  },
+    // Button state
+    this.buttons = {};
 
-  play: function () {
-    var el = this.el;
-    el.addEventListener('hit', this.onHit);
-    el.addEventListener('gripdown', this.onGripClose);
-    el.addEventListener('gripup', this.onGripOpen);
-    el.addEventListener('trackpaddown', this.onGripClose);
-    el.addEventListener('trackpadup', this.onGripOpen);
-    el.addEventListener('triggerdown', this.onGripClose);
-    el.addEventListener('triggerup', this.onGripOpen);
+    scene.addBehavior(this);
   },
 
-  pause: function () {
-    var el = this.el;
-    el.removeEventListener('hit', this.onHit);
-    el.removeEventListener('gripdown', this.onGripClose);
-    el.removeEventListener('gripup', this.onGripOpen);
-    el.removeEventListener('trackpaddown', this.onGripClose);
-    el.removeEventListener('trackpadup', this.onGripOpen);
-    el.removeEventListener('triggerdown', this.onGripClose);
-    el.removeEventListener('triggerup', this.onGripOpen);
-  },
+  /**
+   * Called when component is attached and when component data changes.
+   * Generally modifies the entity based on the data.
+   */
+  update: function () { this.tick(); },
 
-  onGripClose: function (evt) {
-    this.grabbing = true;
+  /**
+   * Called on each iteration of main render loop.
+   */
+  tick: function () {
+    this.updateButtonState();
   },
 
-  onGripOpen: function (evt) {
-    var hitEl = this.hitEl;
-    this.grabbing = false;
-    if (!hitEl) { return; }
-    hitEl.removeState(this.GRABBED_STATE);
-    this.hitEl = undefined;
-    this.physics.world.removeConstraint(this.constraint);
-    this.constraint = null;
-  },
+  /**
+   * Called when a component is removed (e.g., via removeAttribute).
+   * Generally undoes all modifications to the entity.
+   */
+  remove: function () { },
 
-  onHit: function (evt) {
-    var hitEl = evt.detail.el;
-    // If the element is already grabbed (it could be grabbed by another controller).
-    // If the hand is not grabbing the element does not stick.
-    // If we're already grabbing something you can't grab again.
-    if (!hitEl || hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; }
-    hitEl.addState(this.GRABBED_STATE);
-    this.hitEl = hitEl;
-    this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body);
-    this.physics.world.addConstraint(this.constraint);
-  }
-};
+  /*******************************************************************
+   * Universal controls - movement
+   */
 
-},{}],101:[function(require,module,exports){
-var physics = require('aframe-physics-system');
+  isVelocityActive: function () {
+    if (!this.data.enabled || !this.isConnected()) return false;
 
-module.exports = {
-  'checkpoint':      require('./checkpoint'),
-  'cube-env-map':    require('./cube-env-map'),
-  'grab':            require('./grab'),
-  'jump-ability':    require('./jump-ability'),
-  'kinematic-body':  require('./kinematic-body'),
-  'mesh-smooth':     require('./mesh-smooth'),
-  'sphere-collider': require('./sphere-collider'),
-  'toggle-velocity': require('./toggle-velocity'),
+    var dpad = this.getDpad(),
+        joystick0 = this.getJoystick(0),
+        inputX = dpad.x || joystick0.x,
+        inputY = dpad.y || joystick0.y;
 
-  registerAll: function (AFRAME) {
-    if (this._registered) return;
+    return Math.abs(inputX) > JOYSTICK_EPS || Math.abs(inputY) > JOYSTICK_EPS;
+  },
 
-    AFRAME = AFRAME || window.AFRAME;
+  getVelocityDelta: function () {
+    var dpad = this.getDpad(),
+        joystick0 = this.getJoystick(0),
+        inputX = dpad.x || joystick0.x,
+        inputY = dpad.y || joystick0.y,
+        dVelocity = new THREE.Vector3();
 
-    physics.registerAll();
-    if (!AFRAME.components['checkpoint'])      AFRAME.registerComponent('checkpoint',      this['checkpoint']);
-    if (!AFRAME.components['cube-env-map'])    AFRAME.registerComponent('cube-env-map',    this['cube-env-map']);
-    if (!AFRAME.components['grab'])            AFRAME.registerComponent('grab',            this['grab']);
-    if (!AFRAME.components['jump-ability'])    AFRAME.registerComponent('jump-ability',    this['jump-ability']);
-    if (!AFRAME.components['kinematic-body'])  AFRAME.registerComponent('kinematic-body',  this['kinematic-body']);
-    if (!AFRAME.components['mesh-smooth'])     AFRAME.registerComponent('mesh-smooth',     this['mesh-smooth']);
-    if (!AFRAME.components['sphere-collider']) AFRAME.registerComponent('sphere-collider', this['sphere-collider']);
-    if (!AFRAME.components['toggle-velocity']) AFRAME.registerComponent('toggle-velocity', this['toggle-velocity']);
+    if (Math.abs(inputX) > JOYSTICK_EPS) {
+      dVelocity.x += inputX;
+    }
+    if (Math.abs(inputY) > JOYSTICK_EPS) {
+      dVelocity.z += inputY;
+    }
 
-    this._registered = true;
-  }
-};
+    return dVelocity;
+  },
 
-},{"./checkpoint":98,"./cube-env-map":99,"./grab":100,"./jump-ability":102,"./kinematic-body":103,"./mesh-smooth":104,"./sphere-collider":105,"./toggle-velocity":106,"aframe-physics-system":11}],102:[function(require,module,exports){
-var ACCEL_G = -9.8, // m/s^2
-    EASING = -15; // m/s^2
+  /*******************************************************************
+   * Universal controls - rotation
+   */
 
-/**
- * Jump ability.
- */
-module.exports = {
-  dependencies: ['velocity'],
+  isRotationActive: function () {
+    if (!this.data.enabled || !this.isConnected()) return false;
 
-  /* Schema
-  ——————————————————————————————————————————————*/
+    var joystick1 = this.getJoystick(1);
 
-  schema: {
-    on: { default: 'keydown:Space gamepadbuttondown:0' },
-    playerHeight: { default: 1.764 },
-    maxJumps: { default: 1 },
-    distance: { default: 5 },
-    soundJump: { default: '' },
-    soundLand: { default: '' },
-    debug: { default: false }
+    return Math.abs(joystick1.x) > JOYSTICK_EPS || Math.abs(joystick1.y) > JOYSTICK_EPS;
   },
 
-  init: function () {
-    this.velocity = 0;
-    this.numJumps = 0;
-
-    var beginJump = this.beginJump.bind(this),
-        events = this.data.on.split(' ');
-    this.bindings = {};
-    for (var i = 0; i <  events.length; i++) {
-      this.bindings[events[i]] = beginJump;
-      this.el.addEventListener(events[i], beginJump);
-    }
-    this.bindings.collide = this.onCollide.bind(this);
-    this.el.addEventListener('collide', this.bindings.collide);
+  getRotationDelta: function () {
+    var lookVector = this.getJoystick(1);
+    if (Math.abs(lookVector.x) <= JOYSTICK_EPS) lookVector.x = 0;
+    if (Math.abs(lookVector.y) <= JOYSTICK_EPS) lookVector.y = 0;
+    return lookVector;
   },
 
-  remove: function () {
-    for (var event in this.bindings) {
-      if (this.bindings.hasOwnProperty(event)) {
-        this.el.removeEventListener(event, this.bindings[event]);
-        delete this.bindings[event];
+  /*******************************************************************
+   * Button events
+   */
+
+  updateButtonState: function () {
+    var gamepad = this.getGamepad();
+    if (this.data.enabled && gamepad) {
+
+      // Fire DOM events for button state changes.
+      for (var i = 0; i < gamepad.buttons.length; i++) {
+        if (gamepad.buttons[i].pressed && !this.buttons[i]) {
+          this.emit(new GamepadButtonEvent('gamepadbuttondown', i, gamepad.buttons[i]));
+        } else if (!gamepad.buttons[i].pressed && this.buttons[i]) {
+          this.emit(new GamepadButtonEvent('gamepadbuttonup', i, gamepad.buttons[i]));
+        }
+        this.buttons[i] = gamepad.buttons[i].pressed;
       }
-    }
-    this.el.removeEventListener('collide', this.bindings.collide);
-    delete this.bindings.collide;
-  },
 
-  beginJump: function () {
-    if (this.numJumps < this.data.maxJumps) {
-      var data = this.data,
-          initialVelocity = Math.sqrt(-2 * data.distance * (ACCEL_G + EASING)),
-          v = this.el.getAttribute('velocity');
-      this.el.setAttribute('velocity', {x: v.x, y: initialVelocity, z: v.z});
-      this.numJumps++;
+    } else if (Object.keys(this.buttons)) {
+      // Reset state if controls are disabled or controller is lost.
+      this.buttons = {};
     }
   },
 
-  onCollide: function () {
-    this.numJumps = 0;
-  }
-};
-
-},{}],103:[function(require,module,exports){
-/**
- * Kinematic body.
- *
- * Managed dynamic body, which moves but is not affected (directly) by the
- * physics engine. This is not a true kinematic body, in the sense that we are
- * letting the physics engine _compute_ collisions against it and selectively
- * applying those collisions to the object. The physics engine does not decide
- * the position/velocity/rotation of the element.
- *
- * Used for the camera object, because full physics simulation would create
- * movement that feels unnatural to the player. Bipedal movement does not
- * translate nicely to rigid body physics.
- *
- * See: http://www.learn-cocos2d.com/2013/08/physics-engine-platformer-terrible-idea/
- * And: http://oxleygamedev.blogspot.com/2011/04/player-physics-part-2.html
- */
-var CANNON = window.CANNON;
-var EPS = 0.000001;
+  emit: function (event) {
+    // Emit original event.
+    this.el.emit(event.type, event);
 
-module.exports = {
-  dependencies: ['velocity'],
+    // Emit convenience event, identifying button index.
+    this.el.emit(
+      event.type + ':' + event.index,
+      new GamepadButtonEvent(event.type, event.index, event)
+    );
+  },
 
   /*******************************************************************
-   * Schema
+   * Gamepad state
    */
 
-  schema: {
-    mass:           { default: 5 },
-    radius:         { default: 1.3 },
-    height:         { default: 1.764 },
-    linearDamping:  { default: 0.05 },
-    enableSlopes:   { default: true }
+  /**
+   * Returns the Gamepad instance attached to the component. If connected,
+   * a proxy-controls component may provide access to Gamepad input from a
+   * remote device.
+   *
+   * @return {Gamepad}
+   */
+  getGamepad: function () {
+    var localGamepad = navigator.getGamepads
+          && navigator.getGamepads()[this.data.controller],
+        proxyControls = this.el.sceneEl.components['proxy-controls'],
+        proxyGamepad = proxyControls && proxyControls.isConnected()
+          && proxyControls.getGamepad(this.data.controller);
+    return proxyGamepad || localGamepad;
   },
 
-  /*******************************************************************
-   * Lifecycle
+  /**
+   * Returns the state of the given button.
+   * @param  {number} index The button (0-N) for which to find state.
+   * @return {GamepadButton}
    */
+  getButton: function (index) {
+    return this.getGamepad().buttons[index];
+  },
 
-  init: function () {
-    this.system = this.el.sceneEl.systems.physics;
-    this.system.addBehavior(this, this.system.Phase.SIMULATE);
+  /**
+   * Returns state of the given axis. Axes are labelled 0-N, where 0-1 will
+   * represent X/Y on the first joystick, and 2-3 X/Y on the second.
+   * @param  {number} index The axis (0-N) for which to find state.
+   * @return {number} On the interval [-1,1].
+   */
+  getAxis: function (index) {
+    return this.getGamepad().axes[index];
+  },
 
-    var el = this.el,
-        data = this.data,
-        position = (new CANNON.Vec3()).copy(el.getAttribute('position'));
+  /**
+   * Returns the state of the given joystick (0 or 1) as a THREE.Vector2.
+   * @param  {number} id The joystick (0, 1) for which to find state.
+   * @return {THREE.Vector2}
+   */
+  getJoystick: function (index) {
+    var gamepad = this.getGamepad();
+    switch (index) {
+      case 0: return new THREE.Vector2(gamepad.axes[0], gamepad.axes[1]);
+      case 1: return new THREE.Vector2(gamepad.axes[2], gamepad.axes[3]);
+      default: throw new Error('Unexpected joystick index "%d".', index);
+    }
+  },
 
-    this.body = new CANNON.Body({
-      material: this.system.material,
-      position: position,
-      mass: data.mass,
-      linearDamping: data.linearDamping,
-      fixedRotation: true
-    });
-    this.body.addShape(
-      new CANNON.Sphere(data.radius),
-      new CANNON.Vec3(0, data.radius - data.height, 0)
+  /**
+   * Returns the state of the dpad as a THREE.Vector2.
+   * @return {THREE.Vector2}
+   */
+  getDpad: function () {
+    var gamepad = this.getGamepad();
+    if (!gamepad.buttons[GamepadButton.DPAD_RIGHT]) {
+      return new THREE.Vector2();
+    }
+    return new THREE.Vector2(
+      (gamepad.buttons[GamepadButton.DPAD_RIGHT].pressed ? 1 : 0)
+      + (gamepad.buttons[GamepadButton.DPAD_LEFT].pressed ? -1 : 0),
+      (gamepad.buttons[GamepadButton.DPAD_UP].pressed ? -1 : 0)
+      + (gamepad.buttons[GamepadButton.DPAD_DOWN].pressed ? 1 : 0)
     );
+  },
 
-    this.body.el = this.el;
-    this.el.body = this.body;
-    this.system.addBody(this.body);
+  /**
+   * Returns true if the gamepad is currently connected to the system.
+   * @return {boolean}
+   */
+  isConnected: function () {
+    var gamepad = this.getGamepad();
+    return !!(gamepad && gamepad.connected);
   },
 
-  remove: function () {
-    this.system.removeBody(this.body);
-    this.system.removeBehavior(this, this.system.Phase.SIMULATE);
-    delete this.el.body;
+  /**
+   * Returns a string containing some information about the controller. Result
+   * may vary across browsers, for a given controller.
+   * @return {string}
+   */
+  getID: function () {
+    return this.getGamepad().id;
+  }
+};
+
+},{"../../lib/GamepadButton":4,"../../lib/GamepadButtonEvent":5}],88:[function(require,module,exports){
+var radToDeg = THREE.Math.radToDeg,
+    isMobile = AFRAME.utils.device.isMobile();
+
+module.exports = {
+  schema: {
+    enabled: {default: true},
+    standing: {default: true}
   },
 
-  /*******************************************************************
-   * Tick
-   */
+  init: function () {
+    this.isPositionCalibrated = false;
+    this.dolly = new THREE.Object3D();
+    this.hmdEuler = new THREE.Euler();
+    this.previousHMDPosition = new THREE.Vector3();
+    this.deltaHMDPosition = new THREE.Vector3();
+    this.vrControls = new THREE.VRControls(this.dolly);
+    this.rotation = new THREE.Vector3();
+  },
 
-  /**
-   * Checks CANNON.World for collisions and attempts to apply them to the
-   * element automatically, in a player-friendly way.
-   *
-   * There's extra logic for horizontal surfaces here. The basic requirements:
-   * (1) Only apply gravity when not in contact with _any_ horizontal surface.
-   * (2) When moving, project the velocity against exactly one ground surface.
-   *     If in contact with two ground surfaces (e.g. ground + ramp), choose
-   *     the one that collides with current velocity, if any.
-   */
-  step: (function () {
-    var velocity = new THREE.Vector3(),
-        normalizedVelocity = new THREE.Vector3(),
-        currentSurfaceNormal = new THREE.Vector3(),
-        groundNormal = new THREE.Vector3();
+  update: function () {
+    var data = this.data;
+    var vrControls = this.vrControls;
+    vrControls.standing = data.standing;
+    vrControls.update();
+  },
 
-    return function (t, dt) {
-      if (!dt) return;
+  tick: function () {
+    this.vrControls.update();
+  },
 
-      var body = this.body,
-          data = this.data,
-          didCollide = false,
-          height, groundHeight = -Infinity,
-          groundBody;
+  remove: function () {
+    this.vrControls.dispose();
+  },
 
-      dt = Math.min(dt, this.system.data.maxInterval * 1000);
+  isRotationActive: function () {
+    var hmdEuler = this.hmdEuler;
+    if (!this.data.enabled || !(this.el.sceneEl.is('vr-mode') || isMobile)) {
+      return false;
+    }
+    hmdEuler.setFromQuaternion(this.dolly.quaternion, 'YXZ');
+    return !isNullVector(hmdEuler);
+  },
 
-      groundNormal.set(0, 0, 0);
-      velocity.copy(this.el.getAttribute('velocity'));
-      body.velocity.copy(velocity);
-      body.position.copy(this.el.getAttribute('position'));
+  getRotation: function () {
+    var hmdEuler = this.hmdEuler;
+    return this.rotation.set(
+      radToDeg(hmdEuler.x),
+      radToDeg(hmdEuler.y),
+      radToDeg(hmdEuler.z)
+    );
+  },
 
-      for (var i = 0, contact; (contact = this.system.world.contacts[i]); i++) {
-        // 1. Find any collisions involving this element. Get the contact
-        // normal, and make sure it's oriented _out_ of the other object.
-        if (body.id === contact.bi.id) {
-          contact.ni.negate(currentSurfaceNormal);
-        } else if (body.id === contact.bj.id) {
-          currentSurfaceNormal.copy(contact.ni);
-        } else {
-          continue;
-        }
+  isVelocityActive: function () {
+    var deltaHMDPosition = this.deltaHMDPosition;
+    var previousHMDPosition = this.previousHMDPosition;
+    var currentHMDPosition = this.calculateHMDPosition();
+    this.isPositionCalibrated = this.isPositionCalibrated || !isNullVector(previousHMDPosition);
+    if (!this.data.enabled || !this.el.sceneEl.is('vr-mode') || isMobile) {
+      return false;
+    }
+    deltaHMDPosition.copy(currentHMDPosition).sub(previousHMDPosition);
+    previousHMDPosition.copy(currentHMDPosition);
+    return this.isPositionCalibrated && !isNullVector(deltaHMDPosition);
+  },
 
-        didCollide = body.velocity.dot(currentSurfaceNormal) < -EPS;
-        if (didCollide && currentSurfaceNormal.y <= 0.5) {
-          // 2. If current trajectory attempts to move _through_ another
-          // object, project the velocity against the collision plane to
-          // prevent passing through.
-          velocity = velocity.projectOnPlane(currentSurfaceNormal);
-        } else if (currentSurfaceNormal.y > 0.5) {
-          // 3. If in contact with something roughly horizontal (+/- 45º) then
-          // consider that the current ground. Only the highest qualifying
-          // ground is retained.
-          height = body.id === contact.bi.id
-            ? Math.abs(contact.rj.y + contact.bj.position.y)
-            : Math.abs(contact.ri.y + contact.bi.position.y);
-          if (height > groundHeight) {
-            groundHeight = height;
-            groundNormal.copy(currentSurfaceNormal);
-            groundBody = body.id === contact.bi.id ? contact.bj : contact.bi;
-          }
-        }
-      }
+  getPositionDelta: function () {
+    return this.deltaHMDPosition;
+  },
 
-      normalizedVelocity.copy(velocity).normalize();
-      if (groundBody && normalizedVelocity.y < 0.5) {
-        if (!data.enableSlopes) {
-          groundNormal.set(0, 1, 0);
-        } else if (groundNormal.y < 1 - EPS) {
-          groundNormal.copy(this.raycastToGround(groundBody, groundNormal));
-        }
+  calculateHMDPosition: function () {
+    var dolly = this.dolly;
+    var position = new THREE.Vector3();
+    dolly.updateMatrix();
+    position.setFromMatrixPosition(dolly.matrix);
+    return position;
+  }
+};
 
-        // 4. Project trajectory onto the top-most ground object, unless
-        // trajectory is > 45º.
-        velocity = velocity.projectOnPlane(groundNormal);
-      } else {
-        // 5. If not in contact with anything horizontal, apply world gravity.
-        // TODO - Why is the 4x scalar necessary.
-        velocity.add(this.system.world.gravity.scale(dt * 4.0 / 1000));
-      }
+function isNullVector (vector) {
+  return vector.x === 0 && vector.y === 0 && vector.z === 0;
+}
 
-      // 6. If the ground surface has a velocity, apply it directly to current
-      // position, not velocity, to preserve relative velocity.
-      if (groundBody && groundBody.el && groundBody.el.components.velocity) {
-        var groundVelocity = groundBody.el.getAttribute('velocity');
-        body.position.copy({
-          x: body.position.x + groundVelocity.x * dt / 1000,
-          y: body.position.y + groundVelocity.y * dt / 1000,
-          z: body.position.z + groundVelocity.z * dt / 1000
-        });
-        this.el.setAttribute('position', body.position);
-      }
+},{}],89:[function(require,module,exports){
+var physics = require('aframe-physics-system');
 
-      body.velocity.copy(velocity);
-      this.el.setAttribute('velocity', velocity);
-    };
-  }()),
+module.exports = {
+  'checkpoint-controls': require('./checkpoint-controls'),
+  'gamepad-controls':    require('./gamepad-controls'),
+  'hmd-controls':        require('./hmd-controls'),
+  'keyboard-controls':   require('./keyboard-controls'),
+  'mouse-controls':      require('./mouse-controls'),
+  'touch-controls':      require('./touch-controls'),
+  'universal-controls':  require('./universal-controls'),
 
-  /**
-   * When walking on complex surfaces (trimeshes, borders between two shapes),
-   * the collision normals returned for the player sphere can be very
-   * inconsistent. To address this, raycast straight down, find the collision
-   * normal, and return whichever normal is more vertical.
-   * @param  {CANNON.Body} groundBody
-   * @param  {CANNON.Vec3} groundNormal
-   * @return {CANNON.Vec3}
-   */
-  raycastToGround: function (groundBody, groundNormal) {
-    var ray,
-        hitNormal,
-        vFrom = this.body.position,
-        vTo = this.body.position.clone();
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
 
-    vTo.y -= this.data.height;
-    ray = new CANNON.Ray(vFrom, vTo);
-    ray._updateDirection(); // TODO - Report bug.
-    ray.intersectBody(groundBody);
+    AFRAME = AFRAME || window.AFRAME;
 
-    if (!ray.hasHit) return groundNormal;
+    physics.registerAll();
+    if (!AFRAME.components['checkpoint-controls'])  AFRAME.registerComponent('checkpoint-controls', this['checkpoint-controls']);
+    if (!AFRAME.components['gamepad-controls'])     AFRAME.registerComponent('gamepad-controls',    this['gamepad-controls']);
+    if (!AFRAME.components['hmd-controls'])         AFRAME.registerComponent('hmd-controls',        this['hmd-controls']);
+    if (!AFRAME.components['keyboard-controls'])    AFRAME.registerComponent('keyboard-controls',   this['keyboard-controls']);
+    if (!AFRAME.components['mouse-controls'])       AFRAME.registerComponent('mouse-controls',      this['mouse-controls']);
+    if (!AFRAME.components['touch-controls'])       AFRAME.registerComponent('touch-controls',      this['touch-controls']);
+    if (!AFRAME.components['universal-controls'])   AFRAME.registerComponent('universal-controls',  this['universal-controls']);
 
-    // Compare ABS, in case we're projecting against the inside of the face.
-    hitNormal = ray.result.hitNormalWorld;
-    return Math.abs(hitNormal.y) > Math.abs(groundNormal.y) ? hitNormal : groundNormal;
+    this._registered = true;
   }
 };
 
-},{}],104:[function(require,module,exports){
-/**
- * Apply this component to models that looks "blocky", to have Three.js compute
- * vertex normals on the fly for a "smoother" look.
- */
-module.exports = {
-  init: function () {
-    this.el.addEventListener('model-loaded', function (e) {
-      e.detail.model.traverse(function (node) {
-        if (node.isMesh) node.geometry.computeVertexNormals();
-      });
-    })
-  }
-}
+},{"./checkpoint-controls":86,"./gamepad-controls":87,"./hmd-controls":88,"./keyboard-controls":90,"./mouse-controls":91,"./touch-controls":92,"./universal-controls":93,"aframe-physics-system":11}],90:[function(require,module,exports){
+require('../../lib/keyboard.polyfill');
+
+var MAX_DELTA = 0.2,
+    PROXY_FLAG = '__keyboard-controls-proxy';
+
+var KeyboardEvent = window.KeyboardEvent;
 
-},{}],105:[function(require,module,exports){
 /**
- * Based on aframe/examples/showcase/tracked-controls.
+ * Keyboard Controls component.
  *
- * Implement bounding sphere collision detection for entities with a mesh.
- * Sets the specified state on the intersected entities.
+ * Stripped-down version of: https://github.com/donmccurdy/aframe-keyboard-controls
  *
- * @property {string} objects - Selector of the entities to test for collision.
- * @property {string} state - State to set on collided entities.
+ * Bind keyboard events to components, or control your entities with the WASD keys.
+ *
+ * Why use KeyboardEvent.code? "This is set to a string representing the key that was pressed to
+ * generate the KeyboardEvent, without taking the current keyboard layout (e.g., QWERTY vs.
+ * Dvorak), locale (e.g., English vs. French), or any modifier keys into account. This is useful
+ * when you care about which physical key was pressed, rather thanwhich character it corresponds
+ * to. For example, if you’re a writing a game, you might want a certain set of keys to move the
+ * player in different directions, and that mapping should ideally be independent of keyboard
+ * layout. See: https://developers.google.com/web/updates/2016/04/keyboardevent-keys-codes
  *
+ * @namespace wasd-controls
+ * keys the entity moves and if you release it will stop. Easing simulates friction.
+ * to the entity when pressing the keys.
+ * @param {bool} [enabled=true] - To completely enable or disable the controls
  */
 module.exports = {
   schema: {
-    objects: {default: ''},
-    state: {default: 'collided'},
-    radius: {default: 0.05},
-    watch: {default: true}
-  },
-
-  init: function () {
-    /** @type {MutationObserver} */
-    this.observer = null;
-    /** @type {Array<Element>} Elements to watch for collisions. */
-    this.els = [];
-    /** @type {Array<Element>} Elements currently in collision state. */
-    this.collisions = [];
-
-    this.handleHit = this.handleHit.bind(this);
+    enabled:           { default: true },
+    debug:             { default: false }
   },
 
-  remove: function () {
-    this.pause();
+  init: function () {
+    this.dVelocity = new THREE.Vector3();
+    this.localKeys = {};
+    this.listeners = {
+      keydown: this.onKeyDown.bind(this),
+      keyup: this.onKeyUp.bind(this),
+      blur: this.onBlur.bind(this)
+    };
+    this.attachEventListeners();
   },
 
-  play: function () {
-    var sceneEl = this.el.sceneEl;
+  /*******************************************************************
+  * Movement
+  */
 
-    if (this.data.watch) {
-      this.observer = new MutationObserver(this.update.bind(this, null));
-      this.observer.observe(sceneEl, {childList: true, subtree: true});
-    }
+  isVelocityActive: function () {
+    return this.data.enabled && !!Object.keys(this.getKeys()).length;
   },
 
-  pause: function () {
-    if (this.observer) {
-      this.observer.disconnect();
-      this.observer = null;
+  getVelocityDelta: function () {
+    var data = this.data,
+        keys = this.getKeys();
+
+    this.dVelocity.set(0, 0, 0);
+    if (data.enabled) {
+      if (keys.KeyW || keys.ArrowUp)    { this.dVelocity.z -= 1; }
+      if (keys.KeyA || keys.ArrowLeft)  { this.dVelocity.x -= 1; }
+      if (keys.KeyS || keys.ArrowDown)  { this.dVelocity.z += 1; }
+      if (keys.KeyD || keys.ArrowRight) { this.dVelocity.x += 1; }
     }
+
+    return this.dVelocity.clone();
   },
 
-  /**
-   * Update list of entities to test for collision.
-   */
-  update: function () {
-    var data = this.data;
-    var objectEls;
+  /*******************************************************************
+  * Events
+  */
 
-    // Push entities into list of els to intersect.
-    if (data.objects) {
-      objectEls = this.el.sceneEl.querySelectorAll(data.objects);
-    } else {
-      // If objects not defined, intersect with everything.
-      objectEls = this.el.sceneEl.children;
-    }
-    // Convert from NodeList to Array
-    this.els = Array.prototype.slice.call(objectEls);
+  play: function () {
+    this.attachEventListeners();
   },
 
-  tick: (function () {
-    var position = new THREE.Vector3(),
-        meshPosition = new THREE.Vector3(),
-        meshScale = new THREE.Vector3(),
-        colliderScale = new THREE.Vector3(),
-        distanceMap = new Map();
-    return function () {
-      var el = this.el,
-          data = this.data,
-          mesh = el.getObject3D('mesh'),
-          colliderRadius,
-          collisions = [];
-
-      if (!mesh) { return; }
+  pause: function () {
+    this.removeEventListeners();
+  },
 
-      distanceMap.clear();
-      position.copy(el.object3D.getWorldPosition());
-      el.object3D.getWorldScale(colliderScale);
-      colliderRadius = data.radius * scaleFactor(colliderScale);
-      // Update collision list.
-      this.els.forEach(intersect);
+  remove: function () {
+    this.pause();
+  },
 
-      // Emit events and add collision states, in order of distance.
-      collisions
-        .sort(function (a, b) {
-          return distanceMap.get(a) > distanceMap.get(b) ? 1 : -1;
-        })
-        .forEach(this.handleHit);
+  attachEventListeners: function () {
+    window.addEventListener('keydown', this.listeners.keydown, false);
+    window.addEventListener('keyup', this.listeners.keyup, false);
+    window.addEventListener('blur', this.listeners.blur, false);
+  },
 
-      // Remove collision state from current element.
-      if (collisions.length === 0) { el.emit('hit', {el: null}); }
+  removeEventListeners: function () {
+    window.removeEventListener('keydown', this.listeners.keydown);
+    window.removeEventListener('keyup', this.listeners.keyup);
+    window.removeEventListener('blur', this.listeners.blur);
+  },
 
-      // Remove collision state from other elements.
-      this.collisions.filter(function (el) {
-        return !distanceMap.has(el);
-      }).forEach(function removeState (el) {
-        el.removeState(data.state);
-      });
+  onKeyDown: function (event) {
+    if (AFRAME.utils.shouldCaptureKeyEvent(event)) {
+      this.localKeys[event.code] = true;
+      this.emit(event);
+    }
+  },
 
-      // Store new collisions
-      this.collisions = collisions;
+  onKeyUp: function (event) {
+    if (AFRAME.utils.shouldCaptureKeyEvent(event)) {
+      delete this.localKeys[event.code];
+      this.emit(event);
+    }
+  },
 
-      // Bounding sphere collision detection
-      function intersect (el) {
-        var radius, mesh, distance, box, extent, size;
+  onBlur: function () {
+    for (var code in this.localKeys) {
+      if (this.localKeys.hasOwnProperty(code)) {
+        delete this.localKeys[code];
+      }
+    }
+  },
 
-        if (!el.isEntity) { return; }
+  emit: function (event) {
+    // TODO - keydown only initially?
+    // TODO - where the f is the spacebar
 
-        mesh = el.getObject3D('mesh');
+    // Emit original event.
+    if (PROXY_FLAG in event) {
+      // TODO - Method never triggered.
+      this.el.emit(event.type, event);
+    }
 
-        if (!mesh) { return; }
+    // Emit convenience event, identifying key.
+    this.el.emit(event.type + ':' + event.code, new KeyboardEvent(event.type, event));
+    if (this.data.debug) console.log(event.type + ':' + event.code);
+  },
 
-        box = new THREE.Box3().setFromObject(mesh);
-        size = box.getSize();
-        extent = Math.max(size.x, size.y, size.z) / 2;
-        radius = Math.sqrt(2 * extent * extent);
-        box.getCenter(meshPosition);
+  /*******************************************************************
+  * Accessors
+  */
 
-        if (!radius) { return; }
+  isPressed: function (code) {
+    return code in this.getKeys();
+  },
 
-        distance = position.distanceTo(meshPosition);
-        if (distance < radius + colliderRadius) {
-          collisions.push(el);
-          distanceMap.set(el, distance);
-        }
-      }
-      // use max of scale factors to maintain bounding sphere collision
-      function scaleFactor (scaleVec) {
-        return Math.max.apply(null, scaleVec.toArray());
-      }
-    };
-  })(),
+  getKeys: function () {
+    if (this.isProxied()) {
+      return this.el.sceneEl.components['proxy-controls'].getKeyboard();
+    }
+    return this.localKeys;
+  },
 
-  handleHit: function (targetEl) {
-    targetEl.emit('hit');
-    targetEl.addState(this.data.state);
-    this.el.emit('hit', {el: targetEl});
+  isProxied: function () {
+    var proxyControls = this.el.sceneEl.components['proxy-controls'];
+    return proxyControls && proxyControls.isConnected();
   }
+
 };
 
-},{}],106:[function(require,module,exports){
+},{"../../lib/keyboard.polyfill":10}],91:[function(require,module,exports){
+document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock;
+
 /**
- * Toggle velocity.
+ * Mouse + Pointerlock controls.
  *
- * Moves an object back and forth along an axis, within a min/max extent.
+ * Based on: https://github.com/aframevr/aframe/pull/1056
  */
 module.exports = {
-  dependencies: ['velocity'],
   schema: {
-    axis: { default: 'x', oneOf: ['x', 'y', 'z'] },
-    min: { default: 0 },
-    max: { default: 0 },
-    speed: { default: 1 }
+    enabled: { default: true },
+    pointerlockEnabled: { default: true },
+    sensitivity: { default: 1 / 25 }
   },
-  init: function () {
-    var velocity = {x: 0, y: 0, z: 0};
-    velocity[this.data.axis] = this.data.speed;
-    this.el.setAttribute('velocity', velocity);
 
-    if (this.el.sceneEl.addBehavior) this.el.sceneEl.addBehavior(this);
+  init: function () {
+    this.mouseDown = false;
+    this.pointerLocked = false;
+    this.lookVector = new THREE.Vector2();
+    this.bindMethods();
   },
-  remove: function () {},
-  update: function () { this.tick(); },
-  tick: function () {
-    var data = this.data,
-        velocity = this.el.getAttribute('velocity'),
-        position = this.el.getAttribute('position');
-    if (velocity[data.axis] > 0 && position[data.axis] > data.max) {
-      velocity[data.axis] = -data.speed;
-      this.el.setAttribute('velocity', velocity);
-    } else if (velocity[data.axis] < 0 && position[data.axis] < data.min) {
-      velocity[data.axis] = data.speed;
-      this.el.setAttribute('velocity', velocity);
+
+  update: function (previousData) {
+    var data = this.data;
+    if (previousData.pointerlockEnabled && !data.pointerlockEnabled && this.pointerLocked) {
+      document.exitPointerLock();
     }
   },
-};
 
-},{}],107:[function(require,module,exports){
-module.exports = {
-  'nav-mesh':    require('./nav-mesh'),
-  'nav-controller':     require('./nav-controller'),
-  'system':      require('./system'),
+  play: function () {
+    this.addEventListeners();
+  },
 
-  registerAll: function (AFRAME) {
-    if (this._registered) return;
+  pause: function () {
+    this.removeEventListeners();
+    this.lookVector.set(0, 0);
+  },
 
-    AFRAME = AFRAME || window.AFRAME;
+  remove: function () {
+    this.pause();
+  },
 
-    if (!AFRAME.components['nav-mesh']) {
-      AFRAME.registerComponent('nav-mesh', this['nav-mesh']);
-    }
+  bindMethods: function () {
+    this.onMouseDown = this.onMouseDown.bind(this);
+    this.onMouseMove = this.onMouseMove.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+    this.onMouseUp = this.onMouseUp.bind(this);
+    this.onPointerLockChange = this.onPointerLockChange.bind(this);
+    this.onPointerLockChange = this.onPointerLockChange.bind(this);
+    this.onPointerLockChange = this.onPointerLockChange.bind(this);
+  },
 
-    if (!AFRAME.components['nav-controller']) {
-      AFRAME.registerComponent('nav-controller',  this['nav-controller']);
-    }
+  addEventListeners: function () {
+    var sceneEl = this.el.sceneEl;
+    var canvasEl = sceneEl.canvas;
+    var data = this.data;
 
-    if (!AFRAME.systems.nav) {
-      AFRAME.registerSystem('nav', this.system);
+    if (!canvasEl) {
+      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
+      return;
     }
 
-    this._registered = true;
-  }
-};
+    canvasEl.addEventListener('mousedown', this.onMouseDown, false);
+    canvasEl.addEventListener('mousemove', this.onMouseMove, false);
+    canvasEl.addEventListener('mouseup', this.onMouseUp, false);
+    canvasEl.addEventListener('mouseout', this.onMouseUp, false);
 
-},{"./nav-controller":108,"./nav-mesh":109,"./system":110}],108:[function(require,module,exports){
-module.exports = {
-  schema: {
-    destination: {type: 'vec3'},
-    active: {default: false},
-    speed: {default: 2}
+    if (data.pointerlockEnabled) {
+      document.addEventListener('pointerlockchange', this.onPointerLockChange, false);
+      document.addEventListener('mozpointerlockchange', this.onPointerLockChange, false);
+      document.addEventListener('pointerlockerror', this.onPointerLockError, false);
+    }
   },
-  init: function () {
-    this.system = this.el.sceneEl.systems.nav;
-    this.system.addController(this);
-    this.path = [];
-    this.raycaster = new THREE.Raycaster();
+
+  removeEventListeners: function () {
+    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
+    if (canvasEl) {
+      canvasEl.removeEventListener('mousedown', this.onMouseDown, false);
+      canvasEl.removeEventListener('mousemove', this.onMouseMove, false);
+      canvasEl.removeEventListener('mouseup', this.onMouseUp, false);
+      canvasEl.removeEventListener('mouseout', this.onMouseUp, false);
+    }
+    document.removeEventListener('pointerlockchange', this.onPointerLockChange, false);
+    document.removeEventListener('mozpointerlockchange', this.onPointerLockChange, false);
+    document.removeEventListener('pointerlockerror', this.onPointerLockError, false);
   },
-  remove: function () {
-    this.system.removeController(this);
+
+  isRotationActive: function () {
+    return this.data.enabled && (this.mouseDown || this.pointerLocked);
   },
-  update: function () {
-    this.path.length = 0;
+
+  /**
+   * Returns the sum of all mouse movement since last call.
+   */
+  getRotationDelta: function () {
+    var dRotation = this.lookVector.clone().multiplyScalar(this.data.sensitivity);
+    this.lookVector.set(0, 0);
+    return dRotation;
   },
-  tick: (function () {
-    var vDest = new THREE.Vector3();
-    var vDelta = new THREE.Vector3();
-    var vNext = new THREE.Vector3();
 
-    return function (t, dt) {
-      var el = this.el;
-      var data = this.data;
-      var raycaster = this.raycaster;
-      var speed = data.speed * dt / 1000;
+  onMouseMove: function (event) {
+    var previousMouseEvent = this.previousMouseEvent;
 
-      if (!data.active) return;
+    if (!this.data.enabled || !(this.mouseDown || this.pointerLocked)) {
+      return;
+    }
 
-      // Use PatrolJS pathfinding system to get shortest path to target.
-      if (!this.path.length) {
-        this.path = this.system.getPath(this.el.object3D, vDest.copy(data.destination));
-        this.path = this.path || [];
-        el.emit('nav-start');
-      }
+    var movementX = event.movementX || event.mozMovementX || 0;
+    var movementY = event.movementY || event.mozMovementY || 0;
 
-      // If no path is found, exit.
-      if (!this.path.length) {
-        console.warn('[nav] Unable to find path to %o.', data.destination);
-        this.el.setAttribute('nav-controller', {active: false});
-        el.emit('nav-end');
-        return;
-      }
+    if (!this.pointerLocked) {
+      movementX = event.screenX - previousMouseEvent.screenX;
+      movementY = event.screenY - previousMouseEvent.screenY;
+    }
 
-      // Current segment is a vector from current position to next waypoint.
-      var vCurrent = el.object3D.position;
-      var vWaypoint = this.path[0];
-      vDelta.subVectors(vWaypoint, vCurrent);
+    this.lookVector.x += movementX;
+    this.lookVector.y += movementY;
 
-      var distance = vDelta.length();
-      var gazeTarget;
+    this.previousMouseEvent = event;
+  },
 
-      if (distance < speed) {
-        // If <1 step from current waypoint, discard it and move toward next.
-        this.path.shift();
+  onMouseDown: function (event) {
+    var canvasEl = this.el.sceneEl.canvas,
+        isEditing = (AFRAME.INSPECTOR || {}).opened;
 
-        // After discarding the last waypoint, exit pathfinding.
-        if (!this.path.length) {
-          this.el.setAttribute('nav-controller', {active: false});
-          el.emit('nav-end');
-          return;
-        } else {
-          gazeTarget = this.path[0];
-        }
-      } else {
-        // If still far away from next waypoint, find next position for
-        // the current frame.
-        vNext.copy(vDelta.setLength(speed)).add(vCurrent);
-        gazeTarget = vWaypoint;
-      }
+    this.mouseDown = true;
+    this.previousMouseEvent = event;
 
-      // Look at the next waypoint.
-      gazeTarget.y = vCurrent.y;
-      el.object3D.lookAt(gazeTarget);
+    if (this.data.pointerlockEnabled && !this.pointerLocked && !isEditing) {
+      if (canvasEl.requestPointerLock) {
+        canvasEl.requestPointerLock();
+      } else if (canvasEl.mozRequestPointerLock) {
+        canvasEl.mozRequestPointerLock();
+      }
+    }
+  },
 
-      // Raycast against the nav mesh, to keep the controller moving along the
-      // ground, not traveling in a straight line from higher to lower waypoints.
-      raycaster.ray.origin.copy(vNext);
-      raycaster.ray.origin.y += 1.5;
-      raycaster.ray.direction.y = -1;
-      var intersections = raycaster.intersectObject(this.system.getNavMesh());
+  onMouseUp: function () {
+    this.mouseDown = false;
+  },
 
-      if (!intersections.length) {
-        // Raycasting failed. Step toward the waypoint and hope for the best.
-        vCurrent.copy(vNext);
-      } else {
-        // Re-project next position onto nav mesh.
-        vDelta.subVectors(intersections[0].point, vCurrent);
-        vCurrent.add(vDelta.setLength(speed));
-      }
+  onPointerLockChange: function () {
+    this.pointerLocked = !!(document.pointerLockElement || document.mozPointerLockElement);
+  },
 
-    };
-  }())
+  onPointerLockError: function () {
+    this.pointerLocked = false;
+  }
 };
 
-},{}],109:[function(require,module,exports){
-/**
- * nav-mesh
- *
- * Waits for a mesh to be loaded on the current entity, then sets it as the
- * nav mesh in the pathfinding system.
- */
+},{}],92:[function(require,module,exports){
 module.exports = {
+  schema: {
+    enabled: { default: true }
+  },
+
   init: function () {
-    this.system = this.el.sceneEl.systems.nav;
-    this.loadNavMesh();
-    this.el.addEventListener('model-loaded', this.loadNavMesh.bind(this));
+    this.dVelocity = new THREE.Vector3();
+    this.bindMethods();
   },
 
-  loadNavMesh: function () {
-    var object = this.el.getObject3D('mesh');
+  play: function () {
+    this.addEventListeners();
+  },
 
-    if (!object) return;
+  pause: function () {
+    this.removeEventListeners();
+    this.dVelocity.set(0, 0, 0);
+  },
 
-    var navMesh;
-    object.traverse(function (node) {
-      if (node.isMesh) navMesh = node;
-    });
+  remove: function () {
+    this.pause();
+  },
 
-    if (!navMesh) return;
+  addEventListeners: function () {
+    var sceneEl = this.el.sceneEl;
+    var canvasEl = sceneEl.canvas;
 
-    this.system.setNavMesh(navMesh);
-  }
-};
+    if (!canvasEl) {
+      sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this));
+      return;
+    }
 
-},{}],110:[function(require,module,exports){
-var Path = require('three-pathfinding');
+    canvasEl.addEventListener('touchstart', this.onTouchStart);
+    canvasEl.addEventListener('touchend', this.onTouchEnd);
+  },
 
-/**
- * nav
- *
- * Pathfinding system, using PatrolJS.
- */
-module.exports = {
-  init: function () {
-    this.navMesh = null;
-    this.nodes = null;
-    this.controllers = new Set();
+  removeEventListeners: function () {
+    var canvasEl = this.el.sceneEl && this.el.sceneEl.canvas;
+    if (!canvasEl) { return; }
+
+    canvasEl.removeEventListener('touchstart', this.onTouchStart);
+    canvasEl.removeEventListener('touchend', this.onTouchEnd);
   },
 
-  /**
-   * @param {THREE.Mesh} mesh
-   */
-  setNavMesh: function (mesh) {
-    var geometry = mesh.geometry.isBufferGeometry
-      ? new THREE.Geometry().fromBufferGeometry(mesh.geometry)
-      : mesh.geometry;
-    this.navMesh = new THREE.Mesh(geometry);
-    this.nodes = Path.buildNodes(this.navMesh.geometry);
-    Path.setZoneData('level', this.nodes);
+  isVelocityActive: function () {
+    return this.data.enabled && this.isMoving;
   },
 
-  /**
-   * @return {THREE.Mesh}
-   */
-  getNavMesh: function () {
-    return this.navMesh;
+  getVelocityDelta: function () {
+    this.dVelocity.z = this.isMoving ? -1 : 0;
+    return this.dVelocity.clone();
   },
 
-  /**
-   * @param {NavController} ctrl
-   */
-  addController: function (ctrl) {
-    this.controllers.add(ctrl);
+  bindMethods: function () {
+    this.onTouchStart = this.onTouchStart.bind(this);
+    this.onTouchEnd = this.onTouchEnd.bind(this);
   },
 
-  /**
-   * @param {NavController} ctrl
-   */
-  removeController: function (ctrl) {
-    this.controllers.remove(ctrl);
+  onTouchStart: function (e) {
+    this.isMoving = true;
+    e.preventDefault();
   },
 
-  /**
-   * @param  {NavController} ctrl
-   * @param  {THREE.Vector3} target
-   * @return {Array<THREE.Vector3>}
-   */
-  getPath: function (ctrl, target) {
-    var start = ctrl.el.object3D.position;
-    // TODO(donmccurdy): Current group should be cached.
-    var group = Path.getGroup('level', start);
-    return Path.findPath(start, target, 'level', group);
+  onTouchEnd: function (e) {
+    this.isMoving = false;
+    e.preventDefault();
   }
 };
 
-},{"three-pathfinding":119}],111:[function(require,module,exports){
+},{}],93:[function(require,module,exports){
 /**
- * Flat grid.
+ * Universal Controls
  *
- * Defaults to 75x75.
+ * @author Don McCurdy <dm@donmccurdy.com>
  */
-var Primitive = module.exports = {
-  defaultComponents: {
-    geometry: {
-      primitive: 'plane',
-      width: 75,
-      height: 75
-    },
-    rotation: {x: -90, y: 0, z: 0},
-    material: {
-      src: 'url(https://cdn.rawgit.com/donmccurdy/aframe-extras/v1.16.3/assets/grid.png)',
-      repeat: '75 75'
-    }
-  },
-  mappings: {
-    width: 'geometry.width',
-    height: 'geometry.height',
-    src: 'material.src'
-  }
-};
 
-module.exports.registerAll = (function () {
-  var registered = false;
-  return function (AFRAME) {
-    if (registered) return;
-    AFRAME = AFRAME || window.AFRAME;
-    AFRAME.registerPrimitive('a-grid', Primitive);
-    registered = true;
-  };
-}());
+var COMPONENT_SUFFIX = '-controls',
+    MAX_DELTA = 0.2, // ms
+    PI_2 = Math.PI / 2;
 
-},{}],112:[function(require,module,exports){
-var vg = require('../../lib/hex-grid.min.js');
-var defaultHexGrid = require('../../lib/default-hex-grid.json');
+module.exports = {
 
-/**
- * Hex grid.
- */
-var Primitive = module.exports.Primitive = {
-  defaultComponents: {
-    'hexgrid': {}
-  },
-  mappings: {
-    src: 'hexgrid.src'
-  }
-};
+  /*******************************************************************
+   * Schema
+   */
+
+  dependencies: ['velocity', 'rotation'],
 
-var Component = module.exports.Component = {
-  dependencies: ['material'],
   schema: {
-    src: {type: 'asset'}
+    enabled:              { default: true },
+    movementEnabled:      { default: true },
+    movementControls:     { default: ['gamepad', 'keyboard', 'touch', 'hmd'] },
+    rotationEnabled:      { default: true },
+    rotationControls:     { default: ['hmd', 'gamepad', 'mouse'] },
+    movementSpeed:        { default: 5 }, // m/s
+    movementEasing:       { default: 15 }, // m/s2
+    movementEasingY:      { default: 0  }, // m/s2
+    movementAcceleration: { default: 80 }, // m/s2
+    rotationSensitivity:  { default: 0.05 }, // radians/frame, ish
+    fly:                  { default: false },
   },
+
+  /*******************************************************************
+   * Lifecycle
+   */
+
   init: function () {
-    var data = this.data;
-    if (data.src) {
-      fetch(data.src)
-        .then(function (response) { response.json(); })
-        .then(function (json) { this.addMesh(json); });
+    var rotation = this.el.getAttribute('rotation');
+
+    if (this.el.hasAttribute('look-controls') && this.data.rotationEnabled) {
+      console.error('[universal-controls] The `universal-controls` component is a replacement '
+        + 'for `look-controls`, and cannot be used in combination with it.');
+    }
+
+    // Movement
+    this.velocity = new THREE.Vector3();
+
+    // Rotation
+    this.pitch = new THREE.Object3D();
+    this.pitch.rotation.x = THREE.Math.degToRad(rotation.x);
+    this.yaw = new THREE.Object3D();
+    this.yaw.position.y = 10;
+    this.yaw.rotation.y = THREE.Math.degToRad(rotation.y);
+    this.yaw.add(this.pitch);
+    this.heading = new THREE.Euler(0, 0, 0, 'YXZ');
+
+    if (this.el.sceneEl.hasLoaded) {
+      this.injectControls();
     } else {
-      this.addMesh(defaultHexGrid);
+      this.el.sceneEl.addEventListener('loaded', this.injectControls.bind(this));
     }
   },
-  addMesh: function (json) {
-    var grid = new vg.HexGrid();
-    grid.fromJSON(json);
-    var board = new vg.Board(grid);
-    board.generateTilemap();
-    this.el.setObject3D('mesh', board.group);
-    this.addMaterial();
+
+  update: function () {
+    if (this.el.sceneEl.hasLoaded) {
+      this.injectControls();
+    }
   },
-  addMaterial: function () {
-    var materialComponent = this.el.components.material;
-    var material = (materialComponent || {}).material;
-    if (!material) return;
-    this.el.object3D.traverse(function (node) {
-      if (node.isMesh) {
-        node.material = material;
+
+  injectControls: function () {
+    var i, name,
+        data = this.data;
+
+    for (i = 0; i < data.movementControls.length; i++) {
+      name = data.movementControls[i] + COMPONENT_SUFFIX;
+      if (!this.el.components[name]) {
+        this.el.setAttribute(name, '');
       }
-    });
+    }
+
+    for (i = 0; i < data.rotationControls.length; i++) {
+      name = data.rotationControls[i] + COMPONENT_SUFFIX;
+      if (!this.el.components[name]) {
+        this.el.setAttribute(name, '');
+      }
+    }
   },
-  remove: function () {
-    this.el.removeObject3D('mesh');
-  }
-};
 
-module.exports.registerAll = (function () {
-  var registered = false;
-  return function (AFRAME) {
-    if (registered) return;
-    AFRAME = AFRAME || window.AFRAME;
-    AFRAME.registerComponent('hexgrid', Component);
-    AFRAME.registerPrimitive('a-hexgrid', Primitive);
-    registered = true;
-  };
-}());
+  /*******************************************************************
+   * Tick
+   */
 
-},{"../../lib/default-hex-grid.json":7,"../../lib/hex-grid.min.js":9}],113:[function(require,module,exports){
-/**
- * Flat-shaded ocean primitive.
- *
- * Based on a Codrops tutorial:
- * http://tympanus.net/codrops/2016/04/26/the-aviator-animating-basic-3d-scene-threejs/
- */
-var Primitive = module.exports.Primitive = {
-  defaultComponents: {
-    ocean: {},
-    rotation: {x: -90, y: 0, z: 0}
-  },
-  mappings: {
-    width: 'ocean.width',
-    depth: 'ocean.depth',
-    density: 'ocean.density',
-    color: 'ocean.color',
-    opacity: 'ocean.opacity'
-  }
-};
+  tick: function (t, dt) {
+    if (!dt) { return; }
 
-var Component = module.exports.Component = {
-  schema: {
-    // Dimensions of the ocean area.
-    width: {default: 10, min: 0},
-    depth: {default: 10, min: 0},
+    // Update rotation.
+    if (this.data.rotationEnabled) this.updateRotation(dt);
 
-    // Density of waves.
-    density: {default: 10},
+    // Update velocity. If FPS is too low, reset.
+    if (this.data.movementEnabled && dt / 1000 > MAX_DELTA) {
+      this.velocity.set(0, 0, 0);
+      this.el.setAttribute('velocity', this.velocity);
+    } else {
+      this.updateVelocity(dt);
+    }
+  },
 
-    // Wave amplitude and variance.
-    amplitude: {default: 0.1},
-    amplitudeVariance: {default: 0.3},
+  /*******************************************************************
+   * Rotation
+   */
 
-    // Wave speed and variance.
-    speed: {default: 1},
-    speedVariance: {default: 2},
+  updateRotation: function (dt) {
+    var control, dRotation,
+        data = this.data;
 
-    // Material.
-    color: {default: '#7AD2F7', type: 'color'},
-    opacity: {default: 0.8}
+    for (var i = 0, l = data.rotationControls.length; i < l; i++) {
+      control = this.el.components[data.rotationControls[i] + COMPONENT_SUFFIX];
+      if (control && control.isRotationActive()) {
+        if (control.getRotationDelta) {
+          dRotation = control.getRotationDelta(dt);
+          dRotation.multiplyScalar(data.rotationSensitivity);
+          this.yaw.rotation.y -= dRotation.x;
+          this.pitch.rotation.x -= dRotation.y;
+          this.pitch.rotation.x = Math.max(-PI_2, Math.min(PI_2, this.pitch.rotation.x));
+          this.el.setAttribute('rotation', {
+            x: THREE.Math.radToDeg(this.pitch.rotation.x),
+            y: THREE.Math.radToDeg(this.yaw.rotation.y),
+            z: 0
+          });
+        } else if (control.getRotation) {
+          this.el.setAttribute('rotation', control.getRotation());
+        } else {
+          throw new Error('Incompatible rotation controls: %s', data.rotationControls[i]);
+        }
+        break;
+      }
+    }
   },
-
-  /**
-   * Use play() instead of init(), because component mappings – unavailable as dependencies – are
-   * not guaranteed to have parsed when this component is initialized.
+
+  /*******************************************************************
+   * Movement
    */
-  play: function () {
-    var el = this.el,
-        data = this.data,
-        material = el.components.material;
 
-    var geometry = new THREE.PlaneGeometry(data.width, data.depth, data.density, data.density);
-    geometry.mergeVertices();
-    this.waves = [];
-    for (var v, i = 0, l = geometry.vertices.length; i < l; i++) {
-      v = geometry.vertices[i];
-      this.waves.push({
-        z: v.z,
-        ang: Math.random() * Math.PI * 2,
-        amp: data.amplitude + Math.random() * data.amplitudeVariance,
-        speed: (data.speed + Math.random() * data.speedVariance) / 1000 // radians / frame
-      });
-    }
+  updateVelocity: function (dt) {
+    var control, dVelocity,
+        velocity = this.velocity,
+        data = this.data;
 
-    if (!material) {
-      material = {};
-      material.material = new THREE.MeshPhongMaterial({
-        color: data.color,
-        transparent: data.opacity < 1,
-        opacity: data.opacity,
-        shading: THREE.FlatShading,
-      });
+    if (data.movementEnabled) {
+      for (var i = 0, l = data.movementControls.length; i < l; i++) {
+        control = this.el.components[data.movementControls[i] + COMPONENT_SUFFIX];
+        if (control && control.isVelocityActive()) {
+          if (control.getVelocityDelta) {
+            dVelocity = control.getVelocityDelta(dt);
+          } else if (control.getVelocity) {
+            this.el.setAttribute('velocity', control.getVelocity());
+            return;
+          } else if (control.getPositionDelta) {
+            velocity.copy(control.getPositionDelta(dt).multiplyScalar(1000 / dt));
+            this.el.setAttribute('velocity', velocity);
+            return;
+          } else {
+            throw new Error('Incompatible movement controls: ', data.movementControls[i]);
+          }
+          break;
+        }
+      }
     }
 
-    this.mesh = new THREE.Mesh(geometry, material.material);
-    el.setObject3D('mesh', this.mesh);
-  },
+    velocity.copy(this.el.getAttribute('velocity'));
+    velocity.x -= velocity.x * data.movementEasing * dt / 1000;
+    velocity.y -= velocity.y * data.movementEasingY * dt / 1000;
+    velocity.z -= velocity.z * data.movementEasing * dt / 1000;
 
-  remove: function () {
-    this.el.removeObject3D('mesh');
-  },
+    if (dVelocity && data.movementEnabled) {
+      // Set acceleration
+      if (dVelocity.length() > 1) {
+        dVelocity.setLength(this.data.movementAcceleration * dt / 1000);
+      } else {
+        dVelocity.multiplyScalar(this.data.movementAcceleration * dt / 1000);
+      }
 
-  tick: function (t, dt) {
-    if (!dt) return;
+      // Rotate to heading
+      var rotation = this.el.getAttribute('rotation');
+      if (rotation) {
+        this.heading.set(
+          data.fly ? THREE.Math.degToRad(rotation.x) : 0,
+          THREE.Math.degToRad(rotation.y),
+          0
+        );
+        dVelocity.applyEuler(this.heading);
+      }
 
-    var verts = this.mesh.geometry.vertices;
-    for (var v, vprops, i = 0; (v = verts[i]); i++){
-      vprops = this.waves[i];
-      v.z = vprops.z + Math.sin(vprops.ang) * vprops.amp;
-      vprops.ang += vprops.speed * dt;
+      velocity.add(dVelocity);
+
+      // TODO - Several issues here:
+      // (1) Interferes w/ gravity.
+      // (2) Interferes w/ jumping.
+      // (3) Likely to interfere w/ relative position to moving platform.
+      // if (velocity.length() > data.movementSpeed) {
+      //   velocity.setLength(data.movementSpeed);
+      // }
     }
-    this.mesh.geometry.verticesNeedUpdate = true;
+
+    this.el.setAttribute('velocity', velocity);
   }
 };
 
-module.exports.registerAll = (function () {
-  var registered = false;
-  return function (AFRAME) {
-    if (registered) return;
-    AFRAME = AFRAME || window.AFRAME;
-    AFRAME.registerComponent('ocean', Component);
-    AFRAME.registerPrimitive('a-ocean', Primitive);
-    registered = true;
-  };
-}());
+},{}],94:[function(require,module,exports){
+var LoopMode = {
+  once: THREE.LoopOnce,
+  repeat: THREE.LoopRepeat,
+  pingpong: THREE.LoopPingPong
+};
 
-},{}],114:[function(require,module,exports){
 /**
- * Tube following a custom path.
- *
- * Usage:
+ * animation-mixer
  *
- * ```html
- * <a-tube path="5 0 5, 5 0 -5, -5 0 -5" radius="0.5"></a-tube>
- * ```
+ * Player for animation clips. Intended to be compatible with any model format that supports
+ * skeletal or morph animations through THREE.AnimationMixer.
+ * See: https://threejs.org/docs/?q=animation#Reference/Animation/AnimationMixer
  */
-var Primitive = module.exports.Primitive = {
-  defaultComponents: {
-    tube:           {},
-  },
-  mappings: {
-    path:           'tube.path',
-    segments:       'tube.segments',
-    radius:         'tube.radius',
-    radialSegments: 'tube.radialSegments',
-    closed:         'tube.closed'
-  }
-};
-
-var Component = module.exports.Component = {
+module.exports = {
   schema: {
-    path:           {default: []},
-    segments:       {default: 64},
-    radius:         {default: 1},
-    radialSegments: {default: 8},
-    closed:         {default: false}
+    clip:  {default: '*'},
+    duration: {default: 0},
+    crossFadeDuration: {default: 0},
+    loop: {default: 'repeat', oneOf: Object.keys(LoopMode)},
+    repetitions: {default: Infinity, min: 0}
   },
 
   init: function () {
-    var el = this.el,
-        data = this.data,
-        material = el.components.material;
-
-    if (!data.path.length) {
-      console.error('[a-tube] `path` property expected but not found.');
-      return;
-    }
+    /** @type {THREE.Mesh} */
+    this.model = null;
+    /** @type {THREE.AnimationMixer} */
+    this.mixer = null;
+    /** @type {Array<THREE.AnimationAction>} */
+    this.activeActions = [];
 
-    var curve = new THREE.CatmullRomCurve3(data.path.map(function (point) {
-      point = point.split(' ');
-      return new THREE.Vector3(Number(point[0]), Number(point[1]), Number(point[2]));
-    }));
-    var geometry = new THREE.TubeGeometry(
-      curve, data.segments, data.radius, data.radialSegments, data.closed
-    );
+    var model = this.el.getObject3D('mesh');
 
-    if (!material) {
-      material = {};
-      material.material = new THREE.MeshPhongMaterial();
+    if (model) {
+      this.load(model);
+    } else {
+      this.el.addEventListener('model-loaded', function(e) {
+        this.load(e.detail.model);
+      }.bind(this));
     }
-
-    this.mesh = new THREE.Mesh(geometry, material.material);
-    this.el.setObject3D('mesh', this.mesh);
   },
 
-  remove: function () {
-    if (this.mesh) this.el.removeObject3D('mesh');
-  }
-};
-
-module.exports.registerAll = (function () {
-  var registered = false;
-  return function (AFRAME) {
-    if (registered) return;
-    AFRAME = AFRAME || window.AFRAME;
-    AFRAME.registerComponent('tube', Component);
-    AFRAME.registerPrimitive('a-tube', Primitive);
-    registered = true;
-  };
-}());
-
-},{}],115:[function(require,module,exports){
-module.exports = {
-  'a-grid':     require('./a-grid'),
-  'a-hexgrid': require('./a-hexgrid'),
-  'a-ocean':    require('./a-ocean'),
-  'a-tube':     require('./a-tube'),
-
-  registerAll: function (AFRAME) {
-    if (this._registered) return;
-    AFRAME = AFRAME || window.AFRAME;
-    this['a-grid'].registerAll(AFRAME);
-    this['a-hexgrid'].registerAll(AFRAME);
-    this['a-ocean'].registerAll(AFRAME);
-    this['a-tube'].registerAll(AFRAME);
-    this._registered = true;
-  }
-};
-
-},{"./a-grid":111,"./a-hexgrid":112,"./a-ocean":113,"./a-tube":114}],116:[function(require,module,exports){
-const BinaryHeap = require('./BinaryHeap');
-const utils = require('./utils.js');
-
-class AStar {
-  static init (graph) {
-    for (let x = 0; x < graph.length; x++) {
-      //for(var x in graph) {
-      const node = graph[x];
-      node.f = 0;
-      node.g = 0;
-      node.h = 0;
-      node.cost = 1.0;
-      node.visited = false;
-      node.closed = false;
-      node.parent = null;
-    }
-  }
-
-  static cleanUp (graph) {
-    for (let x = 0; x < graph.length; x++) {
-      const node = graph[x];
-      delete node.f;
-      delete node.g;
-      delete node.h;
-      delete node.cost;
-      delete node.visited;
-      delete node.closed;
-      delete node.parent;
-    }
-  }
+  load: function (model) {
+    var el = this.el;
+    this.model = model;
+    this.mixer = new THREE.AnimationMixer(model);
+    this.mixer.addEventListener('loop', function (e) {
+      el.emit('animation-loop', {action: e.action, loopDelta: e.loopDelta});
+    }.bind(this));
+    this.mixer.addEventListener('finished', function (e) {
+      el.emit('animation-finished', {action: e.action, direction: e.direction});
+    }.bind(this));
+    if (this.data.clip) this.update({});
+  },
 
-  static heap () {
-    return new BinaryHeap(function (node) {
-      return node.f;
-    });
-  }
+  remove: function () {
+    if (this.mixer) this.mixer.stopAllAction();
+  },
 
-  static search (graph, start, end) {
-    this.init(graph);
-    //heuristic = heuristic || astar.manhattan;
+  update: function (previousData) {
+    if (!previousData) return;
 
+    this.stopAction();
 
-    const openHeap = this.heap();
+    if (this.data.clip) {
+      this.playAction();
+    }
+  },
 
-    openHeap.push(start);
+  stopAction: function () {
+    var data = this.data;
+    for (var i = 0; i < this.activeActions.length; i++) {
+      data.crossFadeDuration
+        ? this.activeActions[i].fadeOut(data.crossFadeDuration)
+        : this.activeActions[i].stop();
+    }
+    this.activeActions.length = 0;
+  },
 
-    while (openHeap.size() > 0) {
+  playAction: function () {
+    if (!this.mixer) return;
 
-      // Grab the lowest f(x) to process next.  Heap keeps this sorted for us.
-      const currentNode = openHeap.pop();
+    var model = this.model,
+        data = this.data,
+        clips = model.animations || (model.geometry || {}).animations || [];
 
-      // End case -- result has been found, return the traced path.
-      if (currentNode === end) {
-        let curr = currentNode;
-        const ret = [];
-        while (curr.parent) {
-          ret.push(curr);
-          curr = curr.parent;
-        }
-        this.cleanUp(ret);
-        return ret.reverse();
+    if (!clips.length) return;
+
+    var re = wildcardToRegExp(data.clip);
+
+    for (var clip, i = 0; (clip = clips[i]); i++) {
+      if (clip.name.match(re)) {
+        var action = this.mixer.clipAction(clip, model);
+        action.enabled = true;
+        if (data.duration) action.setDuration(data.duration);
+        action
+          .setLoop(LoopMode[data.loop], data.repetitions)
+          .fadeIn(data.crossFadeDuration)
+          .play();
+        this.activeActions.push(action);
       }
+    }
+  },
 
-      // Normal case -- move currentNode from open to closed, process each of its neighbours.
-      currentNode.closed = true;
+  tick: function (t, dt) {
+    if (this.mixer && !isNaN(dt)) this.mixer.update(dt / 1000);
+  }
+};
 
-      // Find all neighbours for the current node. Optionally find diagonal neighbours as well (false by default).
-      const neighbours = this.neighbours(graph, currentNode);
+/**
+ * Creates a RegExp from the given string, converting asterisks to .* expressions,
+ * and escaping all other characters.
+ */
+function wildcardToRegExp (s) {
+  return new RegExp('^' + s.split(/\*+/).map(regExpEscape).join('.*') + '$');
+}
 
-      for (let i = 0, il = neighbours.length; i < il; i++) {
-        const neighbour = neighbours[i];
+/**
+ * RegExp-escapes all characters in the given string.
+ */
+function regExpEscape (s) {
+  return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
+}
 
-        if (neighbour.closed) {
-          // Not a valid node to process, skip to next neighbour.
-          continue;
-        }
+},{}],95:[function(require,module,exports){
+THREE.FBXLoader = require('../../lib/FBXLoader');
 
-        // The g score is the shortest distance from start to current node.
-        // We need to check if the path we have arrived at this neighbour is the shortest one we have seen yet.
-        const gScore = currentNode.g + neighbour.cost;
-        const beenVisited = neighbour.visited;
+/**
+ * fbx-model
+ *
+ * Loader for FBX format. Supports ASCII, but *not* binary, models.
+ */
+module.exports = {
+  schema: {
+    src:         { type: 'asset' },
+    crossorigin: { default: '' }
+  },
 
-        if (!beenVisited || gScore < neighbour.g) {
+  init: function () {
+    this.model = null;
+  },
 
-          // Found an optimal (so far) path to this node.  Take score for node to see how good it is.
-          neighbour.visited = true;
-          neighbour.parent = currentNode;
-          if (!neighbour.centroid || !end.centroid) throw new Error('Unexpected state');
-          neighbour.h = neighbour.h || this.heuristic(neighbour.centroid, end.centroid);
-          neighbour.g = gScore;
-          neighbour.f = neighbour.g + neighbour.h;
+  update: function () {
+    var loader,
+        data = this.data;
+    if (!data.src) return;
 
-          if (!beenVisited) {
-            // Pushing to heap will put it in proper place based on the 'f' value.
-            openHeap.push(neighbour);
-          } else {
-            // Already seen the node, but since it has been rescored we need to reorder it in the heap
-            openHeap.rescoreElement(neighbour);
-          }
-        }
-      }
-    }
+    this.remove();
+    loader = new THREE.FBXLoader();
+    if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
+    loader.load(data.src, this.load.bind(this));
+  },
 
-    // No result was found - empty array signifies failure to find path.
-    return [];
-  }
+  load: function (model) {
+    this.model = model;
+    this.el.setObject3D('mesh', model);
+    this.el.emit('model-loaded', {format: 'fbx', model: model});
+  },
 
-  static heuristic (pos1, pos2) {
-    return utils.distanceToSquared(pos1, pos2);
+  remove: function () {
+    if (this.model) this.el.removeObject3D('mesh');
   }
+};
 
-  static neighbours (graph, node) {
-    const ret = [];
+},{"../../lib/FBXLoader":3}],96:[function(require,module,exports){
+var fetchScript = require('../../lib/fetch-script')();
 
-    for (let e = 0; e < node.neighbours.length; e++) {
-      ret.push(graph[node.neighbours[e]]);
-    }
+var LOADER_SRC = 'https://rawgit.com/mrdoob/three.js/r86/examples/js/loaders/GLTFLoader.js';
 
-    return ret;
-  }
-}
+/**
+ * Legacy loader for glTF 1.0 models.
+ * Asynchronously loads THREE.GLTFLoader from rawgit.
+ */
+module.exports = {
+  schema: {type: 'model'},
 
-module.exports = AStar;
+  init: function () {
+    this.model = null;
+    this.loader = null;
+    this.loaderPromise = loadLoader().then(function () {
+      this.loader = new THREE.GLTFLoader();
+      this.loader.setCrossOrigin('Anonymous');
+    }.bind(this));
+  },
 
-},{"./BinaryHeap":117,"./utils.js":120}],117:[function(require,module,exports){
-// javascript-astar
-// http://github.com/bgrins/javascript-astar
-// Freely distributable under the MIT License.
-// Implements the astar search algorithm in javascript using a binary heap.
+  update: function () {
+    var self = this;
+    var el = this.el;
+    var src = this.data;
 
-class BinaryHeap {
-  constructor (scoreFunction) {
-    this.content = [];
-    this.scoreFunction = scoreFunction;
-  }
+    if (!src) { return; }
 
-  push (element) {
-    // Add the new element to the end of the array.
-    this.content.push(element);
+    this.remove();
 
-    // Allow it to sink down.
-    this.sinkDown(this.content.length - 1);
-  }
+    this.loaderPromise.then(function () {
+      this.loader.load(src, function gltfLoaded (gltfModel) {
+        self.model = gltfModel.scene;
+        self.model.animations = gltfModel.animations;
+        el.setObject3D('mesh', self.model);
+        el.emit('model-loaded', {format: 'gltf', model: self.model});
+      });
+    }.bind(this));
+  },
 
-  pop () {
-    // Store the first element so we can return it later.
-    const result = this.content[0];
-    // Get the element at the end of the array.
-    const end = this.content.pop();
-    // If there are any elements left, put the end element at the
-    // start, and let it bubble up.
-    if (this.content.length > 0) {
-      this.content[0] = end;
-      this.bubbleUp(0);
-    }
-    return result;
+  remove: function () {
+    if (!this.model) { return; }
+    this.el.removeObject3D('mesh');
   }
+};
 
-  remove (node) {
-    const i = this.content.indexOf(node);
+var loadLoader = (function () {
+  var promise;
+  return function () {
+    promise = promise || fetchScript(LOADER_SRC);
+    return promise;
+  };
+}());
 
-    // When it is found, the process seen in 'pop' is repeated
-    // to fill up the hole.
-    const end = this.content.pop();
+},{"../../lib/fetch-script":8}],97:[function(require,module,exports){
+module.exports = {
+  'animation-mixer': require('./animation-mixer'),
+  'fbx-model': require('./fbx-model'),
+  'gltf-model-legacy': require('./gltf-model-legacy'),
+  'json-model': require('./json-model'),
+  'object-model': require('./object-model'),
+  'ply-model': require('./ply-model'),
+
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
+
+    AFRAME = AFRAME || window.AFRAME;
+
+    // THREE.AnimationMixer
+    if (!AFRAME.components['animation-mixer']) {
+      AFRAME.registerComponent('animation-mixer', this['animation-mixer']);
+    }
+
+    // THREE.PlyLoader
+    if (!AFRAME.systems['ply-model']) {
+      AFRAME.registerSystem('ply-model', this['ply-model'].System);
+    }
+    if (!AFRAME.components['ply-model']) {
+      AFRAME.registerComponent('ply-model', this['ply-model'].Component);
+    }
 
-    if (i !== this.content.length - 1) {
-      this.content[i] = end;
+    // THREE.FBXLoader
+    if (!AFRAME.components['fbx-model']) {
+      AFRAME.registerComponent('fbx-model', this['fbx-model']);
+    }
 
-      if (this.scoreFunction(end) < this.scoreFunction(node)) {
-        this.sinkDown(i);
-      } else {
-        this.bubbleUp(i);
-      }
+    // THREE.GLTFLoader
+    if (!AFRAME.components['gltf-model-legacy']) {
+      AFRAME.registerComponent('gltf-model-legacy', this['gltf-model-legacy']);
     }
-  }
 
-  size () {
-    return this.content.length;
-  }
+    // THREE.JsonLoader
+    if (!AFRAME.components['json-model']) {
+      AFRAME.registerComponent('json-model', this['json-model']);
+    }
 
-  rescoreElement (node) {
-    this.sinkDown(this.content.indexOf(node));
+    // THREE.ObjectLoader
+    if (!AFRAME.components['object-model']) {
+      AFRAME.registerComponent('object-model', this['object-model']);
+    }
+
+    this._registered = true;
   }
+};
 
-  sinkDown (n) {
-    // Fetch the element that has to be sunk.
-    const element = this.content[n];
+},{"./animation-mixer":94,"./fbx-model":95,"./gltf-model-legacy":96,"./json-model":98,"./object-model":99,"./ply-model":100}],98:[function(require,module,exports){
+/**
+ * json-model
+ *
+ * Loader for THREE.js JSON format. Somewhat confusingly, there are two different THREE.js formats,
+ * both having the .json extension. This loader supports only THREE.JsonLoader, which typically
+ * includes only a single mesh.
+ *
+ * Check the console for errors, if in doubt. You may need to use `object-model` or
+ * `blend-character-model` for some .js and .json files.
+ *
+ * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
+ */
+module.exports = {
+  schema: {
+    src:         { type: 'asset' },
+    crossorigin: { default: '' }
+  },
 
-    // When at 0, an element can not sink any further.
-    while (n > 0) {
-      // Compute the parent element's index, and fetch it.
-      const parentN = ((n + 1) >> 1) - 1;
-      const parent = this.content[parentN];
+  init: function () {
+    this.model = null;
+  },
 
-      if (this.scoreFunction(element) < this.scoreFunction(parent)) {
-        // Swap the elements if the parent is greater.
-        this.content[parentN] = element;
-        this.content[n] = parent;
-        // Update 'n' to continue at the new position.
-        n = parentN;
-      } else {
-        // Found a parent that is less, no need to sink any further.
-        break;
-      }
-    }
-  }
+  update: function () {
+    var loader,
+        data = this.data;
+    if (!data.src) return;
 
-  bubbleUp (n) {
-    // Look up the target element and its score.
-    const length = this.content.length,
-      element = this.content[n],
-      elemScore = this.scoreFunction(element);
+    this.remove();
+    loader = new THREE.JSONLoader();
+    if (data.crossorigin) loader.crossOrigin = data.crossorigin;
+    loader.load(data.src, function (geometry, materials) {
 
-    while (true) {
-      // Compute the indices of the child elements.
-      const child2N = (n + 1) << 1,
-        child1N = child2N - 1;
-      // This is used to store the new position of the element,
-      // if any.
-      let swap = null;
-      let child1Score;
-      // If the first child exists (is inside the array)...
-      if (child1N < length) {
-        // Look it up and compute its score.
-        const child1 = this.content[child1N];
-        child1Score = this.scoreFunction(child1);
+      // Attempt to automatically detect common material options.
+      materials.forEach(function (mat) {
+        mat.vertexColors = (geometry.faces[0] || {}).color ? THREE.FaceColors : THREE.NoColors;
+        mat.skinning = !!(geometry.bones || []).length;
+        mat.morphTargets = !!(geometry.morphTargets || []).length;
+        mat.morphNormals = !!(geometry.morphNormals || []).length;
+      });
 
-        // If the score is less than our element's, we need to swap.
-        if (child1Score < elemScore) {
-          swap = child1N;
-        }
-      }
+      var model = (geometry.bones || []).length
+        ? new THREE.SkinnedMesh(geometry, new THREE.MultiMaterial(materials))
+        : new THREE.Mesh(geometry, new THREE.MultiMaterial(materials));
 
-      // Do the same checks for the other child.
-      if (child2N < length) {
-        const child2 = this.content[child2N],
-          child2Score = this.scoreFunction(child2);
-        if (child2Score < (swap === null ? elemScore : child1Score)) {
-          swap = child2N;
-        }
-      }
+      this.load(model);
+    }.bind(this));
+  },
 
-      // If the element needs to be moved, swap it, and continue.
-      if (swap !== null) {
-        this.content[n] = this.content[swap];
-        this.content[swap] = element;
-        n = swap;
-      }
+  load: function (model) {
+    this.model = model;
+    this.el.setObject3D('mesh', model);
+    this.el.emit('model-loaded', {format: 'json', model: model});
+  },
 
-      // Otherwise, we are done.
-      else {
-        break;
-      }
-    }
+  remove: function () {
+    if (this.model) this.el.removeObject3D('mesh');
   }
+};
 
-}
+},{}],99:[function(require,module,exports){
+/**
+ * object-model
+ *
+ * Loader for THREE.js JSON format. Somewhat confusingly, there are two different THREE.js formats,
+ * both having the .json extension. This loader supports only THREE.ObjectLoader, which typically
+ * includes multiple meshes or an entire scene.
+ *
+ * Check the console for errors, if in doubt. You may need to use `json-model` or
+ * `blend-character-model` for some .js and .json files.
+ *
+ * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
+ */
+module.exports = {
+  schema: {
+    src:         { type: 'asset' },
+    crossorigin: { default: '' }
+  },
 
-module.exports = BinaryHeap;
+  init: function () {
+    this.model = null;
+  },
 
-},{}],118:[function(require,module,exports){
-const utils = require('./utils');
+  update: function () {
+    var loader,
+        data = this.data;
+    if (!data.src) return;
 
-class Channel {
-  constructor () {
-    this.portals = [];
-  }
+    this.remove();
+    loader = new THREE.ObjectLoader();
+    if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
+    loader.load(data.src, function(object) {
 
-  push (p1, p2) {
-    if (p2 === undefined) p2 = p1;
-    this.portals.push({
-      left: p1,
-      right: p2
-    });
-  }
+      // Enable skinning, if applicable.
+      object.traverse(function(o) {
+        if (o instanceof THREE.SkinnedMesh && o.material) {
+          o.material.skinning = !!((o.geometry && o.geometry.bones) || []).length;
+        }
+      });
 
-  stringPull () {
-    const portals = this.portals;
-    const pts = [];
-    // Init scan state
-    let portalApex, portalLeft, portalRight;
-    let apexIndex = 0,
-      leftIndex = 0,
-      rightIndex = 0;
+      this.load(object);
+    }.bind(this));
+  },
 
-    portalApex = portals[0].left;
-    portalLeft = portals[0].left;
-    portalRight = portals[0].right;
+  load: function (model) {
+    this.model = model;
+    this.el.setObject3D('mesh', model);
+    this.el.emit('model-loaded', {format: 'json', model: model});
+  },
 
-    // Add start point.
-    pts.push(portalApex);
+  remove: function () {
+    if (this.model) this.el.removeObject3D('mesh');
+  }
+};
 
-    for (let i = 1; i < portals.length; i++) {
-      const left = portals[i].left;
-      const right = portals[i].right;
+},{}],100:[function(require,module,exports){
+/**
+ * ply-model
+ *
+ * Wraps THREE.PLYLoader.
+ */
+THREE.PLYLoader = require('../../lib/PLYLoader');
 
-      // Update right vertex.
-      if (utils.triarea2(portalApex, portalRight, right) <= 0.0) {
-        if (utils.vequal(portalApex, portalRight) || utils.triarea2(portalApex, portalLeft, right) > 0.0) {
-          // Tighten the funnel.
-          portalRight = right;
-          rightIndex = i;
-        } else {
-          // Right over left, insert left to path and restart scan from portal left point.
-          pts.push(portalLeft);
-          // Make current left the new apex.
-          portalApex = portalLeft;
-          apexIndex = leftIndex;
-          // Reset portal
-          portalLeft = portalApex;
-          portalRight = portalApex;
-          leftIndex = apexIndex;
-          rightIndex = apexIndex;
-          // Restart scan
-          i = apexIndex;
-          continue;
-        }
-      }
+/**
+ * Loads, caches, resolves geometries.
+ *
+ * @member cache - Promises that resolve geometries keyed by `src`.
+ */
+module.exports.System = {
+  init: function () {
+    this.cache = {};
+  },
 
-      // Update left vertex.
-      if (utils.triarea2(portalApex, portalLeft, left) >= 0.0) {
-        if (utils.vequal(portalApex, portalLeft) || utils.triarea2(portalApex, portalRight, left) < 0.0) {
-          // Tighten the funnel.
-          portalLeft = left;
-          leftIndex = i;
-        } else {
-          // Left over right, insert right to path and restart scan from portal right point.
-          pts.push(portalRight);
-          // Make current right the new apex.
-          portalApex = portalRight;
-          apexIndex = rightIndex;
-          // Reset portal
-          portalLeft = portalApex;
-          portalRight = portalApex;
-          leftIndex = apexIndex;
-          rightIndex = apexIndex;
-          // Restart scan
-          i = apexIndex;
-          continue;
-        }
-      }
-    }
+  /**
+   * @returns {Promise}
+   */
+  getOrLoadGeometry: function (src, skipCache) {
+    var cache = this.cache;
+    var cacheItem = cache[src];
 
-    if ((pts.length === 0) || (!utils.vequal(pts[pts.length - 1], portals[portals.length - 1].left))) {
-      // Append last point to path.
-      pts.push(portals[portals.length - 1].left);
+    if (!skipCache && cacheItem) {
+      return cacheItem;
     }
 
-    this.path = pts;
-    return pts;
-  }
-}
+    cache[src] = new Promise(function (resolve) {
+      var loader = new THREE.PLYLoader();
+      loader.load(src, function (geometry) {
+        resolve(geometry);
+      });
+    });
+    return cache[src];
+  },
+};
 
-module.exports = Channel;
+module.exports.Component = {
+  schema: {
+    skipCache: {type: 'boolean', default: false},
+    src: {type: 'asset'}
+  },
 
-},{"./utils":120}],119:[function(require,module,exports){
-const utils = require('./utils');
-const AStar = require('./AStar');
-const Channel = require('./Channel');
+  init: function () {
+    this.model = null;
+  },
 
-var polygonId = 1;
+  update: function () {
+    var data = this.data;
+    var el = this.el;
+    var loader;
 
-var buildPolygonGroups = function (navigationMesh) {
+    if (!data.src) {
+      console.warn('[%s] `src` property is required.', this.name);
+      return;
+    }
 
-	var polygons = navigationMesh.polygons;
+    // Get geometry from system, create and set mesh.
+    this.system.getOrLoadGeometry(data.src, data.skipCache).then(function (geometry) {
+      var model = createModel(geometry);
+      el.setObject3D('mesh', model);
+      el.emit('model-loaded', {format: 'ply', model: model});
+    });
+  },
 
-	var polygonGroups = [];
-	var groupCount = 0;
+  remove: function () {
+    if (this.model) { this.el.removeObject3D('mesh'); }
+  }
+};
 
-	var spreadGroupId = function (polygon) {
-		polygon.neighbours.forEach((neighbour) => {
-			if (neighbour.group === undefined) {
-				neighbour.group = polygon.group;
-				spreadGroupId(neighbour);
-			}
-		});
-	};
+function createModel (geometry) {
+  return new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({
+    color: 0xFFFFFF,
+    shading: THREE.FlatShading,
+    vertexColors: THREE.VertexColors,
+    shininess: 0
+  }));
+}
 
-	polygons.forEach((polygon) => {
+},{"../../lib/PLYLoader":6}],101:[function(require,module,exports){
+module.exports = {
+  schema: {
+    offset: {default: {x: 0, y: 0, z: 0}, type: 'vec3'}
+  },
 
-		if (polygon.group === undefined) {
-			polygon.group = groupCount++;
-			// Spread it
-			spreadGroupId(polygon);
-		}
+  init: function () {
+    this.active = false;
+    this.targetEl = null;
+    this.fire = this.fire.bind(this);
+    this.offset = new THREE.Vector3();
+  },
 
-		if (!polygonGroups[polygon.group]) polygonGroups[polygon.group] = [];
+  update: function () {
+    this.offset.copy(this.data.offset);
+  },
 
-		polygonGroups[polygon.group].push(polygon);
-	});
+  play: function () { this.el.addEventListener('click', this.fire); },
+  pause: function () { this.el.removeEventListener('click', this.fire); },
+  remove: function () { this.pause(); },
 
-	console.log('Groups built: ', polygonGroups.length);
+  fire: function () {
+    var targetEl = this.el.sceneEl.querySelector('[checkpoint-controls]');
+    if (!targetEl) {
+      throw new Error('No `checkpoint-controls` component found.');
+    }
+    targetEl.components['checkpoint-controls'].setCheckpoint(this.el);
+  },
 
-	return polygonGroups;
+  getOffset: function () {
+    return this.offset.copy(this.data.offset);
+  }
 };
 
-var buildPolygonNeighbours = function (polygon, navigationMesh) {
-	polygon.neighbours = [];
+},{}],102:[function(require,module,exports){
+/**
+ * Specifies an envMap on an entity, without replacing any existing material
+ * properties.
+ */
+module.exports = {
+  schema: {
+    path: {default: ''},
+    extension: {default: 'jpg'},
+    format: {default: 'RGBFormat'},
+    enableBackground: {default: false}
+  },
 
-	// All other nodes that contain at least two of our vertices are our neighbours
-	for (var i = 0, len = navigationMesh.polygons.length; i < len; i++) {
-		if (polygon === navigationMesh.polygons[i]) continue;
+  init: function () {
+    var data = this.data;
 
-		// Don't check polygons that are too far, since the intersection tests take a long time
-		if (polygon.centroid.distanceToSquared(navigationMesh.polygons[i].centroid) > 100 * 100) continue;
+    this.texture = new THREE.CubeTextureLoader().load([
+      data.path + 'posx.' + data.extension, data.path + 'negx.' + data.extension,
+      data.path + 'posy.' + data.extension, data.path + 'negy.' + data.extension,
+      data.path + 'posz.' + data.extension, data.path + 'negz.' + data.extension
+    ]);
+    this.texture.format = THREE[data.format];
 
-		var matches = utils.array_intersect(polygon.vertexIds, navigationMesh.polygons[i].vertexIds);
+    if (data.enableBackground) {
+      this.el.sceneEl.object3D.background = this.texture;
+    }
 
-		if (matches.length >= 2) {
-			polygon.neighbours.push(navigationMesh.polygons[i]);
-		}
-	}
+    this.applyEnvMap();
+    this.el.addEventListener('object3dset', this.applyEnvMap.bind(this));
+  },
+
+  applyEnvMap: function () {
+    var mesh = this.el.getObject3D('mesh');
+    var envMap = this.texture;
+
+    if (!mesh) return;
+
+    mesh.traverse(function (node) {
+      if (node.material && 'envMap' in node.material) {
+        node.material.envMap = envMap;
+        node.material.needsUpdate = true;
+      }
+    });
+  }
 };
 
-var buildPolygonsFromGeometry = function (geometry) {
+},{}],103:[function(require,module,exports){
+/**
+ * Based on aframe/examples/showcase/tracked-controls.
+ *
+ * Handles events coming from the hand-controls.
+ * Determines if the entity is grabbed or released.
+ * Updates its position to move along the controller.
+ */
+module.exports = {
+  init: function () {
+    this.GRABBED_STATE = 'grabbed';
 
-	console.log('Vertices:', geometry.vertices.length, 'polygons:', geometry.faces.length);
+    this.grabbing = false;
+    this.hitEl =      /** @type {AFRAME.Element}    */ null;
+    this.physics =    /** @type {AFRAME.System}     */ this.el.sceneEl.systems.physics;
+    this.constraint = /** @type {CANNON.Constraint} */ null;
 
-	var polygons = [];
-	var vertices = geometry.vertices;
-	var faceVertexUvs = geometry.faceVertexUvs;
+    // Bind event handlers
+    this.onHit = this.onHit.bind(this);
+    this.onGripOpen = this.onGripOpen.bind(this);
+    this.onGripClose = this.onGripClose.bind(this);
+  },
 
-	// Convert the faces into a custom format that supports more than 3 vertices
-	geometry.faces.forEach((face) => {
-		polygons.push({
-			id: polygonId++,
-			vertexIds: [face.a, face.b, face.c],
-			centroid: face.centroid,
-			normal: face.normal,
-			neighbours: []
-		});
-	});
+  play: function () {
+    var el = this.el;
+    el.addEventListener('hit', this.onHit);
+    el.addEventListener('gripdown', this.onGripClose);
+    el.addEventListener('gripup', this.onGripOpen);
+    el.addEventListener('trackpaddown', this.onGripClose);
+    el.addEventListener('trackpadup', this.onGripOpen);
+    el.addEventListener('triggerdown', this.onGripClose);
+    el.addEventListener('triggerup', this.onGripOpen);
+  },
 
-	var navigationMesh = {
-		polygons: polygons,
-		vertices: vertices,
-		faceVertexUvs: faceVertexUvs
-	};
+  pause: function () {
+    var el = this.el;
+    el.removeEventListener('hit', this.onHit);
+    el.removeEventListener('gripdown', this.onGripClose);
+    el.removeEventListener('gripup', this.onGripOpen);
+    el.removeEventListener('trackpaddown', this.onGripClose);
+    el.removeEventListener('trackpadup', this.onGripOpen);
+    el.removeEventListener('triggerdown', this.onGripClose);
+    el.removeEventListener('triggerup', this.onGripOpen);
+  },
 
-	// Build a list of adjacent polygons
-	polygons.forEach((polygon) => {
-		buildPolygonNeighbours(polygon, navigationMesh);
-	});
+  onGripClose: function (evt) {
+    this.grabbing = true;
+  },
 
-	return navigationMesh;
-};
+  onGripOpen: function (evt) {
+    var hitEl = this.hitEl;
+    this.grabbing = false;
+    if (!hitEl) { return; }
+    hitEl.removeState(this.GRABBED_STATE);
+    this.hitEl = undefined;
+    this.physics.world.removeConstraint(this.constraint);
+    this.constraint = null;
+  },
 
-var buildNavigationMesh = function (geometry) {
-	// Prepare geometry
-	utils.computeCentroids(geometry);
-	geometry.mergeVertices();
-	return buildPolygonsFromGeometry(geometry);
+  onHit: function (evt) {
+    var hitEl = evt.detail.el;
+    // If the element is already grabbed (it could be grabbed by another controller).
+    // If the hand is not grabbing the element does not stick.
+    // If we're already grabbing something you can't grab again.
+    if (!hitEl || hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; }
+    hitEl.addState(this.GRABBED_STATE);
+    this.hitEl = hitEl;
+    this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body);
+    this.physics.world.addConstraint(this.constraint);
+  }
 };
 
-var getSharedVerticesInOrder = function (a, b) {
-
-	var aList = a.vertexIds;
-	var bList = b.vertexIds;
+},{}],104:[function(require,module,exports){
+var physics = require('aframe-physics-system');
 
-	var sharedVertices = [];
+module.exports = {
+  'checkpoint':      require('./checkpoint'),
+  'cube-env-map':    require('./cube-env-map'),
+  'grab':            require('./grab'),
+  'jump-ability':    require('./jump-ability'),
+  'kinematic-body':  require('./kinematic-body'),
+  'mesh-smooth':     require('./mesh-smooth'),
+  'sphere-collider': require('./sphere-collider'),
+  'toggle-velocity': require('./toggle-velocity'),
 
-	aList.forEach((vId) => {
-		if (bList.includes(vId)) {
-			sharedVertices.push(vId);
-		}
-	});
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
 
-	if (sharedVertices.length < 2) return [];
+    AFRAME = AFRAME || window.AFRAME;
 
-	// console.log("TRYING aList:", aList, ", bList:", bList, ", sharedVertices:", sharedVertices);
+    physics.registerAll();
+    if (!AFRAME.components['checkpoint'])      AFRAME.registerComponent('checkpoint',      this['checkpoint']);
+    if (!AFRAME.components['cube-env-map'])    AFRAME.registerComponent('cube-env-map',    this['cube-env-map']);
+    if (!AFRAME.components['grab'])            AFRAME.registerComponent('grab',            this['grab']);
+    if (!AFRAME.components['jump-ability'])    AFRAME.registerComponent('jump-ability',    this['jump-ability']);
+    if (!AFRAME.components['kinematic-body'])  AFRAME.registerComponent('kinematic-body',  this['kinematic-body']);
+    if (!AFRAME.components['mesh-smooth'])     AFRAME.registerComponent('mesh-smooth',     this['mesh-smooth']);
+    if (!AFRAME.components['sphere-collider']) AFRAME.registerComponent('sphere-collider', this['sphere-collider']);
+    if (!AFRAME.components['toggle-velocity']) AFRAME.registerComponent('toggle-velocity', this['toggle-velocity']);
 
-	if (sharedVertices.includes(aList[0]) && sharedVertices.includes(aList[aList.length - 1])) {
-		// Vertices on both edges are bad, so shift them once to the left
-		aList.push(aList.shift());
-	}
+    this._registered = true;
+  }
+};
 
-	if (sharedVertices.includes(bList[0]) && sharedVertices.includes(bList[bList.length - 1])) {
-		// Vertices on both edges are bad, so shift them once to the left
-		bList.push(bList.shift());
-	}
+},{"./checkpoint":101,"./cube-env-map":102,"./grab":103,"./jump-ability":105,"./kinematic-body":106,"./mesh-smooth":107,"./sphere-collider":108,"./toggle-velocity":109,"aframe-physics-system":11}],105:[function(require,module,exports){
+var ACCEL_G = -9.8, // m/s^2
+    EASING = -15; // m/s^2
 
-	// Again!
-	sharedVertices = [];
+/**
+ * Jump ability.
+ */
+module.exports = {
+  dependencies: ['velocity'],
 
-	aList.forEach((vId) => {
-		if (bList.includes(vId)) {
-			sharedVertices.push(vId);
-		}
-	});
+  /* Schema
+  ——————————————————————————————————————————————*/
 
-	return sharedVertices;
-};
+  schema: {
+    on: { default: 'keydown:Space gamepadbuttondown:0' },
+    playerHeight: { default: 1.764 },
+    maxJumps: { default: 1 },
+    distance: { default: 5 },
+    soundJump: { default: '' },
+    soundLand: { default: '' },
+    debug: { default: false }
+  },
 
-var groupNavMesh = function (navigationMesh) {
+  init: function () {
+    this.velocity = 0;
+    this.numJumps = 0;
 
-	var saveObj = {};
+    var beginJump = this.beginJump.bind(this),
+        events = this.data.on.split(' ');
+    this.bindings = {};
+    for (var i = 0; i <  events.length; i++) {
+      this.bindings[events[i]] = beginJump;
+      this.el.addEventListener(events[i], beginJump);
+    }
+    this.bindings.collide = this.onCollide.bind(this);
+    this.el.addEventListener('collide', this.bindings.collide);
+  },
 
-	navigationMesh.vertices.forEach((v) => {
-		v.x = utils.roundNumber(v.x, 2);
-		v.y = utils.roundNumber(v.y, 2);
-		v.z = utils.roundNumber(v.z, 2);
-	});
+  remove: function () {
+    for (var event in this.bindings) {
+      if (this.bindings.hasOwnProperty(event)) {
+        this.el.removeEventListener(event, this.bindings[event]);
+        delete this.bindings[event];
+      }
+    }
+    this.el.removeEventListener('collide', this.bindings.collide);
+    delete this.bindings.collide;
+  },
 
-	saveObj.vertices = navigationMesh.vertices;
+  beginJump: function () {
+    if (this.numJumps < this.data.maxJumps) {
+      var data = this.data,
+          initialVelocity = Math.sqrt(-2 * data.distance * (ACCEL_G + EASING)),
+          v = this.el.getAttribute('velocity');
+      this.el.setAttribute('velocity', {x: v.x, y: initialVelocity, z: v.z});
+      this.numJumps++;
+    }
+  },
 
-	var groups = buildPolygonGroups(navigationMesh);
+  onCollide: function () {
+    this.numJumps = 0;
+  }
+};
 
-	saveObj.groups = [];
+},{}],106:[function(require,module,exports){
+/**
+ * Kinematic body.
+ *
+ * Managed dynamic body, which moves but is not affected (directly) by the
+ * physics engine. This is not a true kinematic body, in the sense that we are
+ * letting the physics engine _compute_ collisions against it and selectively
+ * applying those collisions to the object. The physics engine does not decide
+ * the position/velocity/rotation of the element.
+ *
+ * Used for the camera object, because full physics simulation would create
+ * movement that feels unnatural to the player. Bipedal movement does not
+ * translate nicely to rigid body physics.
+ *
+ * See: http://www.learn-cocos2d.com/2013/08/physics-engine-platformer-terrible-idea/
+ * And: http://oxleygamedev.blogspot.com/2011/04/player-physics-part-2.html
+ */
+var CANNON = window.CANNON;
+var EPS = 0.000001;
 
-	var findPolygonIndex = function (group, p) {
-		for (var i = 0; i < group.length; i++) {
-			if (p === group[i]) return i;
-		}
-	};
+module.exports = {
+  dependencies: ['velocity'],
 
-	groups.forEach((group) => {
+  /*******************************************************************
+   * Schema
+   */
 
-		var newGroup = [];
+  schema: {
+    mass:           { default: 5 },
+    radius:         { default: 1.3 },
+    height:         { default: 1.764 },
+    linearDamping:  { default: 0.05 },
+    enableSlopes:   { default: true }
+  },
 
-		group.forEach((p) => {
+  /*******************************************************************
+   * Lifecycle
+   */
 
-			var neighbours = [];
+  init: function () {
+    this.system = this.el.sceneEl.systems.physics;
+    this.system.addBehavior(this, this.system.Phase.SIMULATE);
 
-			p.neighbours.forEach((n) => {
-				neighbours.push(findPolygonIndex(group, n));
-			});
+    var el = this.el,
+        data = this.data,
+        position = (new CANNON.Vec3()).copy(el.getAttribute('position'));
 
+    this.body = new CANNON.Body({
+      material: this.system.material,
+      position: position,
+      mass: data.mass,
+      linearDamping: data.linearDamping,
+      fixedRotation: true
+    });
+    this.body.addShape(
+      new CANNON.Sphere(data.radius),
+      new CANNON.Vec3(0, data.radius - data.height, 0)
+    );
 
-			// Build a portal list to each neighbour
-			var portals = [];
-			p.neighbours.forEach((n) => {
-				portals.push(getSharedVerticesInOrder(p, n));
-			});
+    this.body.el = this.el;
+    this.el.body = this.body;
+    this.system.addBody(this.body);
+  },
 
+  remove: function () {
+    this.system.removeBody(this.body);
+    this.system.removeBehavior(this, this.system.Phase.SIMULATE);
+    delete this.el.body;
+  },
 
-			p.centroid.x = utils.roundNumber(p.centroid.x, 2);
-			p.centroid.y = utils.roundNumber(p.centroid.y, 2);
-			p.centroid.z = utils.roundNumber(p.centroid.z, 2);
+  /*******************************************************************
+   * Tick
+   */
 
-			newGroup.push({
-				id: findPolygonIndex(group, p),
-				neighbours: neighbours,
-				vertexIds: p.vertexIds,
-				centroid: p.centroid,
-				portals: portals
-			});
+  /**
+   * Checks CANNON.World for collisions and attempts to apply them to the
+   * element automatically, in a player-friendly way.
+   *
+   * There's extra logic for horizontal surfaces here. The basic requirements:
+   * (1) Only apply gravity when not in contact with _any_ horizontal surface.
+   * (2) When moving, project the velocity against exactly one ground surface.
+   *     If in contact with two ground surfaces (e.g. ground + ramp), choose
+   *     the one that collides with current velocity, if any.
+   */
+  step: (function () {
+    var velocity = new THREE.Vector3(),
+        normalizedVelocity = new THREE.Vector3(),
+        currentSurfaceNormal = new THREE.Vector3(),
+        groundNormal = new THREE.Vector3();
 
-		});
+    return function (t, dt) {
+      if (!dt) return;
 
-		saveObj.groups.push(newGroup);
-	});
+      var body = this.body,
+          data = this.data,
+          didCollide = false,
+          height, groundHeight = -Infinity,
+          groundBody;
 
-	return saveObj;
-};
+      dt = Math.min(dt, this.system.data.maxInterval * 1000);
 
-var zoneNodes = {};
+      groundNormal.set(0, 0, 0);
+      velocity.copy(this.el.getAttribute('velocity'));
+      body.velocity.copy(velocity);
+      body.position.copy(this.el.getAttribute('position'));
 
-module.exports = {
-	buildNodes: function (geometry) {
-		var navigationMesh = buildNavigationMesh(geometry);
+      for (var i = 0, contact; (contact = this.system.world.contacts[i]); i++) {
+        // 1. Find any collisions involving this element. Get the contact
+        // normal, and make sure it's oriented _out_ of the other object and
+        // enabled (body.collisionReponse is true for both bodies)
+        if (!contact.enabled) { continue; }
+        if (body.id === contact.bi.id) {
+          contact.ni.negate(currentSurfaceNormal);
+        } else if (body.id === contact.bj.id) {
+          currentSurfaceNormal.copy(contact.ni);
+        } else {
+          continue;
+        }
 
-		var zoneNodes = groupNavMesh(navigationMesh);
+        didCollide = body.velocity.dot(currentSurfaceNormal) < -EPS;
+        if (didCollide && currentSurfaceNormal.y <= 0.5) {
+          // 2. If current trajectory attempts to move _through_ another
+          // object, project the velocity against the collision plane to
+          // prevent passing through.
+          velocity = velocity.projectOnPlane(currentSurfaceNormal);
+        } else if (currentSurfaceNormal.y > 0.5) {
+          // 3. If in contact with something roughly horizontal (+/- 45º) then
+          // consider that the current ground. Only the highest qualifying
+          // ground is retained.
+          height = body.id === contact.bi.id
+            ? Math.abs(contact.rj.y + contact.bj.position.y)
+            : Math.abs(contact.ri.y + contact.bi.position.y);
+          if (height > groundHeight) {
+            groundHeight = height;
+            groundNormal.copy(currentSurfaceNormal);
+            groundBody = body.id === contact.bi.id ? contact.bj : contact.bi;
+          }
+        }
+      }
 
-		return zoneNodes;
-	},
-	setZoneData: function (zone, data) {
-		zoneNodes[zone] = data;
-	},
-	getGroup: function (zone, position) {
+      normalizedVelocity.copy(velocity).normalize();
+      if (groundBody && normalizedVelocity.y < 0.5) {
+        if (!data.enableSlopes) {
+          groundNormal.set(0, 1, 0);
+        } else if (groundNormal.y < 1 - EPS) {
+          groundNormal.copy(this.raycastToGround(groundBody, groundNormal));
+        }
 
-		if (!zoneNodes[zone]) return null;
+        // 4. Project trajectory onto the top-most ground object, unless
+        // trajectory is > 45º.
+        velocity = velocity.projectOnPlane(groundNormal);
+      } else {
+        // 5. If not in contact with anything horizontal, apply world gravity.
+        // TODO - Why is the 4x scalar necessary.
+        velocity.add(this.system.world.gravity.scale(dt * 4.0 / 1000));
+      }
 
-		var closestNodeGroup = null;
+      // 6. If the ground surface has a velocity, apply it directly to current
+      // position, not velocity, to preserve relative velocity.
+      if (groundBody && groundBody.el && groundBody.el.components.velocity) {
+        var groundVelocity = groundBody.el.getAttribute('velocity');
+        body.position.copy({
+          x: body.position.x + groundVelocity.x * dt / 1000,
+          y: body.position.y + groundVelocity.y * dt / 1000,
+          z: body.position.z + groundVelocity.z * dt / 1000
+        });
+        this.el.setAttribute('position', body.position);
+      }
 
-		var distance = Math.pow(50, 2);
+      body.velocity.copy(velocity);
+      this.el.setAttribute('velocity', velocity);
+    };
+  }()),
 
-		zoneNodes[zone].groups.forEach((group, index) => {
-			group.forEach((node) => {
-				var measuredDistance = utils.distanceToSquared(node.centroid, position);
-				if (measuredDistance < distance) {
-					closestNodeGroup = index;
-					distance = measuredDistance;
-				}
-			});
-		});
+  /**
+   * When walking on complex surfaces (trimeshes, borders between two shapes),
+   * the collision normals returned for the player sphere can be very
+   * inconsistent. To address this, raycast straight down, find the collision
+   * normal, and return whichever normal is more vertical.
+   * @param  {CANNON.Body} groundBody
+   * @param  {CANNON.Vec3} groundNormal
+   * @return {CANNON.Vec3}
+   */
+  raycastToGround: function (groundBody, groundNormal) {
+    var ray,
+        hitNormal,
+        vFrom = this.body.position,
+        vTo = this.body.position.clone();
 
-		return closestNodeGroup;
-	},
-	getRandomNode: function (zone, group, nearPosition, nearRange) {
+    vTo.y -= this.data.height;
+    ray = new CANNON.Ray(vFrom, vTo);
+    ray._updateDirection(); // TODO - Report bug.
+    ray.intersectBody(groundBody);
 
-		if (!zoneNodes[zone]) return new THREE.Vector3();
+    if (!ray.hasHit) return groundNormal;
 
-		nearPosition = nearPosition || null;
-		nearRange = nearRange || 0;
+    // Compare ABS, in case we're projecting against the inside of the face.
+    hitNormal = ray.result.hitNormalWorld;
+    return Math.abs(hitNormal.y) > Math.abs(groundNormal.y) ? hitNormal : groundNormal;
+  }
+};
 
-		var candidates = [];
+},{}],107:[function(require,module,exports){
+/**
+ * Apply this component to models that looks "blocky", to have Three.js compute
+ * vertex normals on the fly for a "smoother" look.
+ */
+module.exports = {
+  init: function () {
+    this.el.addEventListener('model-loaded', function (e) {
+      e.detail.model.traverse(function (node) {
+        if (node.isMesh) node.geometry.computeVertexNormals();
+      });
+    })
+  }
+}
 
-		var polygons = zoneNodes[zone].groups[group];
+},{}],108:[function(require,module,exports){
+/**
+ * Based on aframe/examples/showcase/tracked-controls.
+ *
+ * Implement bounding sphere collision detection for entities with a mesh.
+ * Sets the specified state on the intersected entities.
+ *
+ * @property {string} objects - Selector of the entities to test for collision.
+ * @property {string} state - State to set on collided entities.
+ *
+ */
+module.exports = {
+  schema: {
+    objects: {default: ''},
+    state: {default: 'collided'},
+    radius: {default: 0.05},
+    watch: {default: true}
+  },
 
-		polygons.forEach((p) => {
-			if (nearPosition && nearRange) {
-				if (utils.distanceToSquared(nearPosition, p.centroid) < nearRange * nearRange) {
-					candidates.push(p.centroid);
-				}
-			} else {
-				candidates.push(p.centroid);
-			}
-		});
+  init: function () {
+    /** @type {MutationObserver} */
+    this.observer = null;
+    /** @type {Array<Element>} Elements to watch for collisions. */
+    this.els = [];
+    /** @type {Array<Element>} Elements currently in collision state. */
+    this.collisions = [];
 
-		return utils.sample(candidates) || new THREE.Vector3();
-	},
-	getClosestNode: function (position, zone, group, checkPolygon = false) {
-		const nodes = zoneNodes[zone].groups[group];
-		const vertices = zoneNodes[zone].vertices;
-		let closestNode = null;
-		let closestDistance = Infinity;
+    this.handleHit = this.handleHit.bind(this);
+    this.handleHitEnd = this.handleHitEnd.bind(this);
+  },
 
-		nodes.forEach((node) => {
-			const distance = utils.distanceToSquared(node.centroid, position);
-			if (distance < closestDistance
-					&& (!checkPolygon || utils.isVectorInPolygon(position, node, vertices))) {
-				closestNode = node;
-				closestDistance = distance;
-			}
-		});
+  remove: function () {
+    this.pause();
+  },
 
-		return closestNode;
-	},
-	findPath: function (startPosition, targetPosition, zone, group) {
-		const nodes = zoneNodes[zone].groups[group];
-		const vertices = zoneNodes[zone].vertices;
+  play: function () {
+    var sceneEl = this.el.sceneEl;
 
-		const closestNode = this.getClosestNode(startPosition, zone, group);
-		const farthestNode = this.getClosestNode(targetPosition, zone, group, true);
+    if (this.data.watch) {
+      this.observer = new MutationObserver(this.update.bind(this, null));
+      this.observer.observe(sceneEl, {childList: true, subtree: true});
+    }
+  },
 
-		// If we can't find any node, just go straight to the target
-		if (!closestNode || !farthestNode) {
-			return null;
-		}
+  pause: function () {
+    if (this.observer) {
+      this.observer.disconnect();
+      this.observer = null;
+    }
+  },
 
-		const paths = AStar.search(nodes, closestNode, farthestNode);
+  /**
+   * Update list of entities to test for collision.
+   */
+  update: function () {
+    var data = this.data;
+    var objectEls;
 
-		const getPortalFromTo = function (a, b) {
-			for (var i = 0; i < a.neighbours.length; i++) {
-				if (a.neighbours[i] === b.id) {
-					return a.portals[i];
-				}
-			}
-		};
+    // Push entities into list of els to intersect.
+    if (data.objects) {
+      objectEls = this.el.sceneEl.querySelectorAll(data.objects);
+    } else {
+      // If objects not defined, intersect with everything.
+      objectEls = this.el.sceneEl.children;
+    }
+    // Convert from NodeList to Array
+    this.els = Array.prototype.slice.call(objectEls);
+  },
 
-		// We have the corridor, now pull the rope.
-		const channel = new Channel();
-		channel.push(startPosition);
-		for (let i = 0; i < paths.length; i++) {
-			const polygon = paths[i];
-			const nextPolygon = paths[i + 1];
+  tick: (function () {
+    var position = new THREE.Vector3(),
+        meshPosition = new THREE.Vector3(),
+        meshScale = new THREE.Vector3(),
+        colliderScale = new THREE.Vector3(),
+        distanceMap = new Map();
+    return function () {
+      var el = this.el,
+          data = this.data,
+          mesh = el.getObject3D('mesh'),
+          colliderRadius,
+          collisions = [];
 
-			if (nextPolygon) {
-				const portals = getPortalFromTo(polygon, nextPolygon);
-				channel.push(
-					vertices[portals[0]],
-					vertices[portals[1]]
-				);
-			}
-		}
-		channel.push(targetPosition);
-		channel.stringPull();
+      if (!mesh) { return; }
 
-		// Return the path, omitting first position (which is already known).
-		const path = channel.path.map((c) => new THREE.Vector3(c.x, c.y, c.z));
-		path.shift();
-		return path;
-	}
-};
+      distanceMap.clear();
+      position.copy(el.object3D.getWorldPosition());
+      el.object3D.getWorldScale(colliderScale);
+      colliderRadius = data.radius * scaleFactor(colliderScale);
+      // Update collision list.
+      this.els.forEach(intersect);
 
-},{"./AStar":116,"./Channel":118,"./utils":120}],120:[function(require,module,exports){
-class Utils {
+      // Emit events and add collision states, in order of distance.
+      collisions
+        .sort(function (a, b) {
+          return distanceMap.get(a) > distanceMap.get(b) ? 1 : -1;
+        })
+        .forEach(this.handleHit);
 
-  static computeCentroids (geometry) {
-    var f, fl, face;
+      // Remove collision state from current element.
+      if (collisions.length === 0) { el.emit('hit', {el: null}); }
 
-    for ( f = 0, fl = geometry.faces.length; f < fl; f ++ ) {
+      // Remove collision state from other elements.
+      this.collisions.filter(function (el) {
+        return !distanceMap.has(el);
+      }).forEach(this.handleHitEnd);
 
-      face = geometry.faces[ f ];
-      face.centroid = new THREE.Vector3( 0, 0, 0 );
+      // Store new collisions
+      this.collisions = collisions;
 
-      face.centroid.add( geometry.vertices[ face.a ] );
-      face.centroid.add( geometry.vertices[ face.b ] );
-      face.centroid.add( geometry.vertices[ face.c ] );
-      face.centroid.divideScalar( 3 );
+      // Bounding sphere collision detection
+      function intersect (el) {
+        var radius, mesh, distance, box, extent, size;
 
-    }
-  }
+        if (!el.isEntity) { return; }
 
-  static roundNumber (number, decimals) {
-    var newnumber = Number(number + '').toFixed(parseInt(decimals));
-    return parseFloat(newnumber);
-  }
+        mesh = el.getObject3D('mesh');
 
-  static sample (list) {
-    return list[Math.floor(Math.random() * list.length)];
-  }
+        if (!mesh) { return; }
 
-  static mergeVertexIds (aList, bList) {
+        box = new THREE.Box3().setFromObject(mesh);
+        size = box.getSize();
+        extent = Math.max(size.x, size.y, size.z) / 2;
+        radius = Math.sqrt(2 * extent * extent);
+        box.getCenter(meshPosition);
 
-    var sharedVertices = [];
+        if (!radius) { return; }
 
-    aList.forEach((vID) => {
-      if (bList.indexOf(vID) >= 0) {
-        sharedVertices.push(vID);
+        distance = position.distanceTo(meshPosition);
+        if (distance < radius + colliderRadius) {
+          collisions.push(el);
+          distanceMap.set(el, distance);
+        }
       }
-    });
+      // use max of scale factors to maintain bounding sphere collision
+      function scaleFactor (scaleVec) {
+        return Math.max.apply(null, scaleVec.toArray());
+      }
+    };
+  })(),
 
-    if (sharedVertices.length < 2) return [];
+  handleHit: function (targetEl) {
+    targetEl.emit('hit');
+    targetEl.addState(this.data.state);
+    this.el.emit('hit', {el: targetEl});
+  },
+  handleHitEnd: function (targetEl) {
+    targetEl.emit('hitend');
+    targetEl.removeState(this.data.state);
+    this.el.emit('hitend', {el: targetEl});
+  }
+};
 
-    if (sharedVertices.includes(aList[0]) && sharedVertices.includes(aList[aList.length - 1])) {
-      // Vertices on both edges are bad, so shift them once to the left
-      aList.push(aList.shift());
-    }
+},{}],109:[function(require,module,exports){
+/**
+ * Toggle velocity.
+ *
+ * Moves an object back and forth along an axis, within a min/max extent.
+ */
+module.exports = {
+  dependencies: ['velocity'],
+  schema: {
+    axis: { default: 'x', oneOf: ['x', 'y', 'z'] },
+    min: { default: 0 },
+    max: { default: 0 },
+    speed: { default: 1 }
+  },
+  init: function () {
+    var velocity = {x: 0, y: 0, z: 0};
+    velocity[this.data.axis] = this.data.speed;
+    this.el.setAttribute('velocity', velocity);
 
-    if (sharedVertices.includes(bList[0]) && sharedVertices.includes(bList[bList.length - 1])) {
-      // Vertices on both edges are bad, so shift them once to the left
-      bList.push(bList.shift());
+    if (this.el.sceneEl.addBehavior) this.el.sceneEl.addBehavior(this);
+  },
+  remove: function () {},
+  update: function () { this.tick(); },
+  tick: function () {
+    var data = this.data,
+        velocity = this.el.getAttribute('velocity'),
+        position = this.el.getAttribute('position');
+    if (velocity[data.axis] > 0 && position[data.axis] > data.max) {
+      velocity[data.axis] = -data.speed;
+      this.el.setAttribute('velocity', velocity);
+    } else if (velocity[data.axis] < 0 && position[data.axis] < data.min) {
+      velocity[data.axis] = data.speed;
+      this.el.setAttribute('velocity', velocity);
     }
+  },
+};
 
-    // Again!
-    sharedVertices = [];
-
-    aList.forEach((vId) => {
-      if (bList.includes(vId)) {
-        sharedVertices.push(vId);
-      }
-    });
+},{}],110:[function(require,module,exports){
+module.exports = {
+  'nav-mesh':    require('./nav-mesh'),
+  'nav-controller':     require('./nav-controller'),
+  'system':      require('./system'),
 
-    var clockwiseMostSharedVertex = sharedVertices[1];
-    var counterClockwiseMostSharedVertex = sharedVertices[0];
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
 
+    AFRAME = AFRAME || window.AFRAME;
 
-    var cList = aList.slice();
-    while (cList[0] !== clockwiseMostSharedVertex) {
-      cList.push(cList.shift());
+    if (!AFRAME.components['nav-mesh']) {
+      AFRAME.registerComponent('nav-mesh', this['nav-mesh']);
     }
 
-    var c = 0;
-
-    var temp = bList.slice();
-    while (temp[0] !== counterClockwiseMostSharedVertex) {
-      temp.push(temp.shift());
-
-      if (c++ > 10) throw new Error('Unexpected state');
+    if (!AFRAME.components['nav-controller']) {
+      AFRAME.registerComponent('nav-controller',  this['nav-controller']);
     }
 
-    // Shave
-    temp.shift();
-    temp.pop();
-
-    cList = cList.concat(temp);
+    if (!AFRAME.systems.nav) {
+      AFRAME.registerSystem('nav', this.system);
+    }
 
-    return cList;
+    this._registered = true;
   }
+};
 
-  static setPolygonCentroid (polygon, navigationMesh) {
-    var sum = new THREE.Vector3();
-
-    var vertices = navigationMesh.vertices;
+},{"./nav-controller":111,"./nav-mesh":112,"./system":113}],111:[function(require,module,exports){
+module.exports = {
+  schema: {
+    destination: {type: 'vec3'},
+    active: {default: false},
+    speed: {default: 2}
+  },
+  init: function () {
+    this.system = this.el.sceneEl.systems.nav;
+    this.system.addController(this);
+    this.path = [];
+    this.raycaster = new THREE.Raycaster();
+  },
+  remove: function () {
+    this.system.removeController(this);
+  },
+  update: function () {
+    this.path.length = 0;
+  },
+  tick: (function () {
+    var vDest = new THREE.Vector3();
+    var vDelta = new THREE.Vector3();
+    var vNext = new THREE.Vector3();
 
-    polygon.vertexIds.forEach((vId) => {
-      sum.add(vertices[vId]);
-    });
+    return function (t, dt) {
+      var el = this.el;
+      var data = this.data;
+      var raycaster = this.raycaster;
+      var speed = data.speed * dt / 1000;
 
-    sum.divideScalar(polygon.vertexIds.length);
+      if (!data.active) return;
 
-    polygon.centroid.copy(sum);
-  }
+      // Use PatrolJS pathfinding system to get shortest path to target.
+      if (!this.path.length) {
+        this.path = this.system.getPath(this.el.object3D, vDest.copy(data.destination));
+        this.path = this.path || [];
+        el.emit('nav-start');
+      }
 
-  static cleanPolygon (polygon, navigationMesh) {
+      // If no path is found, exit.
+      if (!this.path.length) {
+        console.warn('[nav] Unable to find path to %o.', data.destination);
+        this.el.setAttribute('nav-controller', {active: false});
+        el.emit('nav-end');
+        return;
+      }
 
-    var newVertexIds = [];
+      // Current segment is a vector from current position to next waypoint.
+      var vCurrent = el.object3D.position;
+      var vWaypoint = this.path[0];
+      vDelta.subVectors(vWaypoint, vCurrent);
 
-    var vertices = navigationMesh.vertices;
+      var distance = vDelta.length();
+      var gazeTarget;
 
-    for (var i = 0; i < polygon.vertexIds.length; i++) {
+      if (distance < speed) {
+        // If <1 step from current waypoint, discard it and move toward next.
+        this.path.shift();
 
-      var vertex = vertices[polygon.vertexIds[i]];
+        // After discarding the last waypoint, exit pathfinding.
+        if (!this.path.length) {
+          this.el.setAttribute('nav-controller', {active: false});
+          el.emit('nav-end');
+          return;
+        } else {
+          gazeTarget = this.path[0];
+        }
+      } else {
+        // If still far away from next waypoint, find next position for
+        // the current frame.
+        vNext.copy(vDelta.setLength(speed)).add(vCurrent);
+        gazeTarget = vWaypoint;
+      }
 
-      var nextVertexId, previousVertexId;
-      var nextVertex, previousVertex;
+      // Look at the next waypoint.
+      gazeTarget.y = vCurrent.y;
+      el.object3D.lookAt(gazeTarget);
 
-      // console.log("nextVertex: ", nextVertex);
+      // Raycast against the nav mesh, to keep the controller moving along the
+      // ground, not traveling in a straight line from higher to lower waypoints.
+      raycaster.ray.origin.copy(vNext);
+      raycaster.ray.origin.y += 1.5;
+      raycaster.ray.direction.y = -1;
+      var intersections = raycaster.intersectObject(this.system.getNavMesh());
 
-      if (i === 0) {
-        nextVertexId = polygon.vertexIds[1];
-        previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 1];
-      } else if (i === polygon.vertexIds.length - 1) {
-        nextVertexId = polygon.vertexIds[0];
-        previousVertexId = polygon.vertexIds[polygon.vertexIds.length - 2];
+      if (!intersections.length) {
+        // Raycasting failed. Step toward the waypoint and hope for the best.
+        vCurrent.copy(vNext);
       } else {
-        nextVertexId = polygon.vertexIds[i + 1];
-        previousVertexId = polygon.vertexIds[i - 1];
+        // Re-project next position onto nav mesh.
+        vDelta.subVectors(intersections[0].point, vCurrent);
+        vCurrent.add(vDelta.setLength(speed));
       }
 
-      nextVertex = vertices[nextVertexId];
-      previousVertex = vertices[previousVertexId];
+    };
+  }())
+};
 
-      var a = nextVertex.clone().sub(vertex);
-      var b = previousVertex.clone().sub(vertex);
+},{}],112:[function(require,module,exports){
+/**
+ * nav-mesh
+ *
+ * Waits for a mesh to be loaded on the current entity, then sets it as the
+ * nav mesh in the pathfinding system.
+ */
+module.exports = {
+  init: function () {
+    this.system = this.el.sceneEl.systems.nav;
+    this.loadNavMesh();
+    this.el.addEventListener('model-loaded', this.loadNavMesh.bind(this));
+  },
 
-      var angle = a.angleTo(b);
+  loadNavMesh: function () {
+    var object = this.el.getObject3D('mesh');
 
-      // console.log(angle);
+    if (!object) return;
 
-      if (angle > Math.PI - 0.01 && angle < Math.PI + 0.01) {
-        // Unneccesary vertex
-        // console.log("Unneccesary vertex: ", polygon.vertexIds[i]);
-        // console.log("Angle between "+previousVertexId+", "+polygon.vertexIds[i]+" "+nextVertexId+" was: ", angle);
+    var navMesh;
+    object.traverse(function (node) {
+      if (node.isMesh) navMesh = node;
+    });
 
+    if (!navMesh) return;
 
-        // Remove the neighbours who had this vertex
-        var goodNeighbours = [];
-        polygon.neighbours.forEach((neighbour) => {
-          if (!neighbour.vertexIds.includes(polygon.vertexIds[i])) {
-            goodNeighbours.push(neighbour);
-          }
-        });
-        polygon.neighbours = goodNeighbours;
+    this.system.setNavMesh(navMesh);
+  }
+};
 
+},{}],113:[function(require,module,exports){
+var Path = require('three-pathfinding');
 
-        // TODO cleanup the list of vertices and rebuild vertexIds for all polygons
-      } else {
-        newVertexIds.push(polygon.vertexIds[i]);
-      }
+/**
+ * nav
+ *
+ * Pathfinding system, using PatrolJS.
+ */
+module.exports = {
+  init: function () {
+    this.navMesh = null;
+    this.nodes = null;
+    this.controllers = new Set();
+  },
 
-    }
+  /**
+   * @param {THREE.Mesh} mesh
+   */
+  setNavMesh: function (mesh) {
+    var geometry = mesh.geometry.isBufferGeometry
+      ? new THREE.Geometry().fromBufferGeometry(mesh.geometry)
+      : mesh.geometry;
+    this.navMesh = new THREE.Mesh(geometry);
+    this.nodes = Path.buildNodes(this.navMesh.geometry);
+    Path.setZoneData('level', this.nodes);
+  },
 
-    // console.log("New vertexIds: ", newVertexIds);
+  /**
+   * @return {THREE.Mesh}
+   */
+  getNavMesh: function () {
+    return this.navMesh;
+  },
 
-    polygon.vertexIds = newVertexIds;
+  /**
+   * @param {NavController} ctrl
+   */
+  addController: function (ctrl) {
+    this.controllers.add(ctrl);
+  },
 
-    setPolygonCentroid(polygon, navigationMesh);
+  /**
+   * @param {NavController} ctrl
+   */
+  removeController: function (ctrl) {
+    this.controllers.remove(ctrl);
+  },
 
+  /**
+   * @param  {NavController} ctrl
+   * @param  {THREE.Vector3} target
+   * @return {Array<THREE.Vector3>}
+   */
+  getPath: function (ctrl, target) {
+    var start = ctrl.el.object3D.position;
+    // TODO(donmccurdy): Current group should be cached.
+    var group = Path.getGroup('level', start);
+    return Path.findPath(start, target, 'level', group);
   }
+};
 
-  static isConvex (polygon, navigationMesh) {
-
-    var vertices = navigationMesh.vertices;
-
-    if (polygon.vertexIds.length < 3) return false;
-
-    var convex = true;
+},{"three-pathfinding":82}],114:[function(require,module,exports){
+/**
+ * Flat grid.
+ *
+ * Defaults to 75x75.
+ */
+var Primitive = module.exports = {
+  defaultComponents: {
+    geometry: {
+      primitive: 'plane',
+      width: 75,
+      height: 75
+    },
+    rotation: {x: -90, y: 0, z: 0},
+    material: {
+      src: 'url(https://cdn.rawgit.com/donmccurdy/aframe-extras/v1.16.3/assets/grid.png)',
+      repeat: '75 75'
+    }
+  },
+  mappings: {
+    width: 'geometry.width',
+    height: 'geometry.height',
+    src: 'material.src'
+  }
+};
 
-    var total = 0;
+module.exports.registerAll = (function () {
+  var registered = false;
+  return function (AFRAME) {
+    if (registered) return;
+    AFRAME = AFRAME || window.AFRAME;
+    AFRAME.registerPrimitive('a-grid', Primitive);
+    registered = true;
+  };
+}());
 
-    var results = [];
+},{}],115:[function(require,module,exports){
+var vg = require('../../lib/hex-grid.min.js');
+var defaultHexGrid = require('../../lib/default-hex-grid.json');
 
-    for (var i = 0; i < polygon.vertexIds.length; i++) {
+/**
+ * Hex grid.
+ */
+var Primitive = module.exports.Primitive = {
+  defaultComponents: {
+    'hexgrid': {}
+  },
+  mappings: {
+    src: 'hexgrid.src'
+  }
+};
 
-      var vertex = vertices[polygon.vertexIds[i]];
+var Component = module.exports.Component = {
+  dependencies: ['material'],
+  schema: {
+    src: {type: 'asset'}
+  },
+  init: function () {
+    var data = this.data;
+    if (data.src) {
+      fetch(data.src)
+        .then(function (response) { response.json(); })
+        .then(function (json) { this.addMesh(json); });
+    } else {
+      this.addMesh(defaultHexGrid);
+    }
+  },
+  addMesh: function (json) {
+    var grid = new vg.HexGrid();
+    grid.fromJSON(json);
+    var board = new vg.Board(grid);
+    board.generateTilemap();
+    this.el.setObject3D('mesh', board.group);
+    this.addMaterial();
+  },
+  addMaterial: function () {
+    var materialComponent = this.el.components.material;
+    var material = (materialComponent || {}).material;
+    if (!material) return;
+    this.el.object3D.traverse(function (node) {
+      if (node.isMesh) {
+        node.material = material;
+      }
+    });
+  },
+  remove: function () {
+    this.el.removeObject3D('mesh');
+  }
+};
 
-      var nextVertex, previousVertex;
+module.exports.registerAll = (function () {
+  var registered = false;
+  return function (AFRAME) {
+    if (registered) return;
+    AFRAME = AFRAME || window.AFRAME;
+    AFRAME.registerComponent('hexgrid', Component);
+    AFRAME.registerPrimitive('a-hexgrid', Primitive);
+    registered = true;
+  };
+}());
 
-      if (i === 0) {
-        nextVertex = vertices[polygon.vertexIds[1]];
-        previousVertex = vertices[polygon.vertexIds[polygon.vertexIds.length - 1]];
-      } else if (i === polygon.vertexIds.length - 1) {
-        nextVertex = vertices[polygon.vertexIds[0]];
-        previousVertex = vertices[polygon.vertexIds[polygon.vertexIds.length - 2]];
-      } else {
-        nextVertex = vertices[polygon.vertexIds[i + 1]];
-        previousVertex = vertices[polygon.vertexIds[i - 1]];
-      }
+},{"../../lib/default-hex-grid.json":7,"../../lib/hex-grid.min.js":9}],116:[function(require,module,exports){
+/**
+ * Flat-shaded ocean primitive.
+ *
+ * Based on a Codrops tutorial:
+ * http://tympanus.net/codrops/2016/04/26/the-aviator-animating-basic-3d-scene-threejs/
+ */
+var Primitive = module.exports.Primitive = {
+  defaultComponents: {
+    ocean: {},
+    rotation: {x: -90, y: 0, z: 0}
+  },
+  mappings: {
+    width: 'ocean.width',
+    depth: 'ocean.depth',
+    density: 'ocean.density',
+    color: 'ocean.color',
+    opacity: 'ocean.opacity'
+  }
+};
 
-      var a = nextVertex.clone().sub(vertex);
-      var b = previousVertex.clone().sub(vertex);
+var Component = module.exports.Component = {
+  schema: {
+    // Dimensions of the ocean area.
+    width: {default: 10, min: 0},
+    depth: {default: 10, min: 0},
 
-      var angle = a.angleTo(b);
-      total += angle;
+    // Density of waves.
+    density: {default: 10},
 
-      if (angle === Math.PI || angle === 0) return false;
+    // Wave amplitude and variance.
+    amplitude: {default: 0.1},
+    amplitudeVariance: {default: 0.3},
 
-      var r = a.cross(b).y;
-      results.push(r);
-    }
+    // Wave speed and variance.
+    speed: {default: 1},
+    speedVariance: {default: 2},
 
-    // if ( total > (polygon.vertexIds.length-2)*Math.PI ) return false;
+    // Material.
+    color: {default: '#7AD2F7', type: 'color'},
+    opacity: {default: 0.8}
+  },
 
-    results.forEach((r) => {
-      if (r === 0) convex = false;
-    });
+  /**
+   * Use play() instead of init(), because component mappings – unavailable as dependencies – are
+   * not guaranteed to have parsed when this component is initialized.
+   */
+  play: function () {
+    var el = this.el,
+        data = this.data,
+        material = el.components.material;
 
-    if (results[0] > 0) {
-      results.forEach((r) => {
-        if (r < 0) convex = false;
-      });
-    } else {
-      results.forEach((r) => {
-        if (r > 0) convex = false;
+    var geometry = new THREE.PlaneGeometry(data.width, data.depth, data.density, data.density);
+    geometry.mergeVertices();
+    this.waves = [];
+    for (var v, i = 0, l = geometry.vertices.length; i < l; i++) {
+      v = geometry.vertices[i];
+      this.waves.push({
+        z: v.z,
+        ang: Math.random() * Math.PI * 2,
+        amp: data.amplitude + Math.random() * data.amplitudeVariance,
+        speed: (data.speed + Math.random() * data.speedVariance) / 1000 // radians / frame
       });
     }
 
-    return convex;
-  }
-
-  static distanceToSquared (a, b) {
+    if (!material) {
+      material = {};
+      material.material = new THREE.MeshPhongMaterial({
+        color: data.color,
+        transparent: data.opacity < 1,
+        opacity: data.opacity,
+        shading: THREE.FlatShading,
+      });
+    }
 
-    var dx = a.x - b.x;
-    var dy = a.y - b.y;
-    var dz = a.z - b.z;
+    this.mesh = new THREE.Mesh(geometry, material.material);
+    el.setObject3D('mesh', this.mesh);
+  },
 
-    return dx * dx + dy * dy + dz * dz;
+  remove: function () {
+    this.el.removeObject3D('mesh');
+  },
 
-  }
+  tick: function (t, dt) {
+    if (!dt) return;
 
-  //+ Jonas Raoni Soares Silva
-  //@ http://jsfromhell.com/math/is-point-in-poly [rev. #0]
-  static isPointInPoly (poly, pt) {
-    for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
-      ((poly[i].z <= pt.z && pt.z < poly[j].z) || (poly[j].z <= pt.z && pt.z < poly[i].z)) && (pt.x < (poly[j].x - poly[i].x) * (pt.z - poly[i].z) / (poly[j].z - poly[i].z) + poly[i].x) && (c = !c);
-    return c;
+    var verts = this.mesh.geometry.vertices;
+    for (var v, vprops, i = 0; (v = verts[i]); i++){
+      vprops = this.waves[i];
+      v.z = vprops.z + Math.sin(vprops.ang) * vprops.amp;
+      vprops.ang += vprops.speed * dt;
+    }
+    this.mesh.geometry.verticesNeedUpdate = true;
   }
+};
 
-  static isVectorInPolygon (vector, polygon, vertices) {
-
-    // reference point will be the centroid of the polygon
-    // We need to rotate the vector as well as all the points which the polygon uses
+module.exports.registerAll = (function () {
+  var registered = false;
+  return function (AFRAME) {
+    if (registered) return;
+    AFRAME = AFRAME || window.AFRAME;
+    AFRAME.registerComponent('ocean', Component);
+    AFRAME.registerPrimitive('a-ocean', Primitive);
+    registered = true;
+  };
+}());
 
-    var lowestPoint = 100000;
-    var highestPoint = -100000;
+},{}],117:[function(require,module,exports){
+/**
+ * Tube following a custom path.
+ *
+ * Usage:
+ *
+ * ```html
+ * <a-tube path="5 0 5, 5 0 -5, -5 0 -5" radius="0.5"></a-tube>
+ * ```
+ */
+var Primitive = module.exports.Primitive = {
+  defaultComponents: {
+    tube:           {},
+  },
+  mappings: {
+    path:           'tube.path',
+    segments:       'tube.segments',
+    radius:         'tube.radius',
+    radialSegments: 'tube.radialSegments',
+    closed:         'tube.closed'
+  }
+};
 
-    var polygonVertices = [];
+var Component = module.exports.Component = {
+  schema: {
+    path:           {default: []},
+    segments:       {default: 64},
+    radius:         {default: 1},
+    radialSegments: {default: 8},
+    closed:         {default: false}
+  },
 
-    polygon.vertexIds.forEach((vId) => {
-      lowestPoint = Math.min(vertices[vId].y, lowestPoint);
-      highestPoint = Math.max(vertices[vId].y, highestPoint);
-      polygonVertices.push(vertices[vId]);
-    });
+  init: function () {
+    var el = this.el,
+        data = this.data,
+        material = el.components.material;
 
-    if (vector.y < highestPoint + 0.5 && vector.y > lowestPoint - 0.5 &&
-      this.isPointInPoly(polygonVertices, vector)) {
-      return true;
+    if (!data.path.length) {
+      console.error('[a-tube] `path` property expected but not found.');
+      return;
     }
-    return false;
-  }
-
-  static triarea2 (a, b, c) {
-    var ax = b.x - a.x;
-    var az = b.z - a.z;
-    var bx = c.x - a.x;
-    var bz = c.z - a.z;
-    return bx * az - ax * bz;
-  }
 
-  static vequal (a, b) {
-    return this.distanceToSquared(a, b) < 0.00001;
-  }
+    var curve = new THREE.CatmullRomCurve3(data.path.map(function (point) {
+      point = point.split(' ');
+      return new THREE.Vector3(Number(point[0]), Number(point[1]), Number(point[2]));
+    }));
+    var geometry = new THREE.TubeGeometry(
+      curve, data.segments, data.radius, data.radialSegments, data.closed
+    );
 
-  static array_intersect () {
-    let i, shortest, nShortest, n, len, ret = [],
-      obj = {},
-      nOthers;
-    nOthers = arguments.length - 1;
-    nShortest = arguments[0].length;
-    shortest = 0;
-    for (i = 0; i <= nOthers; i++) {
-      n = arguments[i].length;
-      if (n < nShortest) {
-        shortest = i;
-        nShortest = n;
-      }
+    if (!material) {
+      material = {};
+      material.material = new THREE.MeshPhongMaterial();
     }
 
-    for (i = 0; i <= nOthers; i++) {
-      n = (i === shortest) ? 0 : (i || shortest); //Read the shortest array first. Read the first array instead of the shortest
-      len = arguments[n].length;
-      for (var j = 0; j < len; j++) {
-        var elem = arguments[n][j];
-        if (obj[elem] === i - 1) {
-          if (i === nOthers) {
-            ret.push(elem);
-            obj[elem] = 0;
-          } else {
-            obj[elem] = i;
-          }
-        } else if (i === 0) {
-          obj[elem] = 0;
-        }
-      }
-    }
-    return ret;
+    this.mesh = new THREE.Mesh(geometry, material.material);
+    this.el.setObject3D('mesh', this.mesh);
+  },
+
+  remove: function () {
+    if (this.mesh) this.el.removeObject3D('mesh');
   }
-}
+};
 
+module.exports.registerAll = (function () {
+  var registered = false;
+  return function (AFRAME) {
+    if (registered) return;
+    AFRAME = AFRAME || window.AFRAME;
+    AFRAME.registerComponent('tube', Component);
+    AFRAME.registerPrimitive('a-tube', Primitive);
+    registered = true;
+  };
+}());
 
+},{}],118:[function(require,module,exports){
+module.exports = {
+  'a-grid':     require('./a-grid'),
+  'a-hexgrid': require('./a-hexgrid'),
+  'a-ocean':    require('./a-ocean'),
+  'a-tube':     require('./a-tube'),
 
-module.exports = Utils;
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
+    AFRAME = AFRAME || window.AFRAME;
+    this['a-grid'].registerAll(AFRAME);
+    this['a-hexgrid'].registerAll(AFRAME);
+    this['a-ocean'].registerAll(AFRAME);
+    this['a-tube'].registerAll(AFRAME);
+    this._registered = true;
+  }
+};
 
-},{}]},{},[1]);
+},{"./a-grid":114,"./a-hexgrid":115,"./a-ocean":116,"./a-tube":117}]},{},[1]);

+ 7 - 270
support/client/lib/vwf/model/aframe/extras/aframe-extras.loaders.js

@@ -1,6 +1,6 @@
 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
 require('./src/loaders').registerAll();
-},{"./src/loaders":9}],2:[function(require,module,exports){
+},{"./src/loaders":8}],2:[function(require,module,exports){
 /**
  * @author Kyle-Larson https://github.com/Kyle-Larson
  * @author Takahiro https://github.com/takahirox
@@ -5853,109 +5853,6 @@ var LOADER_SRC = 'https://rawgit.com/mrdoob/three.js/r86/examples/js/loaders/GLT
  * Legacy loader for glTF 1.0 models.
  * Asynchronously loads THREE.GLTFLoader from rawgit.
  */
-module.exports.Component = {
-  schema: {type: 'model'},
-
-  init: function () {
-    this.model = null;
-    this.loader = null;
-    this.loaderPromise = loadLoader().then(function () {
-      this.loader = new THREE.GLTFLoader();
-      this.loader.setCrossOrigin('Anonymous');
-    }.bind(this));
-  },
-
-  update: function () {
-    var self = this;
-    var el = this.el;
-    var src = this.data;
-
-    if (!src) { return; }
-
-    this.remove();
-
-    this.loaderPromise.then(function () {
-      this.loader.load(src, function gltfLoaded (gltfModel) {
-        self.model = gltfModel.scene;
-        self.model.animations = gltfModel.animations;
-        self.system.registerModel(self.model);
-        el.setObject3D('mesh', self.model);
-        el.emit('model-loaded', {format: 'gltf', model: self.model});
-      });
-    }.bind(this));
-  },
-
-  remove: function () {
-    if (!this.model) { return; }
-    this.el.removeObject3D('mesh');
-    this.system.unregisterModel(this.model);
-  }
-};
-
-/**
- * glTF model system.
- */
-module.exports.System = {
-  init: function () {
-    this.models = [];
-  },
-
-  /**
-   * Updates shaders for all glTF models in the system.
-   */
-  tick: function () {
-    var sceneEl = this.sceneEl;
-    if (sceneEl.hasLoaded && this.models.length) {
-      THREE.GLTFLoader.Shaders.update(sceneEl.object3D, sceneEl.camera);
-    }
-  },
-
-  /**
-   * Registers a glTF asset.
-   * @param {object} gltf Asset containing a scene and (optional) animations and cameras.
-   */
-  registerModel: function (gltf) {
-    this.models.push(gltf);
-  },
-
-  /**
-   * Unregisters a glTF asset.
-   * @param  {object} gltf Asset containing a scene and (optional) animations and cameras.
-   */
-  unregisterModel: function (gltf) {
-    var models = this.models;
-    var index = models.indexOf(gltf);
-    if (index >= 0) {
-      models.splice(index, 1);
-    }
-  }
-};
-
-var loadLoader = (function () {
-  var promise;
-  return function () {
-    promise = promise || fetchScript(LOADER_SRC);
-    return promise;
-  };
-}());
-
-},{"../../lib/fetch-script":4}],8:[function(require,module,exports){
-var fetchScript = require('../../lib/fetch-script')();
-
-var LOADER_SRC = 'https://rawgit.com/mrdoob/three.js/r87/examples/js/loaders/GLTFLoader.js';
-// Monkeypatch while waiting for three.js r86.
-if (THREE.PropertyBinding.sanitizeNodeName === undefined) {
-
-  THREE.PropertyBinding.sanitizeNodeName = function (s) {
-    return s.replace( /\s/g, '_' ).replace( /[^\w-]/g, '' );
-  };
-
-}
-
-/**
- * Upcoming loader for glTF 2.0 models.
- * Asynchronously loads THREE.GLTF2Loader from rawgit.
- */
 module.exports = {
   schema: {type: 'model'},
 
@@ -6001,16 +5898,14 @@ var loadLoader = (function () {
   };
 }());
 
-},{"../../lib/fetch-script":4}],9:[function(require,module,exports){
+},{"../../lib/fetch-script":4}],8:[function(require,module,exports){
 module.exports = {
   'animation-mixer': require('./animation-mixer'),
   'fbx-model': require('./fbx-model'),
-  'gltf-model-next': require('./gltf-model-next'),
   'gltf-model-legacy': require('./gltf-model-legacy'),
   'json-model': require('./json-model'),
   'object-model': require('./object-model'),
   'ply-model': require('./ply-model'),
-  'three-model': require('./three-model'),
 
   registerAll: function (AFRAME) {
     if (this._registered) return;
@@ -6035,15 +5930,9 @@ module.exports = {
       AFRAME.registerComponent('fbx-model', this['fbx-model']);
     }
 
-    // THREE.GLTF2Loader
-    if (!AFRAME.components['gltf-model-next']) {
-      AFRAME.registerComponent('gltf-model-next', this['gltf-model-next']);
-    }
-
     // THREE.GLTFLoader
     if (!AFRAME.components['gltf-model-legacy']) {
-      AFRAME.registerComponent('gltf-model-legacy', this['gltf-model-legacy'].Component);
-      AFRAME.registerSystem('gltf-model-legacy', this['gltf-model-legacy'].System);
+      AFRAME.registerComponent('gltf-model-legacy', this['gltf-model-legacy']);
     }
 
     // THREE.JsonLoader
@@ -6056,16 +5945,11 @@ module.exports = {
       AFRAME.registerComponent('object-model', this['object-model']);
     }
 
-    // (deprecated) THREE.JsonLoader and THREE.ObjectLoader
-    if (!AFRAME.components['three-model']) {
-      AFRAME.registerComponent('three-model', this['three-model']);
-    }
-
     this._registered = true;
   }
 };
 
-},{"./animation-mixer":5,"./fbx-model":6,"./gltf-model-legacy":7,"./gltf-model-next":8,"./json-model":10,"./object-model":11,"./ply-model":12,"./three-model":13}],10:[function(require,module,exports){
+},{"./animation-mixer":5,"./fbx-model":6,"./gltf-model-legacy":7,"./json-model":9,"./object-model":10,"./ply-model":11}],9:[function(require,module,exports){
 /**
  * json-model
  *
@@ -6125,7 +6009,7 @@ module.exports = {
   }
 };
 
-},{}],11:[function(require,module,exports){
+},{}],10:[function(require,module,exports){
 /**
  * object-model
  *
@@ -6180,7 +6064,7 @@ module.exports = {
   }
 };
 
-},{}],12:[function(require,module,exports){
+},{}],11:[function(require,module,exports){
 /**
  * ply-model
  *
@@ -6261,151 +6145,4 @@ function createModel (geometry) {
   }));
 }
 
-},{"../../lib/PLYLoader":3}],13:[function(require,module,exports){
-var DEFAULT_ANIMATION = '__auto__';
-
-/**
- * three-model
- *
- * Loader for THREE.js JSON format. Somewhat confusingly, there are two
- * different THREE.js formats, both having the .json extension. This loader
- * supports both, but requires you to specify the mode as "object" or "json".
- *
- * Typically, you will use "json" for a single mesh, and "object" for a scene
- * or multiple meshes. Check the console for errors, if in doubt.
- *
- * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
- */
-module.exports = {
-  deprecated: true,
-
-  schema: {
-    src:               { type: 'asset' },
-    loader:            { default: 'object', oneOf: ['object', 'json'] },
-    enableAnimation:   { default: true },
-    animation:         { default: DEFAULT_ANIMATION },
-    animationDuration: { default: 0 },
-    crossorigin:       { default: '' }
-  },
-
-  init: function () {
-    this.model = null;
-    this.mixer = null;
-    console.warn('[three-model] Component is deprecated. Use json-model or object-model instead.');
-  },
-
-  update: function (previousData) {
-    previousData = previousData || {};
-
-    var loader,
-        data = this.data;
-
-    if (!data.src) {
-      this.remove();
-      return;
-    }
-
-    // First load.
-    if (!Object.keys(previousData).length) {
-      this.remove();
-      if (data.loader === 'object') {
-        loader = new THREE.ObjectLoader();
-        if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
-        loader.load(data.src, function(loaded) {
-          loaded.traverse( function(object) {
-            if (object instanceof THREE.SkinnedMesh)
-              loaded = object;
-          });
-          if(loaded.material)
-            loaded.material.skinning = !!((loaded.geometry && loaded.geometry.bones) || []).length;
-          this.load(loaded);
-        }.bind(this));
-      } else if (data.loader === 'json') {
-        loader = new THREE.JSONLoader();
-        if (data.crossorigin) loader.crossOrigin = data.crossorigin;
-        loader.load(data.src, function (geometry, materials) {
-
-          // Attempt to automatically detect common material options.
-          materials.forEach(function (mat) {
-            mat.vertexColors = (geometry.faces[0] || {}).color ? THREE.FaceColors : THREE.NoColors;
-            mat.skinning = !!(geometry.bones || []).length;
-            mat.morphTargets = !!(geometry.morphTargets || []).length;
-            mat.morphNormals = !!(geometry.morphNormals || []).length;
-          });
-
-          var mesh = (geometry.bones || []).length
-            ? new THREE.SkinnedMesh(geometry, new THREE.MultiMaterial(materials))
-            : new THREE.Mesh(geometry, new THREE.MultiMaterial(materials));
-
-          this.load(mesh);
-        }.bind(this));
-      } else {
-        throw new Error('[three-model] Invalid mode "%s".', data.mode);
-      }
-      return;
-    }
-
-    var activeAction = this.model && this.model.activeAction;
-
-    if (data.animation !== previousData.animation) {
-      if (activeAction) activeAction.stop();
-      this.playAnimation();
-      return;
-    }
-
-    if (activeAction && data.enableAnimation !== activeAction.isRunning()) {
-      data.enableAnimation ? this.playAnimation() : activeAction.stop();
-    }
-
-    if (activeAction && data.animationDuration) {
-        activeAction.setDuration(data.animationDuration);
-    }
-  },
-
-  load: function (model) {
-    this.model = model;
-    this.mixer = new THREE.AnimationMixer(this.model);
-    this.el.setObject3D('mesh', model);
-    this.el.emit('model-loaded', {format: 'three', model: model});
-
-    if (this.data.enableAnimation) this.playAnimation();
-  },
-
-  playAnimation: function () {
-    var clip,
-        data = this.data,
-        animations = this.model.animations || this.model.geometry.animations || [];
-
-    if (!data.enableAnimation || !data.animation || !animations.length) {
-      return;
-    }
-
-    clip = data.animation === DEFAULT_ANIMATION
-      ? animations[0]
-      : THREE.AnimationClip.findByName(animations, data.animation);
-
-    if (!clip) {
-      console.error('[three-model] Animation "%s" not found.', data.animation);
-      return;
-    }
-
-    this.model.activeAction = this.mixer.clipAction(clip, this.model);
-    if (data.animationDuration) {
-      this.model.activeAction.setDuration(data.animationDuration);
-    }
-    this.model.activeAction.play();
-  },
-
-  remove: function () {
-    if (this.mixer) this.mixer.stopAllAction();
-    if (this.model) this.el.removeObject3D('mesh');
-  },
-
-  tick: function (t, dt) {
-    if (this.mixer && !isNaN(dt)) {
-      this.mixer.update(dt / 1000);
-    }
-  }
-};
-
-},{}]},{},[1]);
+},{"../../lib/PLYLoader":3}]},{},[1]);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/aframe-extras.loaders.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/aframe-extras.min.js


+ 12 - 6
support/client/lib/vwf/model/aframe/extras/aframe-extras.misc.js

@@ -15683,7 +15683,7 @@ function getGeometry (object) {
     var position = new THREE.Vector3(),
         quaternion = new THREE.Quaternion(),
         scale = new THREE.Vector3();
-    if (meshes[0].geometry instanceof THREE.BufferGeometry) {
+    if (meshes[0].geometry.isBufferGeometry) {
       if (meshes[0].geometry.attributes.position) {
         tmp.fromBufferGeometry(meshes[0].geometry);
       }
@@ -15699,7 +15699,7 @@ function getGeometry (object) {
   // Recursively merge geometry, preserving local transforms.
   while ((mesh = meshes.pop())) {
     mesh.updateMatrixWorld();
-    if (mesh.geometry instanceof THREE.BufferGeometry) {
+    if (mesh.geometry.isBufferGeometry) {
       tmp.fromBufferGeometry(mesh.geometry);
       combined.merge(tmp, mesh.matrixWorld);
     } else {
@@ -16550,7 +16550,9 @@ module.exports = {
 
       for (var i = 0, contact; (contact = this.system.world.contacts[i]); i++) {
         // 1. Find any collisions involving this element. Get the contact
-        // normal, and make sure it's oriented _out_ of the other object.
+        // normal, and make sure it's oriented _out_ of the other object and
+        // enabled (body.collisionReponse is true for both bodies)
+        if (!contact.enabled) { continue; }
         if (body.id === contact.bi.id) {
           contact.ni.negate(currentSurfaceNormal);
         } else if (body.id === contact.bj.id) {
@@ -16685,6 +16687,7 @@ module.exports = {
     this.collisions = [];
 
     this.handleHit = this.handleHit.bind(this);
+    this.handleHitEnd = this.handleHitEnd.bind(this);
   },
 
   remove: function () {
@@ -16760,9 +16763,7 @@ module.exports = {
       // Remove collision state from other elements.
       this.collisions.filter(function (el) {
         return !distanceMap.has(el);
-      }).forEach(function removeState (el) {
-        el.removeState(data.state);
-      });
+      }).forEach(this.handleHitEnd);
 
       // Store new collisions
       this.collisions = collisions;
@@ -16802,6 +16803,11 @@ module.exports = {
     targetEl.emit('hit');
     targetEl.addState(this.data.state);
     this.el.emit('hit', {el: targetEl});
+  },
+  handleHitEnd: function (targetEl) {
+    targetEl.emit('hitend');
+    targetEl.removeState(this.data.state);
+    this.el.emit('hitend', {el: targetEl});
   }
 };
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/aframe-extras.misc.min.js


+ 222 - 222
support/client/lib/vwf/model/aframe/extras/aframe-extras.pathfinding.js

@@ -1,222 +1,6 @@
 (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
 require('./src/pathfinding').registerAll();
-},{"./src/pathfinding":2}],2:[function(require,module,exports){
-module.exports = {
-  'nav-mesh':    require('./nav-mesh'),
-  'nav-controller':     require('./nav-controller'),
-  'system':      require('./system'),
-
-  registerAll: function (AFRAME) {
-    if (this._registered) return;
-
-    AFRAME = AFRAME || window.AFRAME;
-
-    if (!AFRAME.components['nav-mesh']) {
-      AFRAME.registerComponent('nav-mesh', this['nav-mesh']);
-    }
-
-    if (!AFRAME.components['nav-controller']) {
-      AFRAME.registerComponent('nav-controller',  this['nav-controller']);
-    }
-
-    if (!AFRAME.systems.nav) {
-      AFRAME.registerSystem('nav', this.system);
-    }
-
-    this._registered = true;
-  }
-};
-
-},{"./nav-controller":3,"./nav-mesh":4,"./system":5}],3:[function(require,module,exports){
-module.exports = {
-  schema: {
-    destination: {type: 'vec3'},
-    active: {default: false},
-    speed: {default: 2}
-  },
-  init: function () {
-    this.system = this.el.sceneEl.systems.nav;
-    this.system.addController(this);
-    this.path = [];
-    this.raycaster = new THREE.Raycaster();
-  },
-  remove: function () {
-    this.system.removeController(this);
-  },
-  update: function () {
-    this.path.length = 0;
-  },
-  tick: (function () {
-    var vDest = new THREE.Vector3();
-    var vDelta = new THREE.Vector3();
-    var vNext = new THREE.Vector3();
-
-    return function (t, dt) {
-      var el = this.el;
-      var data = this.data;
-      var raycaster = this.raycaster;
-      var speed = data.speed * dt / 1000;
-
-      if (!data.active) return;
-
-      // Use PatrolJS pathfinding system to get shortest path to target.
-      if (!this.path.length) {
-        this.path = this.system.getPath(this.el.object3D, vDest.copy(data.destination));
-        this.path = this.path || [];
-        el.emit('nav-start');
-      }
-
-      // If no path is found, exit.
-      if (!this.path.length) {
-        console.warn('[nav] Unable to find path to %o.', data.destination);
-        this.el.setAttribute('nav-controller', {active: false});
-        el.emit('nav-end');
-        return;
-      }
-
-      // Current segment is a vector from current position to next waypoint.
-      var vCurrent = el.object3D.position;
-      var vWaypoint = this.path[0];
-      vDelta.subVectors(vWaypoint, vCurrent);
-
-      var distance = vDelta.length();
-      var gazeTarget;
-
-      if (distance < speed) {
-        // If <1 step from current waypoint, discard it and move toward next.
-        this.path.shift();
-
-        // After discarding the last waypoint, exit pathfinding.
-        if (!this.path.length) {
-          this.el.setAttribute('nav-controller', {active: false});
-          el.emit('nav-end');
-          return;
-        } else {
-          gazeTarget = this.path[0];
-        }
-      } else {
-        // If still far away from next waypoint, find next position for
-        // the current frame.
-        vNext.copy(vDelta.setLength(speed)).add(vCurrent);
-        gazeTarget = vWaypoint;
-      }
-
-      // Look at the next waypoint.
-      gazeTarget.y = vCurrent.y;
-      el.object3D.lookAt(gazeTarget);
-
-      // Raycast against the nav mesh, to keep the controller moving along the
-      // ground, not traveling in a straight line from higher to lower waypoints.
-      raycaster.ray.origin.copy(vNext);
-      raycaster.ray.origin.y += 1.5;
-      raycaster.ray.direction.y = -1;
-      var intersections = raycaster.intersectObject(this.system.getNavMesh());
-
-      if (!intersections.length) {
-        // Raycasting failed. Step toward the waypoint and hope for the best.
-        vCurrent.copy(vNext);
-      } else {
-        // Re-project next position onto nav mesh.
-        vDelta.subVectors(intersections[0].point, vCurrent);
-        vCurrent.add(vDelta.setLength(speed));
-      }
-
-    };
-  }())
-};
-
-},{}],4:[function(require,module,exports){
-/**
- * nav-mesh
- *
- * Waits for a mesh to be loaded on the current entity, then sets it as the
- * nav mesh in the pathfinding system.
- */
-module.exports = {
-  init: function () {
-    this.system = this.el.sceneEl.systems.nav;
-    this.loadNavMesh();
-    this.el.addEventListener('model-loaded', this.loadNavMesh.bind(this));
-  },
-
-  loadNavMesh: function () {
-    var object = this.el.getObject3D('mesh');
-
-    if (!object) return;
-
-    var navMesh;
-    object.traverse(function (node) {
-      if (node.isMesh) navMesh = node;
-    });
-
-    if (!navMesh) return;
-
-    this.system.setNavMesh(navMesh);
-  }
-};
-
-},{}],5:[function(require,module,exports){
-var Path = require('three-pathfinding');
-
-/**
- * nav
- *
- * Pathfinding system, using PatrolJS.
- */
-module.exports = {
-  init: function () {
-    this.navMesh = null;
-    this.nodes = null;
-    this.controllers = new Set();
-  },
-
-  /**
-   * @param {THREE.Mesh} mesh
-   */
-  setNavMesh: function (mesh) {
-    var geometry = mesh.geometry.isBufferGeometry
-      ? new THREE.Geometry().fromBufferGeometry(mesh.geometry)
-      : mesh.geometry;
-    this.navMesh = new THREE.Mesh(geometry);
-    this.nodes = Path.buildNodes(this.navMesh.geometry);
-    Path.setZoneData('level', this.nodes);
-  },
-
-  /**
-   * @return {THREE.Mesh}
-   */
-  getNavMesh: function () {
-    return this.navMesh;
-  },
-
-  /**
-   * @param {NavController} ctrl
-   */
-  addController: function (ctrl) {
-    this.controllers.add(ctrl);
-  },
-
-  /**
-   * @param {NavController} ctrl
-   */
-  removeController: function (ctrl) {
-    this.controllers.remove(ctrl);
-  },
-
-  /**
-   * @param  {NavController} ctrl
-   * @param  {THREE.Vector3} target
-   * @return {Array<THREE.Vector3>}
-   */
-  getPath: function (ctrl, target) {
-    var start = ctrl.el.object3D.position;
-    // TODO(donmccurdy): Current group should be cached.
-    var group = Path.getGroup('level', start);
-    return Path.findPath(start, target, 'level', group);
-  }
-};
-
-},{"three-pathfinding":9}],6:[function(require,module,exports){
+},{"./src/pathfinding":7}],2:[function(require,module,exports){
 const BinaryHeap = require('./BinaryHeap');
 const utils = require('./utils.js');
 
@@ -341,7 +125,7 @@ class AStar {
 
 module.exports = AStar;
 
-},{"./BinaryHeap":7,"./utils.js":10}],7:[function(require,module,exports){
+},{"./BinaryHeap":3,"./utils.js":6}],3:[function(require,module,exports){
 // javascript-astar
 // http://github.com/bgrins/javascript-astar
 // Freely distributable under the MIT License.
@@ -477,7 +261,7 @@ class BinaryHeap {
 
 module.exports = BinaryHeap;
 
-},{}],8:[function(require,module,exports){
+},{}],4:[function(require,module,exports){
 const utils = require('./utils');
 
 class Channel {
@@ -572,7 +356,7 @@ class Channel {
 
 module.exports = Channel;
 
-},{"./utils":10}],9:[function(require,module,exports){
+},{"./utils":6}],5:[function(require,module,exports){
 const utils = require('./utils');
 const AStar = require('./AStar');
 const Channel = require('./Channel');
@@ -892,7 +676,7 @@ module.exports = {
 	}
 };
 
-},{"./AStar":6,"./Channel":8,"./utils":10}],10:[function(require,module,exports){
+},{"./AStar":2,"./Channel":4,"./utils":6}],6:[function(require,module,exports){
 class Utils {
 
   static computeCentroids (geometry) {
@@ -1212,4 +996,220 @@ class Utils {
 
 module.exports = Utils;
 
-},{}]},{},[1]);
+},{}],7:[function(require,module,exports){
+module.exports = {
+  'nav-mesh':    require('./nav-mesh'),
+  'nav-controller':     require('./nav-controller'),
+  'system':      require('./system'),
+
+  registerAll: function (AFRAME) {
+    if (this._registered) return;
+
+    AFRAME = AFRAME || window.AFRAME;
+
+    if (!AFRAME.components['nav-mesh']) {
+      AFRAME.registerComponent('nav-mesh', this['nav-mesh']);
+    }
+
+    if (!AFRAME.components['nav-controller']) {
+      AFRAME.registerComponent('nav-controller',  this['nav-controller']);
+    }
+
+    if (!AFRAME.systems.nav) {
+      AFRAME.registerSystem('nav', this.system);
+    }
+
+    this._registered = true;
+  }
+};
+
+},{"./nav-controller":8,"./nav-mesh":9,"./system":10}],8:[function(require,module,exports){
+module.exports = {
+  schema: {
+    destination: {type: 'vec3'},
+    active: {default: false},
+    speed: {default: 2}
+  },
+  init: function () {
+    this.system = this.el.sceneEl.systems.nav;
+    this.system.addController(this);
+    this.path = [];
+    this.raycaster = new THREE.Raycaster();
+  },
+  remove: function () {
+    this.system.removeController(this);
+  },
+  update: function () {
+    this.path.length = 0;
+  },
+  tick: (function () {
+    var vDest = new THREE.Vector3();
+    var vDelta = new THREE.Vector3();
+    var vNext = new THREE.Vector3();
+
+    return function (t, dt) {
+      var el = this.el;
+      var data = this.data;
+      var raycaster = this.raycaster;
+      var speed = data.speed * dt / 1000;
+
+      if (!data.active) return;
+
+      // Use PatrolJS pathfinding system to get shortest path to target.
+      if (!this.path.length) {
+        this.path = this.system.getPath(this.el.object3D, vDest.copy(data.destination));
+        this.path = this.path || [];
+        el.emit('nav-start');
+      }
+
+      // If no path is found, exit.
+      if (!this.path.length) {
+        console.warn('[nav] Unable to find path to %o.', data.destination);
+        this.el.setAttribute('nav-controller', {active: false});
+        el.emit('nav-end');
+        return;
+      }
+
+      // Current segment is a vector from current position to next waypoint.
+      var vCurrent = el.object3D.position;
+      var vWaypoint = this.path[0];
+      vDelta.subVectors(vWaypoint, vCurrent);
+
+      var distance = vDelta.length();
+      var gazeTarget;
+
+      if (distance < speed) {
+        // If <1 step from current waypoint, discard it and move toward next.
+        this.path.shift();
+
+        // After discarding the last waypoint, exit pathfinding.
+        if (!this.path.length) {
+          this.el.setAttribute('nav-controller', {active: false});
+          el.emit('nav-end');
+          return;
+        } else {
+          gazeTarget = this.path[0];
+        }
+      } else {
+        // If still far away from next waypoint, find next position for
+        // the current frame.
+        vNext.copy(vDelta.setLength(speed)).add(vCurrent);
+        gazeTarget = vWaypoint;
+      }
+
+      // Look at the next waypoint.
+      gazeTarget.y = vCurrent.y;
+      el.object3D.lookAt(gazeTarget);
+
+      // Raycast against the nav mesh, to keep the controller moving along the
+      // ground, not traveling in a straight line from higher to lower waypoints.
+      raycaster.ray.origin.copy(vNext);
+      raycaster.ray.origin.y += 1.5;
+      raycaster.ray.direction.y = -1;
+      var intersections = raycaster.intersectObject(this.system.getNavMesh());
+
+      if (!intersections.length) {
+        // Raycasting failed. Step toward the waypoint and hope for the best.
+        vCurrent.copy(vNext);
+      } else {
+        // Re-project next position onto nav mesh.
+        vDelta.subVectors(intersections[0].point, vCurrent);
+        vCurrent.add(vDelta.setLength(speed));
+      }
+
+    };
+  }())
+};
+
+},{}],9:[function(require,module,exports){
+/**
+ * nav-mesh
+ *
+ * Waits for a mesh to be loaded on the current entity, then sets it as the
+ * nav mesh in the pathfinding system.
+ */
+module.exports = {
+  init: function () {
+    this.system = this.el.sceneEl.systems.nav;
+    this.loadNavMesh();
+    this.el.addEventListener('model-loaded', this.loadNavMesh.bind(this));
+  },
+
+  loadNavMesh: function () {
+    var object = this.el.getObject3D('mesh');
+
+    if (!object) return;
+
+    var navMesh;
+    object.traverse(function (node) {
+      if (node.isMesh) navMesh = node;
+    });
+
+    if (!navMesh) return;
+
+    this.system.setNavMesh(navMesh);
+  }
+};
+
+},{}],10:[function(require,module,exports){
+var Path = require('three-pathfinding');
+
+/**
+ * nav
+ *
+ * Pathfinding system, using PatrolJS.
+ */
+module.exports = {
+  init: function () {
+    this.navMesh = null;
+    this.nodes = null;
+    this.controllers = new Set();
+  },
+
+  /**
+   * @param {THREE.Mesh} mesh
+   */
+  setNavMesh: function (mesh) {
+    var geometry = mesh.geometry.isBufferGeometry
+      ? new THREE.Geometry().fromBufferGeometry(mesh.geometry)
+      : mesh.geometry;
+    this.navMesh = new THREE.Mesh(geometry);
+    this.nodes = Path.buildNodes(this.navMesh.geometry);
+    Path.setZoneData('level', this.nodes);
+  },
+
+  /**
+   * @return {THREE.Mesh}
+   */
+  getNavMesh: function () {
+    return this.navMesh;
+  },
+
+  /**
+   * @param {NavController} ctrl
+   */
+  addController: function (ctrl) {
+    this.controllers.add(ctrl);
+  },
+
+  /**
+   * @param {NavController} ctrl
+   */
+  removeController: function (ctrl) {
+    this.controllers.remove(ctrl);
+  },
+
+  /**
+   * @param  {NavController} ctrl
+   * @param  {THREE.Vector3} target
+   * @return {Array<THREE.Vector3>}
+   */
+  getPath: function (ctrl, target) {
+    var start = ctrl.el.object3D.position;
+    // TODO(donmccurdy): Current group should be cached.
+    var group = Path.getGroup('level', start);
+    return Path.findPath(start, target, 'level', group);
+  }
+};
+
+},{"three-pathfinding":5}]},{},[1]);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/aframe-extras.pathfinding.min.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/aframe-extras.primitives.min.js


+ 7 - 3
support/client/lib/vwf/model/aframe/extras/components/sphere-collider.js

@@ -28,6 +28,7 @@ module.exports = {
     this.collisions = [];
 
     this.handleHit = this.handleHit.bind(this);
+    this.handleHitEnd = this.handleHitEnd.bind(this);
   },
 
   remove: function () {
@@ -103,9 +104,7 @@ module.exports = {
       // Remove collision state from other elements.
       this.collisions.filter(function (el) {
         return !distanceMap.has(el);
-      }).forEach(function removeState (el) {
-        el.removeState(data.state);
-      });
+      }).forEach(this.handleHitEnd);
 
       // Store new collisions
       this.collisions = collisions;
@@ -145,6 +144,11 @@ module.exports = {
     targetEl.emit('hit');
     targetEl.addState(this.data.state);
     this.el.emit('hit', {el: targetEl});
+  },
+  handleHitEnd: function (targetEl) {
+    targetEl.emit('hitend');
+    targetEl.removeState(this.data.state);
+    this.el.emit('hitend', {el: targetEl});
   }
 };
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/components/sphere-collider.min.js


+ 0 - 150
support/client/lib/vwf/model/aframe/extras/components/three-model.js

@@ -1,150 +0,0 @@
-(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-AFRAME.registerComponent('three-model', require('./src/loaders/three-model'));
-},{"./src/loaders/three-model":2}],2:[function(require,module,exports){
-var DEFAULT_ANIMATION = '__auto__';
-
-/**
- * three-model
- *
- * Loader for THREE.js JSON format. Somewhat confusingly, there are two
- * different THREE.js formats, both having the .json extension. This loader
- * supports both, but requires you to specify the mode as "object" or "json".
- *
- * Typically, you will use "json" for a single mesh, and "object" for a scene
- * or multiple meshes. Check the console for errors, if in doubt.
- *
- * See: https://clara.io/learn/user-guide/data_exchange/threejs_export
- */
-module.exports = {
-  deprecated: true,
-
-  schema: {
-    src:               { type: 'asset' },
-    loader:            { default: 'object', oneOf: ['object', 'json'] },
-    enableAnimation:   { default: true },
-    animation:         { default: DEFAULT_ANIMATION },
-    animationDuration: { default: 0 },
-    crossorigin:       { default: '' }
-  },
-
-  init: function () {
-    this.model = null;
-    this.mixer = null;
-    console.warn('[three-model] Component is deprecated. Use json-model or object-model instead.');
-  },
-
-  update: function (previousData) {
-    previousData = previousData || {};
-
-    var loader,
-        data = this.data;
-
-    if (!data.src) {
-      this.remove();
-      return;
-    }
-
-    // First load.
-    if (!Object.keys(previousData).length) {
-      this.remove();
-      if (data.loader === 'object') {
-        loader = new THREE.ObjectLoader();
-        if (data.crossorigin) loader.setCrossOrigin(data.crossorigin);
-        loader.load(data.src, function(loaded) {
-          loaded.traverse( function(object) {
-            if (object instanceof THREE.SkinnedMesh)
-              loaded = object;
-          });
-          if(loaded.material)
-            loaded.material.skinning = !!((loaded.geometry && loaded.geometry.bones) || []).length;
-          this.load(loaded);
-        }.bind(this));
-      } else if (data.loader === 'json') {
-        loader = new THREE.JSONLoader();
-        if (data.crossorigin) loader.crossOrigin = data.crossorigin;
-        loader.load(data.src, function (geometry, materials) {
-
-          // Attempt to automatically detect common material options.
-          materials.forEach(function (mat) {
-            mat.vertexColors = (geometry.faces[0] || {}).color ? THREE.FaceColors : THREE.NoColors;
-            mat.skinning = !!(geometry.bones || []).length;
-            mat.morphTargets = !!(geometry.morphTargets || []).length;
-            mat.morphNormals = !!(geometry.morphNormals || []).length;
-          });
-
-          var mesh = (geometry.bones || []).length
-            ? new THREE.SkinnedMesh(geometry, new THREE.MultiMaterial(materials))
-            : new THREE.Mesh(geometry, new THREE.MultiMaterial(materials));
-
-          this.load(mesh);
-        }.bind(this));
-      } else {
-        throw new Error('[three-model] Invalid mode "%s".', data.mode);
-      }
-      return;
-    }
-
-    var activeAction = this.model && this.model.activeAction;
-
-    if (data.animation !== previousData.animation) {
-      if (activeAction) activeAction.stop();
-      this.playAnimation();
-      return;
-    }
-
-    if (activeAction && data.enableAnimation !== activeAction.isRunning()) {
-      data.enableAnimation ? this.playAnimation() : activeAction.stop();
-    }
-
-    if (activeAction && data.animationDuration) {
-        activeAction.setDuration(data.animationDuration);
-    }
-  },
-
-  load: function (model) {
-    this.model = model;
-    this.mixer = new THREE.AnimationMixer(this.model);
-    this.el.setObject3D('mesh', model);
-    this.el.emit('model-loaded', {format: 'three', model: model});
-
-    if (this.data.enableAnimation) this.playAnimation();
-  },
-
-  playAnimation: function () {
-    var clip,
-        data = this.data,
-        animations = this.model.animations || this.model.geometry.animations || [];
-
-    if (!data.enableAnimation || !data.animation || !animations.length) {
-      return;
-    }
-
-    clip = data.animation === DEFAULT_ANIMATION
-      ? animations[0]
-      : THREE.AnimationClip.findByName(animations, data.animation);
-
-    if (!clip) {
-      console.error('[three-model] Animation "%s" not found.', data.animation);
-      return;
-    }
-
-    this.model.activeAction = this.mixer.clipAction(clip, this.model);
-    if (data.animationDuration) {
-      this.model.activeAction.setDuration(data.animationDuration);
-    }
-    this.model.activeAction.play();
-  },
-
-  remove: function () {
-    if (this.mixer) this.mixer.stopAllAction();
-    if (this.model) this.el.removeObject3D('mesh');
-  },
-
-  tick: function (t, dt) {
-    if (this.mixer && !isNaN(dt)) {
-      this.mixer.update(dt / 1000);
-    }
-  }
-};
-
-},{}]},{},[1]);

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 0
support/client/lib/vwf/model/aframe/extras/components/three-model.min.js


+ 9 - 60
support/client/lib/vwf/model/aframeComponent.js

@@ -370,10 +370,6 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                             parentNodeAF.setAttribute(aframeObject.compName, 'enabled', propertyValue);
                             break;
 
-                        case "duration":
-                            parentNodeAF.setAttribute(aframeObject.compName, 'duration', propertyValue);
-                            break;
-
                         case "deltaPos":
                             parentNodeAF.setAttribute(aframeObject.compName, 'deltaPos', propertyValue);
                             break;
@@ -621,10 +617,6 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                             value = parentNodeAF.getAttribute(aframeObject.compName).enabled;
                             break;
 
-                        case "duration":
-                            value = parentNodeAF.getAttribute(aframeObject.compName).duration;
-                            break;
-
                         case "deltaPos":
                             value = parentNodeAF.getAttribute(aframeObject.compName).deltaPos;
                             break;
@@ -656,48 +648,6 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                 }
 
 
-                if (value === undefined && isGearVRControlsDefinition(node.prototypes)) {
-                    value = propertyValue;
-
-                    // let parentNodeAF = self.state.nodes[node.parentID].aframeObj;
-                    let parentNodeAF = aframeObject.el;
-
-                    switch (propertyName) {
-
-                        case "armModel":
-
-                            value = AFRAME.utils.coordinates.stringify(parentNodeAF.getAttribute(aframeObject.compName).armModel);
-                            break;
-
-                        case "buttonColor":
-                            value = AFRAME.utils.coordinates.stringify(parentNodeAF.getAttribute(aframeObject.compName).buttonColor);
-                            break;
-
-                        case "buttonTouchedColor":
-                            value = parentNodeAF.getAttribute(aframeObject.compName).buttonTouchedColor;
-                            break;
-
-                        case "buttonHighlightColor":
-                            value = parentNodeAF.getAttribute(aframeObject.compName).buttonHighlightColor;
-                            break;
-
-                        case "hand":
-                            value = parentNodeAF.getAttribute(aframeObject.compName).hand;
-                            break;
-
-                        case "model":
-                            value = parentNodeAF.getAttribute(aframeObject.compName).model;
-                            break;
-
-                        case "rotationOffset":
-                            value = parentNodeAF.getAttribute(aframeObject.compName).rotationOffset;
-                            break;
-
-                    }
-                }
-
-
-
             }
 
             if (value !== undefined) {
@@ -858,6 +808,15 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
 
         }
 
+        if (self.state.isComponentClass(protos, "http://vwf.example.com/aframe/streamSoundComponent.vwf")) {
+            
+            
+            // aframeObj.el.setAttribute(node.type, {});
+            aframeObj.compName = "streamsound";
+            aframeObj.el.setAttribute(aframeObj.compName, {});
+
+        }
+
         if (self.state.isComponentClass(protos, "http://vwf.example.com/aframe/linepath.vwf")) {
             
             
@@ -923,16 +882,6 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
  
          }
 
-
-        if (self.state.isComponentClass(protos, "http://vwf.example.com/aframe/gearvr-controlsComponent.vwf")) {
-
-
-            // aframeObj.el.setAttribute(node.type, {});
-            aframeObj.compName = "gearvr-controls";
-            aframeObj.el.setAttribute(aframeObj.compName, {});
-
-        }
-
         if (self.state.isComponentClass(protos, "http://vwf.example.com/aframe/app-raycaster-listener-component.vwf")) {
 
 

+ 225 - 50
support/client/lib/vwf/view/aframe.js

@@ -32,8 +32,15 @@ define(["module", "vwf/view"], function (module, view) {
             self = this;
             this.nodes = {};
 
+            // this.tickTime = 0;
+            // this.realTickDif = 50;
+            // this.lastrealTickDif = 50;
+            // this.lastRealTick = performance.now();
+
             this.state.appInitialized = false;
 
+            if (options === undefined) { options = {}; }
+
             if (typeof options == "object") {
 
                 this.rootSelector = options["application-root"];
@@ -42,7 +49,9 @@ define(["module", "vwf/view"], function (module, view) {
                 this.rootSelector = options;
             }
 
-         
+            this.gearvr = options.gearvr !== undefined ? options.gearvr : false;
+            this.wmrright = options.wmrright !== undefined ? options.wmrright : false;
+            this.wmrleft = options.wmrleft !== undefined ? options.wmrleft : false;
         },
 
         createdNode: function (nodeID, childID, childExtendsID, childImplementsIDs,
@@ -58,15 +67,51 @@ define(["module", "vwf/view"], function (module, view) {
 
             if (this.state.scenes[childID]) {
                 let scene = this.state.scenes[childID];
+              
+
                 document.body.appendChild(scene); //append is not working in Edge browser
                 createAvatarControl(scene);
                 createAvatar.call(this, childID);
-               // this.state.appInitialized  = true;
+
+                // this.state.appInitialized  = true;
+
+                if (this.gearvr == true) {
+                    console.log("CREATE GEARVR HERE!!");
+                    if (AFRAME.utils.device.isGearVR()) {
+                        let nodeName = 'gearvr-' + self.kernel.moniker();
+                        createGearVRControls();
+                        createGearVRController.call(this, childID, nodeName);
+                    }
+                }
+
+                if (this.wmrright == true) {
+                    console.log("CREATE WMR RIGHT HERE!!");
+                    if (AFRAME.utils.device.checkHasPositionalTracking()) {
+                        let nodeName = 'wmrvr-right-' + self.kernel.moniker();
+                        createWMRVRControls('right');
+                        createWMRVR.call(this, childID, nodeName);
+                    }
+                }
+
+                if (this.wmrleft == true) {
+                    console.log("CREATE WMR LEFT HERE!!");
+                    if (AFRAME.utils.device.checkHasPositionalTracking()) {
+                        let nodeName = 'wmrvr-left-' + self.kernel.moniker();
+                        createWMRVRControls('left');
+                        createWMRVR.call(this, childID, nodeName);
+                    }
+                }
+
             }
 
             if (this.state.nodes[childID] && this.state.nodes[childID].aframeObj) {
-                    this.nodes[childID] = {id:childID,extends:childExtendsID};
-                }
+                this.nodes[childID] = { 
+                    id: childID, 
+                    extends: childExtendsID,
+                    // lastTransformStep: 0,
+                    // lastAnimationStep: 0 
+                };
+            }
 
             // if(this.state.nodes[childID]) {
             //     this.nodes[childID] = {id:childID,extends:childExtendsID};
@@ -78,16 +123,13 @@ define(["module", "vwf/view"], function (module, view) {
         },
 
 
-        initializedNode: function( nodeID, childID ) {
+        initializedNode: function (nodeID, childID) {
 
             var node = this.state.nodes[childID];
             if (!node) {
                 return;
             }
 
-
-          
-
         },
 
         createdProperty: function (nodeId, propertyName, propertyValue) {
@@ -101,12 +143,17 @@ define(["module", "vwf/view"], function (module, view) {
         satProperty: function (nodeId, propertyName, propertyValue) {
             var selfs = this;
 
-             var node = this.state.nodes[ nodeId ];
+            var node = this.state.nodes[nodeId];
 
-            if ( !( node && node.aframeObj ) ) {
+            if (!(node && node.aframeObj)) {
                 return;
             }
 
+            // if (propertyName == 'position') {
+            //     this.nodes[nodeId].lastTransformStep = vwf.time();
+            // }
+
+
             // var aframeObject = node.aframeObj;
             // switch (propertyName) {
             //     case "clickable":
@@ -132,25 +179,25 @@ define(["module", "vwf/view"], function (module, view) {
 
                 let avatarID = self.kernel.moniker();
                 var nodeName = 'avatar-' + avatarID;
-        
+
                 var newNode = {
                     "id": avatarName,
                     "uri": avatarName,
                     "extends": "http://vwf.example.com/aframe/avatar.vwf",
-                    "properties":{
+                    "properties": {
                         "localUrl": '',
-                        "remoteUrl":'',
+                        "remoteUrl": '',
                         "displayName": randId(),
                         "sharing": { audio: true, video: true }
                     }
                 }
-                
-               
+
+
                 if (!self.state.nodes[avatarName]) {
 
-                vwf_view.kernel.createChild(nodeID, avatarName, newNode);
-                vwf_view.kernel.callMethod(avatarName, "createAvatarBody", []);
-                //"/../assets/avatars/male/avatar1.gltf"
+                    vwf_view.kernel.createChild(nodeID, avatarName, newNode);
+                    vwf_view.kernel.callMethod(avatarName, "createAvatarBody", []);
+                    //"/../assets/avatars/male/avatar1.gltf"
                 }
 
             }
@@ -167,80 +214,184 @@ define(["module", "vwf/view"], function (module, view) {
         ticked: function (vwfTime) {
 
             updateAvatarPosition();
-           
-            //lerpTick ()
+
+            //update vr controllers
+            if (this.gearvr == true) {
+                updateHandControllerVR('gearvr-', '#gearvrcontrol');
+            }
+            if (this.wmrright == true) {
+                updateHandControllerVR('wmrvr-right-', '#wmrvrcontrolright');
+            }
+            if (this.wmrleft == true) {
+                updateHandControllerVR('wmrvr-left-', '#wmrvrcontrolleft');
+            }
+
+           // console.log(vwfTime);
+            //lerpTick ();
         }
 
 
     });
 
 
+    function compareCoordinates(a, b) {
+        return a.x !== b.x || a.y !== b.y || a.z !== b.z
+
+    }
+
     function updateAvatarPosition() {
 
         let avatarName = 'avatar-' + self.kernel.moniker();
+        var node = self.state.nodes[avatarName];
+        if (!node) return;
+        if (!node.aframeObj) return;
+
         let el = document.querySelector('#avatarControl');
         if (el) {
-            let postion = el.getAttribute('position');
+            let position = el.getAttribute('position');
+            let rotation = el.getAttribute('rotation');
+
+            let lastRotation = self.nodes[avatarName].selfTickRotation;
+
+            let currentPosition = node.aframeObj.getAttribute('position');
+            let currentRotation = node.aframeObj.getAttribute('rotation');
+
+            if (position && rotation && currentPosition && currentRotation && lastRotation) {
+            if (compareCoordinates(position, currentPosition) || rotation.y !== lastRotation.y) {
+                    console.log("not equal!!")
+                    vwf_view.kernel.callMethod(avatarName, "followAvatarControl", [position, rotation]);
+                }
+            }
+            self.nodes[avatarName].selfTickRotation = Object.assign({}, el.getAttribute('rotation'));
+        }
+
+    }
+
+
+    function updateHandControllerVR(aName, aSelector) {
+        //let avatarName = 'avatar-' + self.kernel.moniker();
+        let avatarName = aName + self.kernel.moniker();
+        var node = self.state.nodes[avatarName];
+        if (!node) return;
+        if (!node.aframeObj) return;
+
+        let el = document.querySelector(aSelector);
+        if (el) {
+            let position = el.getAttribute('position');
             let rotation = el.getAttribute('rotation');
 
-            if ( postion && rotation) {
-                //[postion.x, postion.y, postion.z] //[rotation.x, rotation.y, rotation.z]
+            let lastRotation = self.nodes[avatarName].selfTickRotation;
+            let lastPosition = self.nodes[avatarName].selfTickPosition;
+
+           // let currentPosition = node.aframeObj.getAttribute('position');
+            //let currentRotation = node.aframeObj.getAttribute('rotation');
 
-                vwf_view.kernel.callMethod(avatarName, "followAvatarControl", [postion, rotation]);
+            if (position && rotation && lastRotation && lastPosition) {
+                if (compareCoordinates(position, lastPosition) || compareCoordinates(rotation, lastRotation)) {
+                    console.log("not equal!!");
 
-            // vwf_view.kernel.setProperty(avatarName, "position", AFRAME.utils.coordinates.stringify(postion));
-            // vwf_view.kernel.setProperty(avatarName, "rotation", AFRAME.utils.coordinates.stringify(rotation));
+                    vwf_view.kernel.callMethod(avatarName, "updateVRControl", [position, rotation]);
+                   // vwf_view.kernel.setProperty(avatarName, "position", AFRAME.utils.coordinates.stringify(position));
+                    //vwf_view.kernel.setProperty(avatarName, "rotation", AFRAME.utils.coordinates.stringify(rotation));
+                }
             }
+
+            self.nodes[avatarName].selfTickRotation = Object.assign({}, el.getAttribute('rotation'));
+            self.nodes[avatarName].selfTickPosition = Object.assign({}, el.getAttribute('position'));
+
         }
     }
 
+
     function createAvatarControl(aScene) {
 
         let avatarName = 'avatar-' + self.kernel.moniker();
 
         let controlEl = document.createElement('a-camera');
-       // controlEl.setAttribute('avatar', '');
+        // controlEl.setAttribute('avatar', '');
         controlEl.setAttribute('id', 'avatarControl');
-        controlEl.setAttribute('wasd-controls', {});
-        controlEl.setAttribute('look-controls', {});
-        controlEl.setAttribute('gamepad-controls', {});
+        controlEl.setAttribute('wasd-controls-enabled', true);
+        controlEl.setAttribute('look-controls-enabled', true);
+        controlEl.setAttribute('user-height', 1.6);
+       controlEl.setAttribute('gamepad-controls', {'controller': 0});
+        //controlEl.setAttribute('universal-controls', {});
+        
+        //controlEl.setAttribute('gearvr-controls',{});
         controlEl.setAttribute('camera', 'active', true);
-        controlEl.setAttribute('camera', 'near', 0.51);
+       // controlEl.setAttribute('camera', 'userHeight', 1.6);
+       // controlEl.setAttribute('camera', 'near', 0.51);
 
         aScene.appendChild(controlEl);
 
         let cursorEl = document.createElement('a-cursor');
-        cursorEl.setAttribute('id', 'cursor-'+avatarName);
+        cursorEl.setAttribute('id', 'cursor-' + avatarName);
         cursorEl.setAttribute('raycaster', {});
         cursorEl.setAttribute('raycaster', 'objects', '.intersectable');
         cursorEl.setAttribute('raycaster', 'showLine', false);
 
-       // cursorEl.setAttribute('raycaster', {objects: '.intersectable', showLine: true, far: 100});
-       // cursorEl.setAttribute('raycaster', 'showLine', true);
+        // cursorEl.setAttribute('raycaster', {objects: '.intersectable', showLine: true, far: 100});
+        // cursorEl.setAttribute('raycaster', 'showLine', true);
         controlEl.appendChild(cursorEl);
 
         // let gearVRControlsEl = document.createElement('a-entity');
         // gearVRControlsEl.setAttribute('id', 'gearvr-'+avatarName);
         // gearVRControlsEl.setAttribute('gearvr-controls', {});
         // aScene.appendChild(gearVRControlsEl);
-        
 
-       
-    //     controlEl.addEventListener('componentchanged', function (evt) {
-    //     if (evt.detail.name === 'position') {
-    //         var eventParameters = evt.detail.newData;
-    //          vwf_view.kernel.setProperty(avatarName, "position", [eventParameters.x, eventParameters.y, eventParameters.z]);
-    //     }
 
-    //      if (evt.detail.name === 'rotation') {
-    //         var eventParameters = evt.detail.newData;
-    //            vwf_view.kernel.setProperty(avatarName, "rotation", [eventParameters.x, eventParameters.y, eventParameters.z]);
-    //     }
 
-    // });
+        //     controlEl.addEventListener('componentchanged', function (evt) {
+        //     if (evt.detail.name === 'position') {
+        //         var eventParameters = evt.detail.newData;
+        //          vwf_view.kernel.setProperty(avatarName, "position", [eventParameters.x, eventParameters.y, eventParameters.z]);
+        //     }
+
+        //      if (evt.detail.name === 'rotation') {
+        //         var eventParameters = evt.detail.newData;
+        //            vwf_view.kernel.setProperty(avatarName, "rotation", [eventParameters.x, eventParameters.y, eventParameters.z]);
+        //     }
+
+        // });
 
     }
 
+    function createWMRVR (nodeID, nodeName) { 
+
+        var newNode = {
+            "id": nodeName,
+            "uri": nodeName,
+            "extends": "http://vwf.example.com/aframe/wmrvrcontroller.vwf",
+            "properties": {
+            }
+        }
+
+        if (!self.state.nodes[nodeName]) {
+
+            vwf_view.kernel.createChild(nodeID, nodeName, newNode);
+            vwf_view.kernel.callMethod(nodeName, "createController", []);
+            //"/../assets/controller/wmrvr.gltf"
+        }
+    }
+
+
+    function createGearVRController(nodeID, nodeName) {
+
+            var newNode = {
+                "id": nodeName,
+                "uri": nodeName,
+                "extends": "http://vwf.example.com/aframe/gearvrcontroller.vwf",
+                "properties": {
+                }
+            }
+
+            if (!self.state.nodes[nodeName]) {
+
+                vwf_view.kernel.createChild(nodeID, nodeName, newNode);
+                vwf_view.kernel.callMethod(nodeName, "createController", []);
+                //"/../assets/controller/gearvr.gltf"
+            }
+    }
+
     function createAvatar(nodeID) {
 
         vwf_view.kernel.fireEvent(nodeID, "createAvatar")
@@ -253,15 +404,39 @@ define(["module", "vwf/view"], function (module, view) {
         //     "uri": nodeName,
         //     "extends": "http://vwf.example.com/aframe/avatar.vwf"
         // }
-        
-       
+
+
         // vwf_view.kernel.createChild(nodeID, nodeName, newNode);
         // vwf_view.kernel.callMethod(nodeName, "createAvatarBody");
     }
 
     function randId() {
         return '_' + Math.random().toString(36).substr(2, 9);
-   }
-    
+    }
+
+    function createGearVRControls() {
+
+        let sceneEl = document.querySelector('a-scene');
+        let gearvr = document.createElement('a-entity');
+        gearvr.setAttribute('id', 'gearvrcontrol');
+        gearvr.setAttribute('gearvr-controls', '');
+        gearvr.setAttribute('gearvr-controls', 'hand', 'right');
+        gearvr.setAttribute('gearvrcontrol', '');
+        sceneEl.appendChild(gearvr);
+
+    }
+
+    function createWMRVRControls(hand) {
+
+        let sceneEl = document.querySelector('a-scene');
+        let wmrvr = document.createElement('a-entity');
+        wmrvr.setAttribute('id', 'wmrvrcontrol' + hand);
+        wmrvr.setAttribute('windows-motion-controls', '');
+        wmrvr.setAttribute('windows-motion-controls', 'hand', hand);
+        wmrvr.setAttribute('wmrvrcontrol', {'hand': hand});
+        sceneEl.appendChild(wmrvr);
+    }
+
+
 
 });

+ 75 - 6
support/client/lib/vwf/view/aframeComponent.js

@@ -22,15 +22,21 @@
 /// @requires vwf/view
 
 define(["module", "vwf/view"], function (module, view) {
+    var self;
 
     return view.load(module, {
 
         // == Module Definition ====================================================================
 
         initialize: function (options) {
-            var self = this;
+            self = this;
             this.nodes = {};
 
+            this.tickTime = 0;
+            this.realTickDif = 50;
+            this.lastrealTickDif = 50;
+            this.lastRealTick = performance.now();
+
             this.state.appInitialized = false;
             
                         if (typeof options == "object") {
@@ -62,7 +68,10 @@ define(["module", "vwf/view"], function (module, view) {
                 this.nodes[childID] = {id:childID,extends:childExtendsID};
             } 
             else if (this.state.nodes[childID] && this.state.nodes[childID].aframeObj) {
-                this.nodes[childID] = {id:childID,extends:childExtendsID};
+                this.nodes[childID] = {
+                    id:childID,
+                    extends:childExtendsID
+                };
             }
           
         },
@@ -94,8 +103,7 @@ define(["module", "vwf/view"], function (module, view) {
                             return;
                         }
 
-
-
+                        
             switch (propertyName) {
                 case "color":
                     if (propertyValue) {
@@ -113,12 +121,73 @@ define(["module", "vwf/view"], function (module, view) {
 
         ticked: function (vwfTime) {
 
-            
+            lerpTick ();
 
         }
 
     });
 
-   
+    function lerpTick () {
+        var now = performance.now();
+        self.realTickDif = now - self.lastRealTick;
+
+        self.lastRealTick = now;
+ 
+        //reset - loading can cause us to get behind and always but up against the max prediction value
+       // self.tickTime = 0;
+
+       let interNodes = Object.entries(self.nodes).filter(node => 
+        node[1].extends == 'http://vwf.example.com/aframe/interpolation-component.vwf');
+
+       interNodes.forEach(node => {
+           let nodeID = node[0];
+        if ( self.state.nodes[nodeID] ) {      
+            self.nodes[nodeID].tickTime = 0;
+            if(!self.nodes[nodeID].interpolate)
+            {
+                self.nodes[nodeID].interpolate = {
+                    'position': {},
+                    'rotation': {}
+                }
+            }
+            self.nodes[nodeID].interpolate.position.lastTick = (self.nodes[nodeID].interpolate.position.selfTick);
+            self.nodes[nodeID].interpolate.position.selfTick = getPosition(nodeID);
+
+            self.nodes[nodeID].interpolate.rotation.lastTick = (self.nodes[nodeID].interpolate.rotation.selfTick);
+            self.nodes[nodeID].interpolate.rotation.selfTick = getRotation(nodeID);
+            //console.log(self.nodes[nodeID].interpolate.rotation.selfTick);
+            //self.nodes[nodeID].lastTickTransform = self.nodes[nodeID].selfTickTransform;
+            //self.nodes[nodeID].selfTickTransform = getTransform(nodeID);
+        }
+       })
+
+        // for ( var nodeID in interNodes ) {
+        //     if ( self.state.nodes[nodeID] ) {      
+        //         self.nodes[nodeID].tickTime = 0;
+        //         self.nodes[nodeID].lastTickTransform = self.nodes[nodeID].selfTickTransform;
+        //         self.nodes[nodeID].selfTickTransform = getTransform(nodeID);
+        //     }
+        // }
+
+
+    }
+
+    function getRotation(id) {
+        let r = new THREE.Euler();
+        let rot = r.copy(self.state.nodes[id].aframeObj.el.object3D.rotation);
+        //let rot = self.state.nodes[id].aframeObj.el.getAttribute('rotation');
+        //let interp = (new THREE.Vector3()).fromArray(Object.values(rot))//goog.vec.Mat4.clone(self.state.nodes[id].threeObject.matrix.elements);
+        
+        return rot;
+    }
+
+    function getPosition(id) {
+        let p = new THREE.Vector3();
+        let pos = p.copy(self.state.nodes[id].aframeObj.el.object3D.position);
+        //let pos = self.state.nodes[id].aframeObj.el.getAttribute('position');
+        //let interp = (new THREE.Vector3()).fromArray(Object.values(pos))//goog.vec.Mat4.clone(self.state.nodes[id].threeObject.matrix.elements);
+        
+        return pos;
+    }
 
 });

+ 226 - 57
support/client/lib/vwf/view/editor-new.js

@@ -26,8 +26,9 @@ define([
     "vwf/view",
     "vwf/utility",
     "vwf/view/lib/ace/ace",
-    "vwf/view/lib/colorpicker/colorpicker.min"
-], function (module, version, view, utility, ace, colorpicker) {
+    "vwf/view/lib/colorpicker/colorpicker.min",
+    "vwf/view/widgets"
+], function (module, version, view, utility, ace, colorpicker, widgets) {
 
     var self;
 
@@ -38,6 +39,7 @@ define([
         initialize: function () {
             self = this;
             this.ace = window.ace;
+            this.widgets = widgets;
 
             this.nodes = {};
             this.scenes = {};
@@ -346,8 +348,40 @@ define([
 
 
                                     ]
-                                }
+                                },
+                                {
+                                    $cell: true,
+                                    $type: "div",
+                                    class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-12",
+                                    $components: [
+                                        {
+                                            $cell: true,
+                                            $type: "button",
+                                            class: "mdc-button mdc-button--raised",
+                                            $text: "Wide",
+                                            onclick: function (e) {
+                                                let avatarID = 'avatar-'+vwf.moniker_;
+                                                vwf_view.kernel.callMethod(avatarID, "setBigVideoHead", []);
+                                               
+                                            }
+            
+                                        },
+                                        {
+                                            $cell: true,
+                                            $type: "button",
+                                            class: "mdc-button mdc-button--raised",
+                                            $text: "Small",
+                                            onclick: function (e) {
+                                                let avatarID = 'avatar-'+vwf.moniker_;
+                                                vwf_view.kernel.callMethod(avatarID, "setSmallVideoHead", []);
+                                               
+                                            }
+            
+                                        }
+
+                                    ]
 
+                                }
                             ]
                         }
                     ]
@@ -1039,7 +1073,7 @@ define([
                 class: "mdc-list-divider",
             }
 
-            let gizmoEdit = {
+            let webrtcGUI = {
 
                 $type: "div",
                 class: "propGrid mdc-layout-grid max-width mdc-layout-grid--align-left",
@@ -1051,67 +1085,200 @@ define([
                         $components: [
                             {
                                 $type: "div",
-                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-2",
+                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-3",
                                 $components: [
                                     {
+                                    $type: "span",
+                                    $text: "Chat"
+                                }
 
-                                        $cell: true,
-                                        $type: "span",
-                                        $text: "Edit: ",
-
-                                    }
                                 ]
                             },
                             {
                                 $type: "div",
-                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-7",
+                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-3",
                                 $components: [
-                                    {
-                                        $cell: true,
-                                        $type: "div",
-                                        class: "mdc-switch",
-                                        $components: [
-                                            {
-                                                $type: "input",
-                                                type: "checkbox",
-                                                class: "mdc-switch__native-control",
-                                                id: 'editnode',
-                                                $init: function () {
+                                    widgets.icontoggle({
+                                        'id': "webrtcswitch",
+                                        'label': 'visibility',
+                                        'on': JSON.stringify({"content": "visibility", "label": "Turn On Mic"}),
+                                        'off': JSON.stringify({"content": "visibility_off", "label": "Turn Off Mic"}),
+                                        'state': false,
+                                        'init': function(){
+                                            this._driver = vwf.views["vwf/view/webrtc"];
+                                            if (!this._driver) {
+                                                this.classList.add('mdc-icon-toggle--disabled');
 
-                                                    vwf_view.kernel.getProperty(this._currentNode, 'edit');
+                                            }
 
-                                                },
-                                                //id: "basic-switch",
-                                                onchange: function (e) {
+                                            this.addEventListener('MDCIconToggle:change', (e) => {
+                                                
+                                                let driver = e.target._driver;
+                                                let chkAttr = e.detail.isOn;
+                                                let avatarID = 'avatar-' + self.kernel.moniker();
 
-                                                    var nodeID = document.querySelector('#currentNode')._currentNode;
-                                                    let chkAttr = this.getAttribute('checked');
-                                                    if (chkAttr == "") {
-                                                        self.kernel.setProperty(this._currentNode, 'edit', false);
+                                                let micToggle = document.querySelector('#webrtcaudio');
+                                                let camToggle = document.querySelector('#webrtcvideo');
 
-                                                    } else {
-                                                        self.kernel.setProperty(this._currentNode, 'edit', true);
-                                                    }
+                                                if (chkAttr) {
+                                                    driver.startWebRTC(avatarID);
 
-                                                    vwf_view.kernel.callMethod(nodeID, "showCloseGizmo");
+                                                    micToggle.classList.remove('mdc-icon-toggle--disabled');
+                                                    camToggle.classList.remove('mdc-icon-toggle--disabled');
 
+                                                    console.log("on")
+            
+                                                } else {
+                                                    driver.stopWebRTC(avatarID);
 
+                                                    micToggle.classList.add('mdc-icon-toggle--disabled');
+                                                    camToggle.classList.add('mdc-icon-toggle--disabled');
+                                                    console.log("off")
                                                 }
-                                            },
-                                            {
-                                                $type: "div",
-                                                class: "mdc-switch__background",
-                                                $components: [
-                                                    {
-                                                        $type: "div",
-                                                        class: "mdc-switch__knob"
-                                                    }
-                                                ]
+
+                                               //console.log(e, detail)
+                                                //status.textContent = `Icon Toggle is ${detail.isOn ? 'on' : 'off'}`;
+                                              });
+                                            
+                                        }
+                                    })
+                                ]
+                            },
+                            {
+                                $type: "div",
+                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-3",
+                                $components: [
+                                    widgets.icontoggle({
+                                        'id': "webrtcaudio",
+                                        'label': 'mic',
+                                        'on': JSON.stringify({"content": "mic", "label": "Turn On Mic"}),
+                                        'off': JSON.stringify({"content": "mic_off", "label": "Turn Off Mic"}),
+                                        'state': false,
+                                        'init': function(){
+                                            this._driver = vwf.views["vwf/view/webrtc"];
+                                            let webrtcswitch = document.querySelector('#webrtcswitch');
+
+                                            if (!this._driver) {
+                                                this.classList.add('mdc-icon-toggle--disabled');
                                             }
-                                        ]
+                                            this.classList.add('mdc-icon-toggle--disabled');
+                                            this.addEventListener('MDCIconToggle:change', (e) => {
+                                                
+                                                let driver = e.target._driver;
+                                                let chkAttr = e.detail.isOn;
+                                                if (chkAttr) {
+                                                    driver.muteAudio(chkAttr);
+                                                    console.log("on")
+            
+                                                } else {
+                                                    driver.muteAudio(chkAttr);
+                                                    console.log("off")
+                                                }
+
+                                               //console.log(e, detail)
+                                                //status.textContent = `Icon Toggle is ${detail.isOn ? 'on' : 'off'}`;
+                                              });
+                                            
+                                        }
+                                    })
+                                ]
+                            },
+                            {
+                                $type: "div",
+                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-3",
+                                $components: [
+                                    widgets.icontoggle({
+                                        'id': "webrtcvideo",
+                                        'label': 'videocam',
+                                        'on': JSON.stringify({"content": "videocam", "label": "Turn On Video"}),
+                                        'off': JSON.stringify({"content": "videocam_off", "label": "Turn Off Video"}),
+                                        'state': false,
+                                        'init': function(){
+                                            this._driver = vwf.views["vwf/view/webrtc"];
+
+                                            if (!this._driver) {
+                                                this.classList.add('mdc-icon-toggle--disabled');
+                                            }
+
+                                            this.classList.add('mdc-icon-toggle--disabled');
+                                            this.addEventListener('MDCIconToggle:change', (e) => {
+                                                
+                                                let driver = e.target._driver;
+                                                let chkAttr = e.detail.isOn;
+                                                if (chkAttr) {
+                                                    driver.muteVideo(chkAttr);
+                                                    console.log("on")
+            
+                                                } else {
+                                                    driver.muteVideo(chkAttr);
+                                                    console.log("off")
+                                                }
+
+                                               //console.log(e, detail)
+                                                //status.textContent = `Icon Toggle is ${detail.isOn ? 'on' : 'off'}`;
+                                              });
+                                            
+                                        }
+                                    })
+                                ]
+                            }
+                           
+                        ]
+                    }
+
+                ]
+            }
+
+
+            let gizmoEdit = {
+
+                $type: "div",
+                class: "propGrid mdc-layout-grid max-width mdc-layout-grid--align-left",
+                $components: [
+                    {
+
+                        $type: "div",
+                        class: "mdc-layout-grid__inner",
+                        $components: [
+                            {
+                                $type: "div",
+                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-2",
+                                $components: [
+                                    {
+
+                                        $cell: true,
+                                        $type: "span",
+                                        $text: "Edit: ",
+
                                     }
                                 ]
                             },
+                            {
+                                $type: "div",
+                                class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-7",
+                                $components: [
+                                    widgets.switch({
+                                    'id': 'editnode', 
+                                    'init': function(){
+                                        vwf_view.kernel.getProperty(this._currentNode, 'edit');
+                                    },
+                                    'onchange': function(e){
+
+                                        var nodeID = document.querySelector('#currentNode')._currentNode;
+                                        let chkAttr = this.getAttribute('checked');
+                                        if (chkAttr == "") {
+                                            self.kernel.setProperty(this._currentNode, 'edit', false);
+    
+                                        } else {
+                                            self.kernel.setProperty(this._currentNode, 'edit', true);
+                                        }
+    
+                                        vwf_view.kernel.callMethod(nodeID, "showCloseGizmo");
+                                    }
+                                }
+                                )
+                                ]
+                            },
                             {
                                 $type: "div",
                                 class: "mdc-layout-grid__cell mdc-layout-grid__cell--span-1",
@@ -1278,7 +1445,7 @@ define([
                                                         $text: "Active",
                                                         onclick: function (e) {
                                                             let camera = document.querySelector('#' + this._currentNode);
-                                                            camera.setAttribute('active', true);
+                                                            camera.setAttribute('camera', 'active', true);
                                                         }
 
                                                     }
@@ -2480,18 +2647,13 @@ define([
 
                                 ]
                             },
-                            {
-                                $cell: true,
-                                $type: "hr",
-                                class: "mdc-list-divider",
-                            },
-                            {
-                                $cell: true,
-                                $type: "h3",
-                                class: "userList mdc-list-group__subheader",
-                                $text: "Users online"
-                            },
+                            widgets.divider,
+                            webrtcGUI,
+                            widgets.divider,
+                            widgets.headerH3("h3", "Users online", "userList mdc-list-group__subheader"),
                             clientListCell
+                            //widgets.headerH3("h3", "WebRTC", "userList mdc-list-group__subheader"),
+                            
                         ]
                     }
 
@@ -2572,6 +2734,13 @@ define([
             // let drawer = new mdc.drawer.MDCTemporaryDrawer(document.querySelector('.mdc-temporary-drawer'));
             // document.querySelector('.menu').addEventListener('click', () => drawer.open = true);
 
+        var toggleNodes = document.querySelectorAll('.mdc-icon-toggle');
+        toggleNodes.forEach( el => {
+            mdc.iconToggle.MDCIconToggle.attachTo(el);
+        });
+
+
+
             var drawerEl = document.querySelector('.mdc-temporary-drawer');
             var MDCTemporaryDrawer = mdc.drawer.MDCTemporaryDrawer;
             var drawer = new MDCTemporaryDrawer(drawerEl);

+ 1102 - 0
support/client/lib/vwf/view/webrtc.1.js

@@ -0,0 +1,1102 @@
+"use strict";
+
+// Copyright 2012 United States Government, as represented by the Secretary of Defense, Under
+// Secretary of Defense (Personnel & Readiness).
+// 
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+// in compliance with the License. You may obtain a copy of the License at
+// 
+//   http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software distributed under the License
+// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+// or implied. See the License for the specific language governing permissions and limitations under
+// the License.
+
+/// @module vwf/view/webrtc
+/// @requires vwf/view
+
+define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ], function( module, view, utility, Color, $ ) {
+
+    return view.load( module, {
+
+        // == Module Definition ====================================================================
+
+        initialize: function( options ) {
+
+            if ( !this.state ) {   
+                this.state = {};
+            }
+            
+            this.state.clients = {};
+            this.state.instances = {};
+            this.local = {
+                "ID": undefined,
+                "url": undefined,
+                "stream": undefined,
+                "sharing": { audio: true, video: true } 
+            };
+
+            // turns on logger debugger console messages 
+            this.debugVwf = {
+                "creation": false,
+                "initializing": false,
+                "parenting": false,
+                "deleting": false,
+                "properties": false,
+                "setting": false,
+                "getting": false,
+                "calling": false
+            };
+
+            if ( options === undefined ) { options = {}; }
+
+            this.stereo = options.stereo !== undefined  ? options.stereo : false;
+            this.videoElementsDiv = options.videoElementsDiv !== undefined  ? options.videoElementsDiv : 'videoSurfaces';
+            this.videoProperties = options.videoProperties !== undefined  ? options.videoProperties : {};
+            this.bandwidth = options.bandwidth;
+            this.iceServers = options.iceServers !== undefined  ? options.iceServers : [ { "url": "stun:stun.l.google.com:19302" } ];
+            this.debug = options.debug !== undefined ? options.debug : false;
+
+            this.videosAdded = 0;
+            this.msgQueue = [];
+
+        },
+  
+        createdNode: function( nodeID, childID, childExtendsID, childImplementsIDs,
+            childSource, childType, childIndex, childName, callback /* ( ready ) */ ) {
+
+            if ( this.debugVwf.creation ) {
+                this.kernel.logger.infox( "createdNode", nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childName );
+            }
+
+            if ( childExtendsID === undefined )
+                return;
+
+            var self = this, node;
+            
+            var protos = getPrototypes.call( self, childExtendsID )
+   
+            if ( isClientInstanceDef.call( this, protos ) && childName ) {
+                
+                node = {
+                    "parentID": nodeID,
+                    "ID": childID,
+                    "extendsID": childExtendsID,
+                    "implementsIDs": childImplementsIDs,
+                    "source": childSource,
+                    "type": childType,
+                    "name": childName,
+                    "prototypes": protos,
+                };
+
+                this.state.instances[ childID ] = node;
+
+            } else if ( isClientDefinition.call( this, protos ) && childName ) {
+
+                // check if this instance of client and if this client is for this instance
+                // create a login for this 
+                node = {
+                    "parentID": nodeID,
+                    "ID": childID,
+                    "moniker": undefined,
+                    "extendsID": childExtendsID,
+                    "implementsIDs": childImplementsIDs,
+                    "source": childSource,
+                    "type": childType,
+                    "name": childName,
+                    "prototypes": protos,
+                    "displayName": "",
+                    "connection": undefined,
+                    "localUrl": undefined, 
+                    "remoteUrl": undefined,
+                    //"color": "rgb(0,0,0)",
+                    "createProperty": true, 
+                    "sharing": { audio: true, video: true }                   
+                };
+
+                this.state.clients[ childID ] = node;
+                
+                // add the client specific locals
+                node.moniker = appMoniker.call( this, childName );
+                //console.info( "new client moniker: " + node.moniker );
+                node.displayName = undefined;
+                node.prototypes = protos;
+
+                if ( this.kernel.moniker() == node.moniker ) { 
+                    this.local.ID = childID;
+                    
+                    if ( this.videoElementsDiv ) {
+                        $('body').append(
+                            "<div id='"+self.videoElementsDiv+"'></div>"
+                        );                   
+                    }
+                } 
+            }
+
+        }, 
+
+        initializedNode: function( nodeID, childID, childExtendsID, childImplementsIDs, 
+            childSource, childType, childIndex, childName ) {
+
+            if ( this.debugVwf.initializing ) {
+                this.kernel.logger.infox( "initializedNode", nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childName );
+            } 
+
+            if ( childExtendsID === undefined )
+                return;
+
+            var client = this.state.clients[ childID ];
+
+            if ( client ) {
+                if ( this.local.ID == childID ){
+                    
+                    // local client object
+                    // grab access to the webcam 
+                    capture.call( this, this.local.sharing );
+                   
+                    var remoteClient = undefined;
+                    // existing clients
+                    for ( var clientID in this.state.clients ) {
+                        
+                        if ( clientID != this.local.ID ) {
+                            // create property for this client on each existing client
+                            remoteClient = this.state.clients[ clientID ];
+
+                            if ( remoteClient.createProperty ) {
+                                //console.info( "++ 1 ++    createProperty( "+clientID+", "+this.kernel.moniker()+" )" );  
+                                remoteClient.createProperty = false;
+                                this.kernel.createProperty( clientID, this.kernel.moniker() );                                
+                            }                          
+                        }
+                    }
+                } else {
+                    // not the local client, but if the local client has logged
+                    // in create the property for this on the new client
+                    if ( this.local.ID )   {
+                        if ( client.createProperty ) {
+                            client.createProperty = false;
+                            //console.info( "++ 2 ++    createProperty( "+childID+", "+this.kernel.moniker()+" )" );
+                            this.kernel.createProperty( childID, this.kernel.moniker() );
+                        }
+                    }
+                }
+            }            
+        },
+
+        deletedNode: function( nodeID ) {
+            
+            if ( this.debugVwf.deleting ) {
+                this.kernel.logger.infox( "deletedNode", nodeID );
+            }
+            // debugger;
+
+            //if ( this.kernel.find( nodeID, "parent::element(*,'http://vwf.example.com/clients.vwf')" ).length > 0 ) {
+                //if ( this.kernel.find( nodeID ).length > 0 ) {
+                var moniker = nodeID.slice(-20);//this.kernel.name( nodeID );
+                var client = undefined;
+
+                if ( moniker == this.kernel.moniker() ) {
+                    
+                    // this is the client that has left the converstaion
+                    // go through the peerConnections and close the 
+                    // all current connections
+                    var peer, peerMoniker;
+                    for ( var peerID in this.state.clients ) {
+                        peer = this.state.clients[ peerID ];
+                        peerMoniker = appMoniker.call( this, peer.name )
+                        if ( peerMoniker != this.kernel.moniker ) {
+                            peer.connection && peer.connection.disconnect();
+                        }
+                    }
+
+                } else {
+
+                    // this is a client who has has a peer leave the converstaion
+                    // remove that client, and the 
+                    client = findClientByMoniker.call( this, moniker );
+                    if ( client ) {
+                        client.connection && client.connection.disconnect();
+
+                        removeClient.call( this, client );
+                        delete this.state.clients[ client ]
+                    }
+
+                     
+                }
+            //}         
+
+        },
+  
+        createdProperty: function( nodeID, propertyName, propertyValue ) {
+
+            if ( this.debugVwf.properties ) {
+                this.kernel.logger.infox( "C === createdProperty ", nodeID, propertyName, propertyValue );
+            }
+
+            this.satProperty( nodeID, propertyName, propertyValue );
+        },        
+
+        initializedProperty: function( nodeID, propertyName, propertyValue ) {
+
+            if ( this.debugVwf.properties ) {
+                this.kernel.logger.infox( "  I === initializedProperty ", nodeID, propertyName, propertyValue );
+            }
+
+            this.satProperty( nodeID, propertyName, propertyValue );
+        },        
+
+        satProperty: function( nodeID, propertyName, propertyValue ) {
+            
+            
+            if ( this.debugVwf.properties || this.debugVwf.setting ) {
+                this.kernel.logger.infox( "    S === satProperty ", nodeID, propertyName, propertyValue );
+            } 
+
+            var client = this.state.clients[ nodeID ];
+
+            if ( client ) {
+                switch( propertyName ) {
+                    
+                    case "sharing":
+                        if ( propertyValue ) {
+                            client.sharing = propertyValue;
+                            if ( nodeID == this.local.ID ) {
+                                updateSharing.call( this, nodeID, propertyValue );
+                            }
+                        }
+                        break;
+
+                    case "localUrl":
+                        if ( propertyValue ) {
+                            if ( nodeID != this.local.ID ) {
+                                client.localUrl = propertyValue;
+                            }
+                        }
+                        break;
+
+                    case "remoteUrl":
+                        if ( propertyValue ) {
+                            client.remoteUrl = propertyValue;
+                         }
+                        break;
+
+                    case "displayName":
+                        if ( propertyValue ) {
+                            client.displayName = propertyValue;
+                        }
+                        break; 
+
+                    case "color":
+                        var clr = new utility.color( propertyValue );
+                        if ( clr ) {
+                            client.color = clr.toString();
+                        }
+                        break;    
+
+                    default:  
+                        // propertyName is the moniker of the client that 
+                        // this connection supports
+                        if ( nodeID == this.local.ID ) {
+                            if ( propertyValue ) {
+                                // propertyName - moniker of the client
+                                // propertyValue - peerConnection message
+                                handlePeerMessage.call( this, propertyName, propertyValue );
+                            }
+                        }
+                        break;
+                }
+            }
+        },
+
+        gotProperty: function( nodeID, propertyName, propertyValue ) {
+
+            if ( this.debugVwf.properties || this.debugVwf.getting ) {
+                this.kernel.logger.infox( "   G === gotProperty ", nodeID, propertyName, propertyValue );
+            }
+            var value = undefined;
+
+            return value;
+        },
+
+        calledMethod: function( nodeID, methodName, methodParameters, methodValue ) {
+            
+            if ( this.debugVwf.calling ) {
+                this.kernel.logger.infox( "  CM === calledMethod ", nodeID, methodName, methodParameters );
+            }
+
+            switch ( methodName ) {
+                case "setLocalMute":
+                    if ( this.kernel.moniker() == this.kernel.client() ) {
+                        methodValue = setMute.call( this, methodParameters );
+                    }
+                    break;
+            }
+        },       
+
+        firedEvent: function( nodeID, eventName, eventParameters ) {
+        },
+
+    } );
+ 
+    function createVideoElementAsAsset(id) {
+        
+          var video = document.querySelector('#' + id);
+        
+          if (!video) {
+            video = document.createElement('video');
+          }
+        
+          video.setAttribute('id', id);
+          video.setAttribute('autoplay', true);
+          video.setAttribute('src', '');
+          video.setAttribute("webkit-playsinline", true);
+          video.setAttribute("controls", true);
+          video.setAttribute("width", 640);
+          video.setAttribute("height", 480);
+        
+          var assets = document.querySelector('a-assets');
+        
+        //   if (!assets) {
+        //     assets = document.createElement('a-assets');
+        //     document.querySelector('a-scene').appendChild(assets);
+        //   }
+        
+          if (!assets.contains(video)) {
+            assets.appendChild(video);
+          }
+        
+          return video;
+        }
+
+
+    function getPrototypes( extendsID ) {
+        var prototypes = [];
+        var id = extendsID;
+
+        while ( id !== undefined ) {
+            prototypes.push( id );
+            id = this.kernel.prototype( id );
+        }
+                
+        return prototypes;
+    }
+
+    function getPeer( moniker ) {
+        var clientNode;
+        for ( var id in this.state.clients ) {
+            if ( this.state.clients[id].moniker == moniker ) {
+                clientNode = this.state.clients[id];
+                break;
+            }
+        }
+        return clientNode;
+    }
+
+    function displayLocal( stream, name, color ) {
+        var id = this.kernel.moniker();
+        return displayVideo.call( this, id, stream, this.local.url, name, id, true, color );
+    }
+
+    function displayVideo( id, stream, url, name, destMoniker, muted, color ) {
+        
+        var divId = undefined;
+
+        if ( this.videoProperties.create ) {
+            this.videosAdded++
+            var $container;
+            divId = name + this.videosAdded;
+            var videoId = "video-" + divId;
+
+            $container = $( "#" + this.videoElementsDiv );
+            if ( muted ) {
+                $container.append(
+                    "<div id='"+ divId + "'>" +
+                        "<video class='vwf-webrtc-video' id='" + videoId +
+                            "' width='320' height='240' " +
+                            "loop='loop' autoplay muted " +
+                            "style='position: absolute; left: 0; top: 0; z-index: 40;'>" +
+                        "</video>" +
+                    "</div>"
+                );
+
+            } else {
+                $container.append(
+                    "<div id='"+ divId + "'>" +
+                        "<video class='vwf-webrtc-video' id='" + videoId +
+                            "' width='320' height='240'" +
+                            " loop='loop' autoplay " +
+                            "style='position: absolute; left: 0; top: 0; z-index: 40;'>" +
+                        "</video>" +
+                    "</div>"
+                );
+            }
+            
+            var videoE = $( '#'+ videoId )[0];
+            if ( videoE && stream ) {
+                navigator.attachMediaStream( videoE, stream );
+                if ( muted ) {
+                    videoE.muted = true;  // firefox isn't mapping the muted property correctly
+                }
+            }  
+
+            var divEle = $('#'+divId);
+            divEle && divEle.draggable && divEle.draggable();
+           
+        } 
+
+        var clr = new utility.color( color );
+        if ( clr ) { 
+            clr = clr.toArray(); 
+        }
+
+        this.kernel.callMethod( this.kernel.application(), "createVideo", [ {
+            "ID": id,
+            "url": url, 
+            "name": name, 
+            "muted": muted, 
+            "color": clr ? clr : color
+        }, destMoniker ] );       
+        
+        
+        let video = createVideoElementAsAsset(name);
+        video.srcObject = stream;
+
+       this.kernel.callMethod( 'avatar-'+id, "setVideoTexture", [name]);
+
+        return divId;
+    }
+
+    function removeVideo( client ) {
+        
+        if ( client.videoDivID ) {
+            var $videoWin = $( "#" + client.videoDivID );
+            if ( $videoWin ) {
+                $videoWin.remove();
+            }
+            client.videoDivID = undefined;
+        }
+
+        this.kernel.callMethod( this.kernel.application(), "removeVideo", [ client.moniker ] );
+
+    }
+
+    function displayRemote( id, stream, url, name, destMoniker, color ) {
+        return displayVideo.call( this, id, stream, url, name, destMoniker, false, color );
+    }
+
+    function capture( media ) {
+
+        if ( this.local.stream === undefined && ( media.video || media.audio ) ) {
+            var self = this;
+            var constraints = { 
+                "audio": media.audio, 
+                "video": media.video ? { "mandatory": {}, "optional": [] } : false, 
+            };
+            
+            var successCallback = function( stream ) {
+                self.local.url = URL.createObjectURL( stream );
+                self.local.stream = stream;
+
+                self.kernel.setProperty( self.local.ID, "localUrl", self.local.url );
+
+                var localNode = self.state.clients[ self.local.ID ];
+                displayLocal.call( self, stream, localNode.displayName, localNode.color );
+                sendOffers.call( self );
+            };
+
+            var errorCallback = function( error ) { 
+                console.log("failed to get video stream error: " + error); 
+            };
+
+            try { 
+                navigator.getUserMedia( constraints, successCallback, errorCallback );
+            } catch (e) { 
+                console.log("getUserMedia: error " + e ); 
+            };
+        }
+    }  
+
+    function appMoniker( name ) {
+        return name.substr( 7, name.length-1 );
+    }
+    
+    function findClientByMoniker( moniker ) {
+        var client = undefined;
+        for ( var id in this.state.clients ) {
+            if ( client === undefined && moniker == this.state.clients[ id ].moniker ) {
+                client = this.state.clients[ id ];
+            }
+        }
+        return client;
+    }
+
+    function removeClient( client ) {
+        if ( client ) {
+            removeVideo.call( this, client );
+        }
+    }
+
+    function sendOffers() {
+        var peerNode;
+        for ( var id in this.state.clients ) {
+            if ( id != this.local.ID ) {
+                peerNode = this.state.clients[ id ];
+                
+                // if there's a url then connect                     
+                if ( peerNode.localUrl && peerNode.localUrl != "" && peerNode.connection === undefined ) {
+                    createPeerConnection.call( this, peerNode, true );   
+                }                
+            }
+        }
+        
+
+    }
+
+    function updateSharing( nodeID, sharing ) {
+        setMute.call( this, !sharing.audio );
+        setPause.call( this, !sharing.video );
+    }
+
+
+    function setMute( mute ) {
+        if ( this.local.stream && this.local.stream.audioTracks && this.local.stream.audioTracks.length > 0 ) {
+          if ( mute !== undefined ) {
+            this.local.stream.audioTracks[0].enabled = !mute;
+          }
+        }
+    };
+
+    function setPause( pause ) {
+        if ( this.local.stream && this.local.stream.videoTracks && this.local.stream.videoTracks.length > 0 ) {
+            if ( pause !== undefined ) {
+                this.local.stream.videoTracks[0].enabled = !pause;
+            }
+        }
+    }
+
+    function release() {
+      for ( id in this.connections ) {
+          this.connections[id].disconnect();
+      } 
+      this.connections = {};
+    }  
+
+    function hasStream() {
+        return ( this.stream !== undefined );
+    }
+
+    function createPeerConnection( peerNode, sendOffer ) {
+        if ( peerNode ) {
+            if ( peerNode.connection === undefined ) {
+                peerNode.connection = new mediaConnection( this, peerNode );
+                peerNode.connection.connect( this.local.stream, sendOffer );
+
+                //if ( this.bandwidth !== undefined ) {
+                //    debugger;
+                //}
+            }
+        }
+    }
+
+    function handlePeerMessage( propertyName, msg ) {
+        var peerNode = getPeer.call( this, propertyName )
+        if ( peerNode ) {
+            if ( peerNode.connection !== undefined ) {
+                peerNode.connection.processMessage( msg );
+            } else {
+                if ( msg.type === 'offer' ) {
+                    
+                    this.msgQueue.unshift( msg );
+
+                    peerNode.connection = new mediaConnection( this, peerNode );
+                    peerNode.connection.connect( this.local.stream, false );
+
+                    while ( this.msgQueue.length > 0 ) {
+                      peerNode.connection.processMessage( this.msgQueue.shift() );
+                    }
+                    this.msgQueue = [];
+                } else {
+                    this.msgQueue.push( msg );
+                }
+            }
+        }     
+    }
+
+    function deletePeerConnection( peerID ) {
+        var peerNode = this.state.clients[ peerID ];
+        if ( peerNode ) {
+            peerNode.connection.disconnect();
+            peerNode.connection = undefined;
+        }
+    } 
+
+    function getConnectionStats() {
+        var peerNode = undefined;
+        for ( var id in this.state.clients ) {
+            peerNode = this.state.clients[ id ];
+            if ( peerNode && peerNode.connection ) {
+                peerNode.connection.getStats();
+            }
+        } 
+    }
+
+    function isClientDefinition( prototypes ) {
+        var foundClient = false;
+        if ( prototypes ) {
+            var len = prototypes.length;
+            for ( var i = 0; i < len && !foundClient; i++ ) {
+                foundClient = ( prototypes[i] == "http://vwf.example.com/aframe/avatar.vwf" );
+            }
+        }
+
+        return foundClient;
+    }
+
+    function isClientInstanceDef( nodeID ) {
+        return ( nodeID == "http://vwf.example.com/clients.vwf" );
+    }
+
+    function mediaConnection( view, peerNode ) {
+        this.view = view;
+        this.peerNode = peerNode;        
+        
+        // 
+        this.stream = undefined;
+        this.url = undefined;
+        this.pc = undefined;
+        this.connected = false;
+        this.streamAdded = false;
+        this.state = "created";
+
+        // webrtc peerConnection parameters
+        this.pc_config = { "iceServers": this.view.iceServers };
+        this.pc_constraints = { "optional": [ { "DtlsSrtpKeyAgreement": true } ] };
+        // Set up audio and video regardless of what devices are present.
+        this.sdpConstraints = { 'mandatory': {
+                                'OfferToReceiveAudio':true, 
+                                'OfferToReceiveVideo':true }};
+
+        this.connect = function( stream, sendOffer ) {
+            var self = this;
+            if ( this.pc === undefined ) {
+                if ( this.view.debug ) console.log("Creating PeerConnection.");
+                
+                var iceCallback = function( event ) {
+                    //console.log( "------------------------ iceCallback ------------------------" );
+                    if ( event.candidate ) {
+                        var sMsg = { 
+                            "type": 'candidate',
+                            "label": event.candidate.sdpMLineIndex,
+                            "id": event.candidate.sdpMid,
+                            "candidate": event.candidate.candidate
+                        };
+
+                        // each client creates a property for each other
+                        // the message value is broadcast via the property
+                        self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sMsg );
+                    } else {
+                        if ( self.view.debug ) console.log("End of candidates.");
+                    }
+                }; 
+
+                // if ( webrtcDetectedBrowser == "firefox" ) {
+                //     this.pc_config = {"iceServers":[{"url":"stun:23.21.150.121"}]};
+                // }
+
+                try {
+                    this.pc = new RTCPeerConnection( this.pc_config, this.pc_constraints );
+                    this.pc.onicecandidate = iceCallback;
+
+                    if ( self.view.debug ) console.log("Created RTCPeerConnnection with config \"" + JSON.stringify( this.pc_config ) + "\".");
+                } catch (e) {
+                    console.log("Failed to create PeerConnection, exception: " + e.message);
+                    alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser.");
+                    return;
+                } 
+
+                this.pc.onnegotiationeeded = function( event ) {
+                    //debugger;
+                    //console.info( "onnegotiationeeded." );
+                }
+
+                this.pc.onaddstream = function( event ) {
+                    if ( self.view.debug ) console.log("Remote stream added.");
+                    
+                    self.stream = event.stream;
+                    self.url = URL.createObjectURL( event.stream );
+                    
+                    if ( self.view.debug ) console.log("Remote stream added.  url: " + self.url );
+
+                    var divID = displayRemote.call( self.view, self.peerNode.moniker, self.stream, self.url, self.peerNode.displayName, view.kernel.moniker(), self.peerNode.color );
+                    if ( divID !== undefined ) {
+                        self.peerNode.videoDivID = divID;
+                    }
+                };
+
+                this.pc.onremovestream = function( event ) {
+                    if ( self.view.debug ) console.log("Remote stream removed.");
+                };
+
+                this.pc.onsignalingstatechange = function() {
+                    //console.info( "onsignalingstatechange state change." );
+                }
+
+                this.pc.oniceconnectionstatechange = function( ) {
+                    if ( self && self.pc ) {
+                        var state = self.pc.signalingState || self.pc.readyState;
+                        //console.info( "peerConnection state change: " + state );
+                    } 
+                }
+
+                if ( stream ) {
+                    if ( this.view.debug ) console.log("Adding local stream.");
+
+                    this.pc.addStream( stream );
+                    this.streamAdded = true;
+                }
+
+                if ( sendOffer ){
+                    this.call();
+                }
+            }
+            this.connected = ( this.pc !== undefined );
+        };
+
+        this.setMute = function( mute ) {
+            if ( this.stream && this.stream.audioTracks && this.stream.audioTracks.length > 0 ) {
+                if ( mute !== undefined ) {
+                    this.stream.audioTracks[0].enabled = !mute;
+                }
+            }
+        }
+
+        this.setPause = function( pause ) {
+            if ( this.stream && this.stream.videoTracks && this.stream.videoTracks.length > 0 ) {
+                if ( pause !== undefined ) {
+                    this.stream.videoTracks[0].enabled = !pause;
+                }
+            }
+        }
+
+        this.disconnect = function() {
+            if ( this.view.debug ) console.log( "PC.disconnect  " + this.peerID );
+            
+            if ( this.pc ) {
+                this.pc.close();
+                this.pc = undefined;
+            }
+        };
+
+        this.processMessage = function( msg ) {
+            //var msg = JSON.parse(message); 
+            if ( this.view.debug ) console.log('S->C: ' +  JSON.stringify(msg) );
+            if ( this.pc ) {
+                if ( msg.type === 'offer') {
+                    if ( this.view.stereo ) {
+                        msg.sdp = addStereo( msg.sdp );
+                    }
+                    this.pc.setRemoteDescription( new RTCSessionDescription( msg ) );
+                    this.answer();
+                } else if ( msg.type === 'answer' && this.streamAdded ) {
+                    if ( this.view.stereo ) {
+                        msg.sdp = addStereo( msg.sdp );
+                    }                    
+                    this.pc.setRemoteDescription( new RTCSessionDescription( msg ) );
+                } else if ( msg.type === 'candidate' && this.streamAdded ) {
+                    var candidate = new RTCIceCandidate( { 
+                        "sdpMLineIndex": msg.label,
+                        "candidate": msg.candidate 
+                    } );
+                    this.pc.addIceCandidate( candidate );
+                } else if ( msg.type === 'bye' && this.streamAdded ) {
+                    this.hangup();
+                }
+            } 
+        };
+
+        this.answer = function() {
+            if ( this.view.debug ) console.log( "Send answer to peer" );
+            
+            var self = this;
+            var answerer = function( sessionDescription ) {
+                // Set Opus as the preferred codec in SDP if Opus is present.
+                sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
+                sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
+
+                self.pc.setLocalDescription( sessionDescription );
+
+                self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sessionDescription );
+            };
+
+            this.pc.createAnswer( answerer, null, this.sdpConstraints);
+        };
+
+        this.call = function() {
+            var self = this;
+            var constraints = {
+                "optional": [], 
+                "mandatory": {}
+            };
+
+            // temporary measure to remove Moz* constraints in Chrome
+            if ( navigator.webrtcDetectedBrowser === "chrome" ) {
+                for ( var prop in constraints.mandatory ) {
+                    if ( prop.indexOf("Moz") != -1 ) {
+                        delete constraints.mandatory[ prop ];
+                    }
+                }
+            }   
+            constraints = this.mergeConstraints( constraints, this.sdpConstraints );
+
+            if ( this.view.debug ) console.log("Sending offer to peer, with constraints: \n" +  "  \"" + JSON.stringify(constraints) + "\".")
+          
+            var offerer = function( sessionDescription ) {
+                // Set Opus as the preferred codec in SDP if Opus is present.
+                sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
+
+                sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
+                self.pc.setLocalDescription( sessionDescription );
+                
+                //sendSignalMessage.call( sessionDescription, self.peerID );
+                self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sessionDescription );
+            };
+
+            var onFailure = function(e) {
+                console.log(e)
+            }
+
+            this.pc.createOffer( offerer, onFailure, constraints );
+        };
+
+        this.setBandwidth = function( sdp ) {
+
+            // apparently this only works in chrome
+            if ( this.bandwidth === undefined || moz ) {
+                return sdp;
+            }
+
+            // remove existing bandwidth lines
+            sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
+            
+            if ( this.bandwidth.audio ) {
+                sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + this.bandwidth.audio + '\r\n');
+            }
+
+            if ( this.bandwidth.video ) {
+                sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + this.bandwidth.video + '\r\n');
+            }
+
+            if ( this.bandwidth.data /*&& !options.preferSCTP */ ) {
+                sdp = sdp.replace(/a=mid:data\r\n/g, 'a=mid:data\r\nb=AS:' + this.bandwidth.data + '\r\n');
+            }
+            return sdp;
+        }
+
+        this.getStats = function(){
+          if ( this.pc && this.pc.getStats ) {
+            console.info( "pc.iceConnectionState = " + this.pc.iceConnectionState );
+            console.info( " pc.iceGatheringState = " + this.pc.iceGatheringState );
+            console.info( "        pc.readyState = " + this.pc.readyState );
+            console.info( "    pc.signalingState = " + this.pc.signalingState );
+
+            var consoleStats = function( obj ) {
+                console.info( '   Timestamp:' + obj.timestamp );
+                if ( obj.id ) {
+                    console.info( '        id: ' + obj.id );
+                }
+                if ( obj.type ) {
+                    console.info( '        type: ' + obj.type );
+                }
+                if ( obj.names ) {
+                    var names = obj.names();
+                    for ( var i = 0; i < names.length; ++i ) {
+                        console.info( "         "+names[ i ]+": " + obj.stat( names[ i ] ) );
+                    }
+                } else {
+                    if ( obj.stat && obj.stat( 'audioOutputLevel' ) ) {
+                        console.info( "         audioOutputLevel: " + obj.stat( 'audioOutputLevel' ) );
+                    }
+                }
+            };
+
+            // local function
+            var readStats = function( stats ) {
+                var results = stats.result();
+                var bitrateText = 'No bitrate stats';
+
+                for ( var i = 0; i < results.length; ++i ) {
+                    var res = results[ i ];
+                    console.info( 'Report ' + i );
+                    if ( !res.local || res.local === res ) {
+                        
+                        consoleStats( res );
+                        // The bandwidth info for video is in a type ssrc stats record
+                        // with googFrameHeightReceived defined.
+                        // Should check for mediatype = video, but this is not
+                        // implemented yet.
+                        if ( res.type == 'ssrc' && res.stat( 'googFrameHeightReceived' ) ) {
+                            var bytesNow = res.stat( 'bytesReceived' );
+                            if ( timestampPrev > 0) {
+                                var bitRate = Math.round( ( bytesNow - bytesPrev ) * 8 / ( res.timestamp - timestampPrev ) );
+                                bitrateText = bitRate + ' kbits/sec';
+                            }
+                            timestampPrev = res.timestamp;
+                            bytesPrev = bytesNow;
+                        }
+                    } else {
+                        // Pre-227.0.1445 (188719) browser
+                        if ( res.local ) {
+                            console.info( "  Local: " );
+                            consoleStats( res.local );
+                        }
+                        if ( res.remote ) {
+                            console.info( "  Remote: " );
+                            consoleStats( res.remote );
+                        }
+                    }
+                }
+                console.info( "    bitrate: " + bitrateText )
+            } 
+
+            this.pc.getStats( readStats );        
+          }
+        }
+
+        this.hangup = function() {
+            if ( this.view.debug ) console.log( "PC.hangup  " + this.id );
+            
+            if ( this.pc ) {
+                this.pc.close();
+                this.pc = undefined;
+            }
+        };
+
+        this.mergeConstraints = function( cons1, cons2 ) {
+            var merged = cons1;
+            for (var name in cons2.mandatory) {
+                merged.mandatory[ name ] = cons2.mandatory[ name ];
+            }
+            merged.optional.concat( cons2.optional );
+            return merged;
+        }
+
+        // Set Opus as the default audio codec if it's present.
+        this.preferOpus = function( sdp ) {
+            var sdpLines = sdp.split( '\r\n' );
+
+            // Search for m line.
+            for ( var i = 0; i < sdpLines.length; i++ ) {
+                if ( sdpLines[i].search( 'm=audio' ) !== -1 ) {
+                    var mLineIndex = i;
+                    break;
+                } 
+            }
+
+            if ( mLineIndex === null ) {
+                return sdp;
+            }
+
+            // for ( var i = 0; i < sdpLines.length; i++ ) {
+            //     if ( i == 0 ) console.info( "=============================================" );
+                
+            //     console.info( "sdpLines["+i+"] = " + sdpLines[i] );
+            // }
+
+            // If Opus is available, set it as the default in m line.
+            for ( var i = 0; i < sdpLines.length; i++ ) {
+
+                if ( sdpLines[i].search( 'opus/48000' ) !== -1 ) {        
+                    var opusPayload = this.extractSdp( sdpLines[i], /:(\d+) opus\/48000/i );
+                    if ( opusPayload) {
+                        sdpLines[ mLineIndex ] = this.setDefaultCodec( sdpLines[ mLineIndex ], opusPayload );
+                    }
+                    break;
+                }
+            }
+
+            // Remove CN in m line and sdp.
+            sdpLines = this.removeCN( sdpLines, mLineIndex );
+
+            sdp = sdpLines.join('\r\n');
+            return sdp;
+        }
+
+        // Set Opus in stereo if stereo is enabled.
+        function addStereo( sdp ) {
+            var sdpLines = sdp.split('\r\n');
+
+            // Find opus payload.
+            for (var i = 0; i < sdpLines.length; i++) {
+              if (sdpLines[i].search('opus/48000') !== -1) {
+                var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
+                break;
+              }
+            }
+
+            // Find the payload in fmtp line.
+            for (var i = 0; i < sdpLines.length; i++) {
+              if (sdpLines[i].search('a=fmtp') !== -1) {
+                var payload = extractSdp(sdpLines[i], /a=fmtp:(\d+)/ );
+                if (payload === opusPayload) {
+                  var fmtpLineIndex = i;
+                  break;
+                }
+              }
+            }
+            // No fmtp line found.
+            if (fmtpLineIndex === null)
+              return sdp;
+
+            // Append stereo=1 to fmtp line.
+            sdpLines[fmtpLineIndex] = sdpLines[fmtpLineIndex].concat(' stereo=1');
+
+            sdp = sdpLines.join('\r\n');
+            return sdp;
+        }
+
+        // Strip CN from sdp before CN constraints is ready.
+        this.removeCN = function( sdpLines, mLineIndex ) {
+            var mLineElements = sdpLines[mLineIndex].split( ' ' );
+            // Scan from end for the convenience of removing an item.
+            for ( var i = sdpLines.length-1; i >= 0; i-- ) {
+                var payload = this.extractSdp( sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i );
+                if ( payload ) {
+                    var cnPos = mLineElements.indexOf( payload );
+                    if ( cnPos !== -1 ) {
+                        // Remove CN payload from m line.
+                        mLineElements.splice( cnPos, 1 );
+                    }
+                    // Remove CN line in sdp
+                    sdpLines.splice( i, 1 );
+                }
+            }
+
+            sdpLines[ mLineIndex ] = mLineElements.join( ' ' );
+            return sdpLines;
+        }
+
+        this.extractSdp = function( sdpLine, pattern ) {
+            var result = sdpLine.match( pattern );
+            return ( result && result.length == 2 ) ? result[ 1 ] : null;
+        }    
+
+        // Set the selected codec to the first in m line.
+        this.setDefaultCodec = function( mLine, payload ) {
+            var elements = mLine.split( ' ' );
+            var newLine = new Array();
+            var index = 0;
+            for ( var i = 0; i < elements.length; i++ ) {
+                if ( index === 3 ) // Format of media starts from the fourth.
+                    newLine[ index++ ] = payload; // Put target payload to the first.
+                if ( elements[ i ] !== payload )
+                    newLine[ index++ ] = elements[ i ];
+            }
+            return newLine.join( ' ' );
+        }
+
+    } 
+
+
+
+} );

+ 328 - 193
support/client/lib/vwf/view/webrtc.js

@@ -37,18 +37,6 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                 "sharing": { audio: true, video: true } 
             };
 
-            // turns on logger debugger console messages 
-            this.debugVwf = {
-                "creation": false,
-                "initializing": false,
-                "parenting": false,
-                "deleting": false,
-                "properties": false,
-                "setting": false,
-                "getting": false,
-                "calling": false
-            };
-
             if ( options === undefined ) { options = {}; }
 
             this.stereo = options.stereo !== undefined  ? options.stereo : false;
@@ -66,10 +54,6 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
         createdNode: function( nodeID, childID, childExtendsID, childImplementsIDs,
             childSource, childType, childIndex, childName, callback /* ( ready ) */ ) {
 
-            if ( this.debugVwf.creation ) {
-                this.kernel.logger.infox( "createdNode", nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childName );
-            }
-
             if ( childExtendsID === undefined )
                 return;
 
@@ -126,25 +110,85 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                 if ( this.kernel.moniker() == node.moniker ) { 
                     this.local.ID = childID;
                     
-                    if ( this.videoElementsDiv ) {
-                        $('body').append(
-                            "<div id='"+self.videoElementsDiv+"'></div>"
-                        );                   
-                    }
+                    
                 } 
             }
 
         }, 
 
-        initializedNode: function( nodeID, childID, childExtendsID, childImplementsIDs, 
-            childSource, childType, childIndex, childName ) {
+        deleteConnection: function(nodeID){
 
-            if ( this.debugVwf.initializing ) {
-                this.kernel.logger.infox( "initializedNode", nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childName );
-            } 
+             // debugger;
 
-            if ( childExtendsID === undefined )
-                return;
+            //if ( this.kernel.find( nodeID, "parent::element(*,'http://vwf.example.com/clients.vwf')" ).length > 0 ) {
+                //if ( this.kernel.find( nodeID ).length > 0 ) {
+                    var moniker = nodeID.slice(-20);//this.kernel.name( nodeID );
+                    var client = undefined;
+    
+                    if ( moniker == this.kernel.moniker() ) {
+                        
+                        // this is the client that has left the converstaion
+                        // go through the peerConnections and close the 
+                        // all current connections
+                        var peer, peerMoniker;
+                        for ( var peerID in this.state.clients ) {
+                            peer = this.state.clients[ peerID ];
+                            peerMoniker = appMoniker.call( this, peer.name )
+                            if ( peerMoniker != this.kernel.moniker() ) {
+                                peer.connection && peer.connection.disconnect();
+                                let peername = 'avatar-' + peerMoniker;
+                                deletePeerConnection.call( this, peername);
+                               
+                            }
+                        }
+    
+                    } else {
+    
+                        // this is a client who has has a peer leave the converstaion
+                        // remove that client, and the 
+                        client = findClientByMoniker.call( this, moniker );
+                        if ( client ) {
+                            client.connection && client.connection.disconnect();
+    
+                            //removeClient.call( this, client );
+                            //delete this.state.clients[ client ]
+                        }
+    
+                         
+                    }
+
+        },
+
+
+        stopWebRTC: function(nodeID){
+
+            if( this.local.stream ){
+
+                
+                var tracks =  this.local.stream.getTracks();
+                tracks.forEach(function(track) {
+                    track.stop();
+                  });
+                  this.local.stream = undefined;
+
+
+
+                let vidui = document.querySelector('#webrtcvideo');
+                const viduicomp = new mdc.iconToggle.MDCIconToggle(vidui); //new mdc.select.MDCIconToggle
+                if (vidui) viduicomp.on = false;
+
+                let micui = document.querySelector('#webrtcaudio');
+                const micuicomp = new mdc.iconToggle.MDCIconToggle(micui);
+                if (micui) micuicomp.on = false;
+
+                this.deleteConnection(nodeID);
+                this.kernel.callMethod(nodeID, "removeSoundWebRTC");
+                this.kernel.callMethod(nodeID, "removeVideoTexture");
+                
+            }
+
+        },
+        startWebRTC: function(childID) {
 
             var client = this.state.clients[ childID ];
 
@@ -182,13 +226,22 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                     }
                 }
             }            
+
+        },
+
+        initializedNode: function( nodeID, childID, childExtendsID, childImplementsIDs, 
+            childSource, childType, childIndex, childName ) {
+
+
+            if ( childExtendsID === undefined )
+                return;
+
+            
         },
 
         deletedNode: function( nodeID ) {
             
-            if ( this.debugVwf.deleting ) {
-                this.kernel.logger.infox( "deletedNode", nodeID );
-            }
+
             // debugger;
 
             //if ( this.kernel.find( nodeID, "parent::element(*,'http://vwf.example.com/clients.vwf')" ).length > 0 ) {
@@ -205,8 +258,10 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                     for ( var peerID in this.state.clients ) {
                         peer = this.state.clients[ peerID ];
                         peerMoniker = appMoniker.call( this, peer.name )
-                        if ( peerMoniker != this.kernel.moniker ) {
+                        if ( peerMoniker != this.kernel.moniker() ) {
                             peer.connection && peer.connection.disconnect();
+                            let peername = 'avatar-' + peerMoniker;
+                            deletePeerConnection.call( this, peername);
                         }
                     }
 
@@ -230,28 +285,19 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
   
         createdProperty: function( nodeID, propertyName, propertyValue ) {
 
-            if ( this.debugVwf.properties ) {
-                this.kernel.logger.infox( "C === createdProperty ", nodeID, propertyName, propertyValue );
-            }
+
 
             this.satProperty( nodeID, propertyName, propertyValue );
         },        
 
         initializedProperty: function( nodeID, propertyName, propertyValue ) {
 
-            if ( this.debugVwf.properties ) {
-                this.kernel.logger.infox( "  I === initializedProperty ", nodeID, propertyName, propertyValue );
-            }
 
             this.satProperty( nodeID, propertyName, propertyValue );
         },        
 
         satProperty: function( nodeID, propertyName, propertyValue ) {
             
-            
-            if ( this.debugVwf.properties || this.debugVwf.setting ) {
-                this.kernel.logger.infox( "    S === satProperty ", nodeID, propertyName, propertyValue );
-            } 
 
             var client = this.state.clients[ nodeID ];
 
@@ -287,12 +333,6 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                         }
                         break; 
 
-                    case "color":
-                        var clr = new utility.color( propertyValue );
-                        if ( clr ) {
-                            client.color = clr.toString();
-                        }
-                        break;    
 
                     default:  
                         // propertyName is the moniker of the client that 
@@ -311,19 +351,12 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
 
         gotProperty: function( nodeID, propertyName, propertyValue ) {
 
-            if ( this.debugVwf.properties || this.debugVwf.getting ) {
-                this.kernel.logger.infox( "   G === gotProperty ", nodeID, propertyName, propertyValue );
-            }
             var value = undefined;
 
             return value;
         },
 
         calledMethod: function( nodeID, methodName, methodParameters, methodValue ) {
-            
-            if ( this.debugVwf.calling ) {
-                this.kernel.logger.infox( "  CM === calledMethod ", nodeID, methodName, methodParameters );
-            }
 
             switch ( methodName ) {
                 case "setLocalMute":
@@ -331,15 +364,64 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                         methodValue = setMute.call( this, methodParameters );
                     }
                     break;
+                
+                case "webrtcTurnOnOff":
+                    if ( this.kernel.moniker() == this.kernel.client() ) {
+                        console.log("WEBRTC turn on/off")
+                        methodValue = turnOnOffTracks.call( this, methodParameters );
+                    }
+                    break;    
+
+                case "webrtcMuteAudio":
+                    if ( this.kernel.moniker() == this.kernel.client() ) {
+                        methodValue = muteAudio.call( this, methodParameters[0] );
+                    }
+                    break;  
+
+                case "webrtcMuteVideo":
+                    if ( this.kernel.moniker() == this.kernel.client() ) {
+                        methodValue = this.muteVideo.call( this, methodParameters[0] );
+                    }
+                    break; 
+
             }
         },       
 
         firedEvent: function( nodeID, eventName, eventParameters ) {
         },
 
+        muteVideo: function ( mute ) {
+            let str = this.local.stream;
+            if ( str ) {
+              
+                var tracks = str.getVideoTracks();
+    
+                tracks.forEach(function(track) {
+                    track.enabled = mute;
+                  });
+            }
+        },
+
+        muteAudio: function ( mute ) {
+            let str = this.local.stream;
+            if ( str ) {
+              
+                var tracks = str.getAudioTracks();
+    
+                tracks.forEach(function(track) {
+                    track.enabled = mute;
+                  });
+            }
+        }
+
+        
+
+       
+
+
     } );
  
-    function createVideoElementAsAsset(id) {
+    function createVideoElementAsAsset(id, local) {
         
           var video = document.querySelector('#' + id);
         
@@ -349,10 +431,21 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
         
           video.setAttribute('id', id);
           video.setAttribute('autoplay', true);
-          video.setAttribute('src', '');
+          //video.setAttribute('src', '');
           video.setAttribute("webkit-playsinline", true);
           video.setAttribute("controls", true);
+          video.setAttribute("width", 640);
+          video.setAttribute("height", 480);
+
+          if (local) video.setAttribute("muted", true);
         
+        //   let audioID = '#audio-' + id;
+        //   var audio = document.querySelector(audioID);
+        //   if (!audio) {
+        //     audio = document.createElement('audio');
+        //   }
+        //   audio.setAttribute('id', audioID);
+
           var assets = document.querySelector('a-assets');
         
         //   if (!assets) {
@@ -364,7 +457,11 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
             assets.appendChild(video);
           }
         
-          return video;
+        //   if (!assets.contains(audio)) {
+        //     assets.appendChild(audio);
+        //   }
+
+          return video //{'video': video, 'audio': audio};
         }
 
 
@@ -391,127 +488,102 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
         return clientNode;
     }
 
-    function displayLocal( stream, name, color ) {
+    function displayLocal( stream, name) {
         var id = this.kernel.moniker();
-        return displayVideo.call( this, id, stream, this.local.url, name, id, true, color );
+        return displayVideo.call( this, id, stream, this.local.url, name, id, true);
     }
 
-    function displayVideo( id, stream, url, name, destMoniker, muted, color ) {
+    function displayVideo( id, stream, url, name, destMoniker, local) {
         
-        var divId = undefined;
-
-        if ( this.videoProperties.create ) {
-            this.videosAdded++
-            var $container;
-            divId = name + this.videosAdded;
-            var videoId = "video-" + divId;
-
-            $container = $( "#" + this.videoElementsDiv );
-            if ( muted ) {
-                $container.append(
-                    "<div id='"+ divId + "'>" +
-                        "<video class='vwf-webrtc-video' id='" + videoId +
-                            "' width='320' height='240' " +
-                            "loop='loop' autoplay muted " +
-                            "style='position: absolute; left: 0; top: 0; z-index: 40;'>" +
-                        "</video>" +
-                    "</div>"
-                );
-
-            } else {
-                $container.append(
-                    "<div id='"+ divId + "'>" +
-                        "<video class='vwf-webrtc-video' id='" + videoId +
-                            "' width='320' height='240'" +
-                            " loop='loop' autoplay " +
-                            "style='position: absolute; left: 0; top: 0; z-index: 40;'>" +
-                        "</video>" +
-                    "</div>"
-                );
-            }
-            
-            var videoE = $( '#'+ videoId )[0];
-            if ( videoE && stream ) {
-                navigator.attachMediaStream( videoE, stream );
-                if ( muted ) {
-                    videoE.muted = true;  // firefox isn't mapping the muted property correctly
-                }
-            }  
+        let va = createVideoElementAsAsset(name, local);
+        //video.setAttribute('src', url);
+        va.srcObject = stream;
 
-            var divEle = $('#'+divId);
-            divEle && divEle.draggable && divEle.draggable();
-           
-        } 
+        //var audioCtx = new AudioContext();
+        //var source = audioCtx.createMediaStreamSource(stream);
+        //va.audio.src = stream;
 
-        var clr = new utility.color( color );
-        if ( clr ) { 
-            clr = clr.toArray(); 
-        }
-
-        this.kernel.callMethod( this.kernel.application(), "createVideo", [ {
-            "ID": id,
-            "url": url, 
-            "name": name, 
-            "muted": muted, 
-            "color": clr ? clr : color
-        }, destMoniker ] );       
-        
+        this.kernel.callMethod( 'avatar-'+id, "setVideoTexture", [name]);
         
-        let video = createVideoElementAsAsset(name);
-        video.srcObject = stream;
-
-       this.kernel.callMethod( 'avatar-'+id, "setVideoTexture", [name]);
-
-        return divId;
+        return id;
     }
 
     function removeVideo( client ) {
         
-        if ( client.videoDivID ) {
-            var $videoWin = $( "#" + client.videoDivID );
-            if ( $videoWin ) {
-                $videoWin.remove();
-            }
-            client.videoDivID = undefined;
-        }
+        // if ( client.videoDivID ) {
+        //     var $videoWin = $( "#" + client.videoDivID );
+        //     if ( $videoWin ) {
+        //         $videoWin.remove();
+        //     }
+        //     client.videoDivID = undefined;
+        // }
 
-        this.kernel.callMethod( this.kernel.application(), "removeVideo", [ client.moniker ] );
+        // this.kernel.callMethod( this.kernel.application(), "removeVideo", [ client.moniker ] );
 
     }
 
     function displayRemote( id, stream, url, name, destMoniker, color ) {
-        return displayVideo.call( this, id, stream, url, name, destMoniker, false, color );
+
+        let audioID = 'audio-' + name;
+        this.kernel.callMethod( 'avatar-'+id, "setSoundWebRTC", [audioID]);
+
+        return displayVideo.call( this, id, stream, url, name, destMoniker, true );
     }
 
     function capture( media ) {
 
         if ( this.local.stream === undefined && ( media.video || media.audio ) ) {
             var self = this;
-            var constraints = { 
-                "audio": media.audio, 
-                "video": media.video ? { "mandatory": {}, "optional": [] } : false, 
-            };
-            
-            var successCallback = function( stream ) {
-                self.local.url = URL.createObjectURL( stream );
-                self.local.stream = stream;
 
-                self.kernel.setProperty( self.local.ID, "localUrl", self.local.url );
+            var constraints = {
+                audio: true,
+                video: true
+              };
+
+            navigator.mediaDevices.getUserMedia(constraints).then(handleSuccess).catch(handleError);
 
+            function handleError(error) {
+                console.log('navigator.getUserMedia error: ', error);
+              }
+
+            function handleSuccess(stream) {
+                // var videoTracks = stream.getVideoTracks();
+                // console.log('Got stream with constraints:', constraints);
+                // if (videoTracks.length) {
+                //     videoTracks[0].enabled = true;
+                // }
+
+                self.local.url = "url" //URL.createObjectURL( stream );
+                self.local.stream = stream;
+                self.kernel.setProperty( self.local.ID, "localUrl", self.local.url );
                 var localNode = self.state.clients[ self.local.ID ];
-                displayLocal.call( self, stream, localNode.displayName, localNode.color );
-                sendOffers.call( self );
-            };
 
-            var errorCallback = function( error ) { 
-                console.log("failed to get video stream error: " + error); 
-            };
 
-            try { 
-                navigator.getUserMedia( constraints, successCallback, errorCallback );
-            } catch (e) { 
-                console.log("getUserMedia: error " + e ); 
-            };
+                self.muteAudio(false);
+                self.muteVideo(false);
+
+                let webRTCGUI = document.querySelector('#webrtcswitch');
+                if (webRTCGUI) webRTCGUI.setAttribute("aria-pressed", true);
+
+                let videoTracks = stream.getVideoTracks();
+                let vstatus =  videoTracks[0].enabled;
+
+                let vidui = document.querySelector('#webrtcvideo');
+                const viduicomp = new mdc.iconToggle.MDCIconToggle(vidui); //new mdc.select.MDCIconToggle
+                if (vidui) viduicomp.on = vstatus;
+
+                let audioTracks = stream.getAudioTracks();
+                let astatus =  audioTracks[0].enabled;
+
+                let micui = document.querySelector('#webrtcaudio');
+                const micuicomp = new mdc.iconToggle.MDCIconToggle(micui);
+                if (micui) micuicomp.on = astatus;
+
+
+               
+                displayLocal.call( self, stream, localNode.displayName);
+                sendOffers.call( self );
+              } 
         }
     }  
 
@@ -556,6 +628,35 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
         setPause.call( this, !sharing.video );
     }
 
+    function turnOnOffTracks( mute ) {
+        let str = this.local.stream;
+        if ( str ) {
+            var audioTracks = str.getAudioTracks();
+            var videoTracks = str.getVideoTracks();
+
+            audioTracks.forEach(function(track) {
+                track.enabled = mute[0];
+              });
+
+              videoTracks.forEach(function(track) {
+                track.enabled = mute[0];
+              });
+        }
+    };
+
+    function muteAudio( mute ) {
+        let str = this.local.stream;
+        if ( str ) {
+            var audioTracks = str.getAudioTracks();
+
+            audioTracks.forEach(function(track) {
+                track.enabled = mute;
+              });
+
+        }
+    };
+   
+    
 
     function setMute( mute ) {
         if ( this.local.stream && this.local.stream.audioTracks && this.local.stream.audioTracks.length > 0 ) {
@@ -668,12 +769,16 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
         this.state = "created";
 
         // webrtc peerConnection parameters
-        this.pc_config = { "iceServers": this.view.iceServers };
+        this.pc_config =  {'iceServers': [
+            {'url': 'stun:stun.l.google.com:19302'},
+            {'url': 'stun:stun1.l.google.com:19302'}
+        ]};//{ "iceServers": this.view.iceServers };
+
         this.pc_constraints = { "optional": [ { "DtlsSrtpKeyAgreement": true } ] };
         // Set up audio and video regardless of what devices are present.
-        this.sdpConstraints = { 'mandatory': {
-                                'OfferToReceiveAudio':true, 
-                                'OfferToReceiveVideo':true }};
+        this.sdpConstraints = {
+                                'offerToReceiveAudio':1, 
+                                'offerToReceiveVideo':1 };
 
         this.connect = function( stream, sendOffer ) {
             var self = this;
@@ -703,7 +808,7 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                 // }
 
                 try {
-                    this.pc = new RTCPeerConnection( this.pc_config, this.pc_constraints );
+                    this.pc = new RTCPeerConnection( this.pc_config, this.pc_constraints);
                     this.pc.onicecandidate = iceCallback;
 
                     if ( self.view.debug ) console.log("Created RTCPeerConnnection with config \"" + JSON.stringify( this.pc_config ) + "\".");
@@ -718,11 +823,12 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                     //console.info( "onnegotiationeeded." );
                 }
 
-                this.pc.onaddstream = function( event ) {
+                this.pc.ontrack = function( event ) {
                     if ( self.view.debug ) console.log("Remote stream added.");
                     
-                    self.stream = event.stream;
-                    self.url = URL.createObjectURL( event.stream );
+                    self.stream = event.streams[0];
+
+                    self.url = "url" //URL.createObjectURL( event.streams[0] );
                     
                     if ( self.view.debug ) console.log("Remote stream added.  url: " + self.url );
 
@@ -748,9 +854,20 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                 }
 
                 if ( stream ) {
-                    if ( this.view.debug ) console.log("Adding local stream.");
+                    
+                    // stream.getVideoTracks();
+                    // stream.getAudioTracks();
+
+                    stream.getTracks().forEach(
+                        function(track) {
+                            self.pc.addTrack(
+                            track,
+                            stream
+                          );
+                        }
+                      );
 
-                    this.pc.addStream( stream );
+                    //this.pc.addStream( stream );
                     this.streamAdded = true;
                 }
 
@@ -791,16 +908,16 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
             if ( this.view.debug ) console.log('S->C: ' +  JSON.stringify(msg) );
             if ( this.pc ) {
                 if ( msg.type === 'offer') {
-                    if ( this.view.stereo ) {
-                        msg.sdp = addStereo( msg.sdp );
-                    }
-                    this.pc.setRemoteDescription( new RTCSessionDescription( msg ) );
+                    // if ( this.view.stereo ) {
+                    //     msg.sdp = addStereo( msg.sdp );
+                    // }
+                    this.pc.setRemoteDescription( new RTCSessionDescription( msg ) ); //msg.sdp
                     this.answer();
                 } else if ( msg.type === 'answer' && this.streamAdded ) {
-                    if ( this.view.stereo ) {
-                        msg.sdp = addStereo( msg.sdp );
-                    }                    
-                    this.pc.setRemoteDescription( new RTCSessionDescription( msg ) );
+                    // if ( this.view.stereo ) {
+                    //     msg.sdp = addStereo( msg.sdp );
+                    // }                    
+                    this.pc.setRemoteDescription( new RTCSessionDescription( msg ) ); //msg.sdp
                 } else if ( msg.type === 'candidate' && this.streamAdded ) {
                     var candidate = new RTCIceCandidate( { 
                         "sdpMLineIndex": msg.label,
@@ -818,43 +935,55 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
             
             var self = this;
             var answerer = function( sessionDescription ) {
-                // Set Opus as the preferred codec in SDP if Opus is present.
-                sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
-                sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
+                // // Set Opus as the preferred codec in SDP if Opus is present.
+                // sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
+                // sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
 
                 self.pc.setLocalDescription( sessionDescription );
-
                 self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sessionDescription );
             };
 
-            this.pc.createAnswer( answerer, null, this.sdpConstraints);
+            function onCreateSessionDescriptionError(error) {
+                console.log('Failed to create session description: ' + error.toString());
+              }
+
+            this.pc.createAnswer(
+                self.sdpConstraints
+            ).then(
+                answerer,
+                onCreateSessionDescriptionError
+              );
+            //this.pc.createAnswer( answerer, null, this.sdpConstraints);
         };
 
         this.call = function() {
             var self = this;
             var constraints = {
-                "optional": [], 
-                "mandatory": {}
+                offerToReceiveAudio: 1,
+                offerToReceiveVideo: 1
             };
 
-            // temporary measure to remove Moz* constraints in Chrome
-            if ( navigator.webrtcDetectedBrowser === "chrome" ) {
-                for ( var prop in constraints.mandatory ) {
-                    if ( prop.indexOf("Moz") != -1 ) {
-                        delete constraints.mandatory[ prop ];
-                    }
-                }
-            }   
-            constraints = this.mergeConstraints( constraints, this.sdpConstraints );
-
-            if ( this.view.debug ) console.log("Sending offer to peer, with constraints: \n" +  "  \"" + JSON.stringify(constraints) + "\".")
           
             var offerer = function( sessionDescription ) {
-                // Set Opus as the preferred codec in SDP if Opus is present.
-                sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
 
-                sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
-                self.pc.setLocalDescription( sessionDescription );
+                self.pc.setLocalDescription(sessionDescription).then(
+                    function() {
+                      onSetLocalSuccess(self.pc);
+                    },
+                    onSetSessionDescriptionError
+                  );
+
+                  function onSetLocalSuccess(pc) {
+                    console.log(self.pc + ' setLocalDescription complete');
+                  }
+
+                  function onSetSessionDescriptionError(error) {
+                    console.log('Failed to set session description: ' + error.toString());
+                  }
+                // Set Opus as the preferred codec in SDP if Opus is present.
+                // sessionDescription.sdp = self.preferOpus( sessionDescription.sdp );
+                // sessionDescription.sdp = self.setBandwidth( sessionDescription.sdp );
+                // self.pc.setLocalDescription( sessionDescription );
                 
                 //sendSignalMessage.call( sessionDescription, self.peerID );
                 self.view.kernel.setProperty( self.peerNode.ID, self.view.kernel.moniker(), sessionDescription );
@@ -864,7 +993,13 @@ define( [ "module", "vwf/view", "vwf/utility", "vwf/utility/color", "jquery" ],
                 console.log(e)
             }
 
-            this.pc.createOffer( offerer, onFailure, constraints );
+            self.pc.createOffer(
+                constraints
+              ).then(
+                offerer,
+                onFailure
+              );
+            //this.pc.createOffer( offerer, onFailure, constraints );
         };
 
         this.setBandwidth = function( sdp ) {

+ 4471 - 0
support/client/lib/vwf/view/webrtc/dist/adapter-latest.js

@@ -0,0 +1,4471 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+
+function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : dtlsRole || 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+}
+
+// Edge does not like
+// 1) stun: filtered after 14393 unless ?transport=udp is present
+// 2) turn: that does not have all of turn:host:port?transport=udp
+// 3) turn: with ipv6 addresses
+// 4) turn: occurring muliple times
+function filterIceServers(iceServers, edgeVersion) {
+  var hasTurn = false;
+  iceServers = JSON.parse(JSON.stringify(iceServers));
+  return iceServers.filter(function(server) {
+    if (server && (server.urls || server.url)) {
+      var urls = server.urls || server.url;
+      if (server.url && !server.urls) {
+        console.warn('RTCIceServer.url is deprecated! Use urls instead.');
+      }
+      var isString = typeof urls === 'string';
+      if (isString) {
+        urls = [urls];
+      }
+      urls = urls.filter(function(url) {
+        var validTurn = url.indexOf('turn:') === 0 &&
+            url.indexOf('transport=udp') !== -1 &&
+            url.indexOf('turn:[') === -1 &&
+            !hasTurn;
+
+        if (validTurn) {
+          hasTurn = true;
+          return true;
+        }
+        return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
+            url.indexOf('?transport=udp') === -1;
+      });
+
+      delete server.url;
+      server.urls = isString ? urls[0] : urls;
+      return !!urls.length;
+    }
+    return false;
+  });
+}
+
+// Determines the intersection of local and remote capabilities.
+function getCommonCapabilities(localCapabilities, remoteCapabilities) {
+  var commonCapabilities = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: []
+  };
+
+  var findCodecByPayloadType = function(pt, codecs) {
+    pt = parseInt(pt, 10);
+    for (var i = 0; i < codecs.length; i++) {
+      if (codecs[i].payloadType === pt ||
+          codecs[i].preferredPayloadType === pt) {
+        return codecs[i];
+      }
+    }
+  };
+
+  var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
+    var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
+    var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
+    return lCodec && rCodec &&
+        lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
+  };
+
+  localCapabilities.codecs.forEach(function(lCodec) {
+    for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+      var rCodec = remoteCapabilities.codecs[i];
+      if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
+          lCodec.clockRate === rCodec.clockRate) {
+        if (lCodec.name.toLowerCase() === 'rtx' &&
+            lCodec.parameters && rCodec.parameters.apt) {
+          // for RTX we need to find the local rtx that has a apt
+          // which points to the same local codec as the remote one.
+          if (!rtxCapabilityMatches(lCodec, rCodec,
+              localCapabilities.codecs, remoteCapabilities.codecs)) {
+            continue;
+          }
+        }
+        rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
+        // number of channels is the highest common number of channels
+        rCodec.numChannels = Math.min(lCodec.numChannels,
+            rCodec.numChannels);
+        // push rCodec so we reply with offerer payload type
+        commonCapabilities.codecs.push(rCodec);
+
+        // determine common feedback mechanisms
+        rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
+          for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
+            if (lCodec.rtcpFeedback[j].type === fb.type &&
+                lCodec.rtcpFeedback[j].parameter === fb.parameter) {
+              return true;
+            }
+          }
+          return false;
+        });
+        // FIXME: also need to determine .parameters
+        //  see https://github.com/openpeer/ortc/issues/569
+        break;
+      }
+    }
+  });
+
+  localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+    for (var i = 0; i < remoteCapabilities.headerExtensions.length;
+         i++) {
+      var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+      if (lHeaderExtension.uri === rHeaderExtension.uri) {
+        commonCapabilities.headerExtensions.push(rHeaderExtension);
+        break;
+      }
+    }
+  });
+
+  // FIXME: fecMechanisms
+  return commonCapabilities;
+}
+
+// is action=setLocalDescription with type allowed in signalingState
+function isActionAllowedInSignalingState(action, type, signalingState) {
+  return {
+    offer: {
+      setLocalDescription: ['stable', 'have-local-offer'],
+      setRemoteDescription: ['stable', 'have-remote-offer']
+    },
+    answer: {
+      setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
+      setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
+    }
+  }[type][action].indexOf(signalingState) !== -1;
+}
+
+function maybeAddCandidate(iceTransport, candidate) {
+  // Edge's internal representation adds some fields therefore
+  // not all fieldѕ are taken into account.
+  var alreadyAdded = iceTransport.getRemoteCandidates()
+      .find(function(remoteCandidate) {
+        return candidate.foundation === remoteCandidate.foundation &&
+            candidate.ip === remoteCandidate.ip &&
+            candidate.port === remoteCandidate.port &&
+            candidate.priority === remoteCandidate.priority &&
+            candidate.protocol === remoteCandidate.protocol &&
+            candidate.type === remoteCandidate.type;
+      });
+  if (!alreadyAdded) {
+    iceTransport.addRemoteCandidate(candidate);
+  }
+  return !alreadyAdded;
+}
+
+module.exports = function(window, edgeVersion) {
+  var RTCPeerConnection = function(config) {
+    var self = this;
+
+    var _eventTarget = document.createDocumentFragment();
+    ['addEventListener', 'removeEventListener', 'dispatchEvent']
+        .forEach(function(method) {
+          self[method] = _eventTarget[method].bind(_eventTarget);
+        });
+
+    this.onicecandidate = null;
+    this.onaddstream = null;
+    this.ontrack = null;
+    this.onremovestream = null;
+    this.onsignalingstatechange = null;
+    this.oniceconnectionstatechange = null;
+    this.onicegatheringstatechange = null;
+    this.onnegotiationneeded = null;
+    this.ondatachannel = null;
+    this.canTrickleIceCandidates = null;
+
+    this.needNegotiation = false;
+
+    this.localStreams = [];
+    this.remoteStreams = [];
+
+    this.localDescription = null;
+    this.remoteDescription = null;
+
+    this.signalingState = 'stable';
+    this.iceConnectionState = 'new';
+    this.iceGatheringState = 'new';
+
+    config = JSON.parse(JSON.stringify(config || {}));
+
+    this.usingBundle = config.bundlePolicy === 'max-bundle';
+    if (config.rtcpMuxPolicy === 'negotiate') {
+      var e = new Error('rtcpMuxPolicy \'negotiate\' is not supported');
+      e.name = 'NotSupportedError';
+      throw(e);
+    } else if (!config.rtcpMuxPolicy) {
+      config.rtcpMuxPolicy = 'require';
+    }
+
+    switch (config.iceTransportPolicy) {
+      case 'all':
+      case 'relay':
+        break;
+      default:
+        config.iceTransportPolicy = 'all';
+        break;
+    }
+
+    switch (config.bundlePolicy) {
+      case 'balanced':
+      case 'max-compat':
+      case 'max-bundle':
+        break;
+      default:
+        config.bundlePolicy = 'balanced';
+        break;
+    }
+
+    config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);
+
+    this._iceGatherers = [];
+    if (config.iceCandidatePoolSize) {
+      for (var i = config.iceCandidatePoolSize; i > 0; i--) {
+        this._iceGatherers = new window.RTCIceGatherer({
+          iceServers: config.iceServers,
+          gatherPolicy: config.iceTransportPolicy
+        });
+      }
+    } else {
+      config.iceCandidatePoolSize = 0;
+    }
+
+    this._config = config;
+
+    // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
+    // everything that is needed to describe a SDP m-line.
+    this.transceivers = [];
+
+    this._sdpSessionId = SDPUtils.generateSessionId();
+    this._sdpSessionVersion = 0;
+
+    this._dtlsRole = undefined; // role for a=setup to use in answers.
+  };
+
+  RTCPeerConnection.prototype._emitGatheringStateChange = function() {
+    var event = new Event('icegatheringstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onicegatheringstatechange === 'function') {
+      this.onicegatheringstatechange(event);
+    }
+  };
+
+  RTCPeerConnection.prototype.getConfiguration = function() {
+    return this._config;
+  };
+
+  RTCPeerConnection.prototype.getLocalStreams = function() {
+    return this.localStreams;
+  };
+
+  RTCPeerConnection.prototype.getRemoteStreams = function() {
+    return this.remoteStreams;
+  };
+
+  // internal helper to create a transceiver object.
+  // (whih is not yet the same as the WebRTC 1.0 transceiver)
+  RTCPeerConnection.prototype._createTransceiver = function(kind) {
+    var hasBundleTransport = this.transceivers.length > 0;
+    var transceiver = {
+      track: null,
+      iceGatherer: null,
+      iceTransport: null,
+      dtlsTransport: null,
+      localCapabilities: null,
+      remoteCapabilities: null,
+      rtpSender: null,
+      rtpReceiver: null,
+      kind: kind,
+      mid: null,
+      sendEncodingParameters: null,
+      recvEncodingParameters: null,
+      stream: null,
+      wantReceive: true
+    };
+    if (this.usingBundle && hasBundleTransport) {
+      transceiver.iceTransport = this.transceivers[0].iceTransport;
+      transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
+    } else {
+      var transports = this._createIceAndDtlsTransports();
+      transceiver.iceTransport = transports.iceTransport;
+      transceiver.dtlsTransport = transports.dtlsTransport;
+    }
+    this.transceivers.push(transceiver);
+    return transceiver;
+  };
+
+  RTCPeerConnection.prototype.addTrack = function(track, stream) {
+    var transceiver;
+    for (var i = 0; i < this.transceivers.length; i++) {
+      if (!this.transceivers[i].track &&
+          this.transceivers[i].kind === track.kind) {
+        transceiver = this.transceivers[i];
+      }
+    }
+    if (!transceiver) {
+      transceiver = this._createTransceiver(track.kind);
+    }
+
+    this._maybeFireNegotiationNeeded();
+
+    if (this.localStreams.indexOf(stream) === -1) {
+      this.localStreams.push(stream);
+    }
+
+    transceiver.track = track;
+    transceiver.stream = stream;
+    transceiver.rtpSender = new window.RTCRtpSender(track,
+        transceiver.dtlsTransport);
+    return transceiver.rtpSender;
+  };
+
+  RTCPeerConnection.prototype.addStream = function(stream) {
+    var self = this;
+    if (edgeVersion >= 15025) {
+      stream.getTracks().forEach(function(track) {
+        self.addTrack(track, stream);
+      });
+    } else {
+      // Clone is necessary for local demos mostly, attaching directly
+      // to two different senders does not work (build 10547).
+      // Fixed in 15025 (or earlier)
+      var clonedStream = stream.clone();
+      stream.getTracks().forEach(function(track, idx) {
+        var clonedTrack = clonedStream.getTracks()[idx];
+        track.addEventListener('enabled', function(event) {
+          clonedTrack.enabled = event.enabled;
+        });
+      });
+      clonedStream.getTracks().forEach(function(track) {
+        self.addTrack(track, clonedStream);
+      });
+    }
+  };
+
+  RTCPeerConnection.prototype.removeStream = function(stream) {
+    var idx = this.localStreams.indexOf(stream);
+    if (idx > -1) {
+      this.localStreams.splice(idx, 1);
+      this._maybeFireNegotiationNeeded();
+    }
+  };
+
+  RTCPeerConnection.prototype.getSenders = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpSender;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpSender;
+    });
+  };
+
+  RTCPeerConnection.prototype.getReceivers = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpReceiver;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpReceiver;
+    });
+  };
+
+
+  RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex,
+      usingBundle) {
+    var self = this;
+    if (usingBundle && sdpMLineIndex > 0) {
+      return this.transceivers[0].iceGatherer;
+    } else if (this._iceGatherers.length) {
+      return this._iceGatherers.shift();
+    }
+    var iceGatherer = new window.RTCIceGatherer({
+      iceServers: this._config.iceServers,
+      gatherPolicy: this._config.iceTransportPolicy
+    });
+    Object.defineProperty(iceGatherer, 'state',
+        {value: 'new', writable: true}
+    );
+
+    this.transceivers[sdpMLineIndex].candidates = [];
+    this.transceivers[sdpMLineIndex].bufferCandidates = function(event) {
+      var end = !event.candidate || Object.keys(event.candidate).length === 0;
+      // polyfill since RTCIceGatherer.state is not implemented in
+      // Edge 10547 yet.
+      iceGatherer.state = end ? 'completed' : 'gathering';
+      if (self.transceivers[sdpMLineIndex].candidates !== null) {
+        self.transceivers[sdpMLineIndex].candidates.push(event.candidate);
+      }
+    };
+    iceGatherer.addEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    return iceGatherer;
+  };
+
+  // start gathering from an RTCIceGatherer.
+  RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) {
+    var self = this;
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer.onlocalcandidate) {
+      return;
+    }
+    var candidates = this.transceivers[sdpMLineIndex].candidates;
+    this.transceivers[sdpMLineIndex].candidates = null;
+    iceGatherer.removeEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    iceGatherer.onlocalcandidate = function(evt) {
+      if (self.usingBundle && sdpMLineIndex > 0) {
+        // if we know that we use bundle we can drop candidates with
+        // ѕdpMLineIndex > 0. If we don't do this then our state gets
+        // confused since we dispose the extra ice gatherer.
+        return;
+      }
+      var event = new Event('icecandidate');
+      event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
+
+      var cand = evt.candidate;
+      // Edge emits an empty object for RTCIceCandidateComplete‥
+      var end = !cand || Object.keys(cand).length === 0;
+      if (end) {
+        // polyfill since RTCIceGatherer.state is not implemented in
+        // Edge 10547 yet.
+        if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') {
+          iceGatherer.state = 'completed';
+        }
+      } else {
+        if (iceGatherer.state === 'new') {
+          iceGatherer.state = 'gathering';
+        }
+        // RTCIceCandidate doesn't have a component, needs to be added
+        cand.component = 1;
+        event.candidate.candidate = SDPUtils.writeCandidate(cand);
+      }
+
+      // update local description.
+      var sections = SDPUtils.splitSections(self.localDescription.sdp);
+      if (!end) {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=' + event.candidate.candidate + '\r\n';
+      } else {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=end-of-candidates\r\n';
+      }
+      self.localDescription.sdp = sections.join('');
+      var complete = self.transceivers.every(function(transceiver) {
+        return transceiver.iceGatherer &&
+            transceiver.iceGatherer.state === 'completed';
+      });
+
+      if (self.iceGatheringState !== 'gathering') {
+        self.iceGatheringState = 'gathering';
+        self._emitGatheringStateChange();
+      }
+
+      // Emit candidate. Also emit null candidate when all gatherers are
+      // complete.
+      if (!end) {
+        self.dispatchEvent(event);
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(event);
+        }
+      }
+      if (complete) {
+        self.dispatchEvent(new Event('icecandidate'));
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(new Event('icecandidate'));
+        }
+        self.iceGatheringState = 'complete';
+        self._emitGatheringStateChange();
+      }
+    };
+
+    // emit already gathered candidates.
+    window.setTimeout(function() {
+      candidates.forEach(function(candidate) {
+        var e = new Event('RTCIceGatherEvent');
+        e.candidate = candidate;
+        iceGatherer.onlocalcandidate(e);
+      });
+    }, 0);
+  };
+
+  // Create ICE transport and DTLS transport.
+  RTCPeerConnection.prototype._createIceAndDtlsTransports = function() {
+    var self = this;
+    var iceTransport = new window.RTCIceTransport(null);
+    iceTransport.onicestatechange = function() {
+      self._updateConnectionState();
+    };
+
+    var dtlsTransport = new window.RTCDtlsTransport(iceTransport);
+    dtlsTransport.ondtlsstatechange = function() {
+      self._updateConnectionState();
+    };
+    dtlsTransport.onerror = function() {
+      // onerror does not set state to failed by itself.
+      Object.defineProperty(dtlsTransport, 'state',
+          {value: 'failed', writable: true});
+      self._updateConnectionState();
+    };
+
+    return {
+      iceTransport: iceTransport,
+      dtlsTransport: dtlsTransport
+    };
+  };
+
+  // Destroy ICE gatherer, ICE transport and DTLS transport.
+  // Without triggering the callbacks.
+  RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function(
+      sdpMLineIndex) {
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer) {
+      delete iceGatherer.onlocalcandidate;
+      delete this.transceivers[sdpMLineIndex].iceGatherer;
+    }
+    var iceTransport = this.transceivers[sdpMLineIndex].iceTransport;
+    if (iceTransport) {
+      delete iceTransport.onicestatechange;
+      delete this.transceivers[sdpMLineIndex].iceTransport;
+    }
+    var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport;
+    if (dtlsTransport) {
+      delete dtlsTransport.ondtlsstatechange;
+      delete dtlsTransport.onerror;
+      delete this.transceivers[sdpMLineIndex].dtlsTransport;
+    }
+  };
+
+  // Start the RTP Sender and Receiver for a transceiver.
+  RTCPeerConnection.prototype._transceive = function(transceiver,
+      send, recv) {
+    var params = getCommonCapabilities(transceiver.localCapabilities,
+        transceiver.remoteCapabilities);
+    if (send && transceiver.rtpSender) {
+      params.encodings = transceiver.sendEncodingParameters;
+      params.rtcp = {
+        cname: SDPUtils.localCName,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.recvEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpSender.send(params);
+    }
+    if (recv && transceiver.rtpReceiver && params.codecs.length > 0) {
+      // remove RTX field in Edge 14942
+      if (transceiver.kind === 'video'
+          && transceiver.recvEncodingParameters
+          && edgeVersion < 15019) {
+        transceiver.recvEncodingParameters.forEach(function(p) {
+          delete p.rtx;
+        });
+      }
+      params.encodings = transceiver.recvEncodingParameters;
+      params.rtcp = {
+        cname: transceiver.rtcpParameters.cname,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.sendEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpReceiver.receive(params);
+    }
+  };
+
+  RTCPeerConnection.prototype.setLocalDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setLocalDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set local ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var sections;
+    var sessionpart;
+    if (description.type === 'offer') {
+      // VERY limited support for SDP munging. Limited to:
+      // * changing the order of codecs
+      sections = SDPUtils.splitSections(description.sdp);
+      sessionpart = sections.shift();
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var caps = SDPUtils.parseRtpParameters(mediaSection);
+        self.transceivers[sdpMLineIndex].localCapabilities = caps;
+      });
+
+      this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+        self._gather(transceiver.mid, sdpMLineIndex);
+      });
+    } else if (description.type === 'answer') {
+      sections = SDPUtils.splitSections(self.remoteDescription.sdp);
+      sessionpart = sections.shift();
+      var isIceLite = SDPUtils.matchPrefix(sessionpart,
+          'a=ice-lite').length > 0;
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var transceiver = self.transceivers[sdpMLineIndex];
+        var iceGatherer = transceiver.iceGatherer;
+        var iceTransport = transceiver.iceTransport;
+        var dtlsTransport = transceiver.dtlsTransport;
+        var localCapabilities = transceiver.localCapabilities;
+        var remoteCapabilities = transceiver.remoteCapabilities;
+
+        // treat bundle-only as not-rejected.
+        var rejected = SDPUtils.isRejected(mediaSection) &&
+            !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+
+        if (!rejected && !transceiver.isDatachannel) {
+          var remoteIceParameters = SDPUtils.getIceParameters(
+              mediaSection, sessionpart);
+          var remoteDtlsParameters = SDPUtils.getDtlsParameters(
+              mediaSection, sessionpart);
+          if (isIceLite) {
+            remoteDtlsParameters.role = 'server';
+          }
+
+          if (!self.usingBundle || sdpMLineIndex === 0) {
+            self._gather(transceiver.mid, sdpMLineIndex);
+            if (iceTransport.state === 'new') {
+              iceTransport.start(iceGatherer, remoteIceParameters,
+                  isIceLite ? 'controlling' : 'controlled');
+            }
+            if (dtlsTransport.state === 'new') {
+              dtlsTransport.start(remoteDtlsParameters);
+            }
+          }
+
+          // Calculate intersection of capabilities.
+          var params = getCommonCapabilities(localCapabilities,
+              remoteCapabilities);
+
+          // Start the RTCRtpSender. The RTCRtpReceiver for this
+          // transceiver has already been started in setRemoteDescription.
+          self._transceive(transceiver,
+              params.codecs.length > 0,
+              false);
+        }
+      });
+    }
+
+    this.localDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-local-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+
+    // If a success callback was provided, emit ICE candidates after it
+    // has been executed. Otherwise, emit callback after the Promise is
+    // resolved.
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    return new Promise(function(resolve) {
+      if (cb) {
+        cb.apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.setRemoteDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setRemoteDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set remote ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var streams = {};
+    this.remoteStreams.forEach(function(stream) {
+      streams[stream.id] = stream;
+    });
+    var receiverList = [];
+    var sections = SDPUtils.splitSections(description.sdp);
+    var sessionpart = sections.shift();
+    var isIceLite = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-lite').length > 0;
+    var usingBundle = SDPUtils.matchPrefix(sessionpart,
+        'a=group:BUNDLE ').length > 0;
+    this.usingBundle = usingBundle;
+    var iceOptions = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-options:')[0];
+    if (iceOptions) {
+      this.canTrickleIceCandidates = iceOptions.substr(14).split(' ')
+          .indexOf('trickle') >= 0;
+    } else {
+      this.canTrickleIceCandidates = false;
+    }
+
+    sections.forEach(function(mediaSection, sdpMLineIndex) {
+      var lines = SDPUtils.splitLines(mediaSection);
+      var kind = SDPUtils.getKind(mediaSection);
+      // treat bundle-only as not-rejected.
+      var rejected = SDPUtils.isRejected(mediaSection) &&
+          !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+      var protocol = lines[0].substr(2).split(' ')[2];
+
+      var direction = SDPUtils.getDirection(mediaSection, sessionpart);
+      var remoteMsid = SDPUtils.parseMsid(mediaSection);
+
+      var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier();
+
+      // Reject datachannels which are not implemented yet.
+      if (kind === 'application' && protocol === 'DTLS/SCTP') {
+        self.transceivers[sdpMLineIndex] = {
+          mid: mid,
+          isDatachannel: true
+        };
+        return;
+      }
+
+      var transceiver;
+      var iceGatherer;
+      var iceTransport;
+      var dtlsTransport;
+      var rtpReceiver;
+      var sendEncodingParameters;
+      var recvEncodingParameters;
+      var localCapabilities;
+
+      var track;
+      // FIXME: ensure the mediaSection has rtcp-mux set.
+      var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
+      var remoteIceParameters;
+      var remoteDtlsParameters;
+      if (!rejected) {
+        remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters.role = 'client';
+      }
+      recvEncodingParameters =
+          SDPUtils.parseRtpEncodingParameters(mediaSection);
+
+      var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection);
+
+      var isComplete = SDPUtils.matchPrefix(mediaSection,
+          'a=end-of-candidates', sessionpart).length > 0;
+      var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
+          .map(function(cand) {
+            return SDPUtils.parseCandidate(cand);
+          })
+          .filter(function(cand) {
+            return cand.component === 1;
+          });
+
+      // Check if we can use BUNDLE and dispose transports.
+      if ((description.type === 'offer' || description.type === 'answer') &&
+          !rejected && usingBundle && sdpMLineIndex > 0 &&
+          self.transceivers[sdpMLineIndex]) {
+        self._disposeIceAndDtlsTransports(sdpMLineIndex);
+        self.transceivers[sdpMLineIndex].iceGatherer =
+            self.transceivers[0].iceGatherer;
+        self.transceivers[sdpMLineIndex].iceTransport =
+            self.transceivers[0].iceTransport;
+        self.transceivers[sdpMLineIndex].dtlsTransport =
+            self.transceivers[0].dtlsTransport;
+        if (self.transceivers[sdpMLineIndex].rtpSender) {
+          self.transceivers[sdpMLineIndex].rtpSender.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+        if (self.transceivers[sdpMLineIndex].rtpReceiver) {
+          self.transceivers[sdpMLineIndex].rtpReceiver.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+      }
+      if (description.type === 'offer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex] ||
+            self._createTransceiver(kind);
+        transceiver.mid = mid;
+
+        if (!transceiver.iceGatherer) {
+          transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+              usingBundle);
+        }
+
+        if (cands.length && transceiver.iceTransport.state === 'new') {
+          if (isComplete && (!usingBundle || sdpMLineIndex === 0)) {
+            transceiver.iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        localCapabilities = window.RTCRtpReceiver.getCapabilities(kind);
+
+        // filter RTX until additional stuff needed for RTX is implemented
+        // in adapter.js
+        if (edgeVersion < 15019) {
+          localCapabilities.codecs = localCapabilities.codecs.filter(
+              function(codec) {
+                return codec.name !== 'rtx';
+              });
+        }
+
+        sendEncodingParameters = transceiver.sendEncodingParameters || [{
+          ssrc: (2 * sdpMLineIndex + 2) * 1001
+        }];
+
+        var isNewTrack = false;
+        if (direction === 'sendrecv' || direction === 'sendonly') {
+          isNewTrack = !transceiver.rtpReceiver;
+          rtpReceiver = transceiver.rtpReceiver ||
+              new window.RTCRtpReceiver(transceiver.dtlsTransport, kind);
+
+          if (isNewTrack) {
+            var stream;
+            track = rtpReceiver.track;
+            // FIXME: does not work with Plan B.
+            if (remoteMsid) {
+              if (!streams[remoteMsid.stream]) {
+                streams[remoteMsid.stream] = new window.MediaStream();
+                Object.defineProperty(streams[remoteMsid.stream], 'id', {
+                  get: function() {
+                    return remoteMsid.stream;
+                  }
+                });
+              }
+              Object.defineProperty(track, 'id', {
+                get: function() {
+                  return remoteMsid.track;
+                }
+              });
+              stream = streams[remoteMsid.stream];
+            } else {
+              if (!streams.default) {
+                streams.default = new window.MediaStream();
+              }
+              stream = streams.default;
+            }
+            stream.addTrack(track);
+            receiverList.push([track, rtpReceiver, stream]);
+          }
+        }
+
+        transceiver.localCapabilities = localCapabilities;
+        transceiver.remoteCapabilities = remoteCapabilities;
+        transceiver.rtpReceiver = rtpReceiver;
+        transceiver.rtcpParameters = rtcpParameters;
+        transceiver.sendEncodingParameters = sendEncodingParameters;
+        transceiver.recvEncodingParameters = recvEncodingParameters;
+
+        // Start the RTCRtpReceiver now. The RTPSender is started in
+        // setLocalDescription.
+        self._transceive(self.transceivers[sdpMLineIndex],
+            false,
+            isNewTrack);
+      } else if (description.type === 'answer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex];
+        iceGatherer = transceiver.iceGatherer;
+        iceTransport = transceiver.iceTransport;
+        dtlsTransport = transceiver.dtlsTransport;
+        rtpReceiver = transceiver.rtpReceiver;
+        sendEncodingParameters = transceiver.sendEncodingParameters;
+        localCapabilities = transceiver.localCapabilities;
+
+        self.transceivers[sdpMLineIndex].recvEncodingParameters =
+            recvEncodingParameters;
+        self.transceivers[sdpMLineIndex].remoteCapabilities =
+            remoteCapabilities;
+        self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters;
+
+        if (cands.length && iceTransport.state === 'new') {
+          if ((isIceLite || isComplete) &&
+              (!usingBundle || sdpMLineIndex === 0)) {
+            iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        if (!usingBundle || sdpMLineIndex === 0) {
+          if (iceTransport.state === 'new') {
+            iceTransport.start(iceGatherer, remoteIceParameters,
+                'controlling');
+          }
+          if (dtlsTransport.state === 'new') {
+            dtlsTransport.start(remoteDtlsParameters);
+          }
+        }
+
+        self._transceive(transceiver,
+            direction === 'sendrecv' || direction === 'recvonly',
+            direction === 'sendrecv' || direction === 'sendonly');
+
+        if (rtpReceiver &&
+            (direction === 'sendrecv' || direction === 'sendonly')) {
+          track = rtpReceiver.track;
+          if (remoteMsid) {
+            if (!streams[remoteMsid.stream]) {
+              streams[remoteMsid.stream] = new window.MediaStream();
+            }
+            streams[remoteMsid.stream].addTrack(track);
+            receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]);
+          } else {
+            if (!streams.default) {
+              streams.default = new window.MediaStream();
+            }
+            streams.default.addTrack(track);
+            receiverList.push([track, rtpReceiver, streams.default]);
+          }
+        } else {
+          // FIXME: actually the receiver should be created later.
+          delete transceiver.rtpReceiver;
+        }
+      }
+    });
+
+    if (this._dtlsRole === undefined) {
+      this._dtlsRole = description.type === 'offer' ? 'active' : 'passive';
+    }
+
+    this.remoteDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-remote-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+    Object.keys(streams).forEach(function(sid) {
+      var stream = streams[sid];
+      if (stream.getTracks().length) {
+        if (self.remoteStreams.indexOf(stream) === -1) {
+          self.remoteStreams.push(stream);
+          var event = new Event('addstream');
+          event.stream = stream;
+          window.setTimeout(function() {
+            self.dispatchEvent(event);
+            if (typeof self.onaddstream === 'function') {
+              self.onaddstream(event);
+            }
+          });
+        }
+
+        receiverList.forEach(function(item) {
+          var track = item[0];
+          var receiver = item[1];
+          if (stream.id !== item[2].id) {
+            return;
+          }
+          var trackEvent = new Event('track');
+          trackEvent.track = track;
+          trackEvent.receiver = receiver;
+          trackEvent.transceiver = {receiver: receiver};
+          trackEvent.streams = [stream];
+          window.setTimeout(function() {
+            self.dispatchEvent(trackEvent);
+            if (typeof self.ontrack === 'function') {
+              self.ontrack(trackEvent);
+            }
+          });
+        });
+      }
+    });
+
+    // check whether addIceCandidate({}) was called within four seconds after
+    // setRemoteDescription.
+    window.setTimeout(function() {
+      if (!(self && self.transceivers)) {
+        return;
+      }
+      self.transceivers.forEach(function(transceiver) {
+        if (transceiver.iceTransport &&
+            transceiver.iceTransport.state === 'new' &&
+            transceiver.iceTransport.getRemoteCandidates().length > 0) {
+          console.warn('Timeout for addRemoteCandidate. Consider sending ' +
+              'an end-of-candidates notification');
+          transceiver.iceTransport.addRemoteCandidate({});
+        }
+      });
+    }, 4000);
+
+    return new Promise(function(resolve) {
+      if (args.length > 1 && typeof args[1] === 'function') {
+        args[1].apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.close = function() {
+    this.transceivers.forEach(function(transceiver) {
+      /* not yet
+      if (transceiver.iceGatherer) {
+        transceiver.iceGatherer.close();
+      }
+      */
+      if (transceiver.iceTransport) {
+        transceiver.iceTransport.stop();
+      }
+      if (transceiver.dtlsTransport) {
+        transceiver.dtlsTransport.stop();
+      }
+      if (transceiver.rtpSender) {
+        transceiver.rtpSender.stop();
+      }
+      if (transceiver.rtpReceiver) {
+        transceiver.rtpReceiver.stop();
+      }
+    });
+    // FIXME: clean up tracks, local streams, remote streams, etc
+    this._updateSignalingState('closed');
+  };
+
+  // Update the signaling state.
+  RTCPeerConnection.prototype._updateSignalingState = function(newState) {
+    this.signalingState = newState;
+    var event = new Event('signalingstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onsignalingstatechange === 'function') {
+      this.onsignalingstatechange(event);
+    }
+  };
+
+  // Determine whether to fire the negotiationneeded event.
+  RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() {
+    var self = this;
+    if (this.signalingState !== 'stable' || this.needNegotiation === true) {
+      return;
+    }
+    this.needNegotiation = true;
+    window.setTimeout(function() {
+      if (self.needNegotiation === false) {
+        return;
+      }
+      self.needNegotiation = false;
+      var event = new Event('negotiationneeded');
+      self.dispatchEvent(event);
+      if (typeof self.onnegotiationneeded === 'function') {
+        self.onnegotiationneeded(event);
+      }
+    }, 0);
+  };
+
+  // Update the connection state.
+  RTCPeerConnection.prototype._updateConnectionState = function() {
+    var newState;
+    var states = {
+      'new': 0,
+      closed: 0,
+      connecting: 0,
+      checking: 0,
+      connected: 0,
+      completed: 0,
+      disconnected: 0,
+      failed: 0
+    };
+    this.transceivers.forEach(function(transceiver) {
+      states[transceiver.iceTransport.state]++;
+      states[transceiver.dtlsTransport.state]++;
+    });
+    // ICETransport.completed and connected are the same for this purpose.
+    states.connected += states.completed;
+
+    newState = 'new';
+    if (states.failed > 0) {
+      newState = 'failed';
+    } else if (states.connecting > 0 || states.checking > 0) {
+      newState = 'connecting';
+    } else if (states.disconnected > 0) {
+      newState = 'disconnected';
+    } else if (states.new > 0) {
+      newState = 'new';
+    } else if (states.connected > 0 || states.completed > 0) {
+      newState = 'connected';
+    }
+
+    if (newState !== this.iceConnectionState) {
+      this.iceConnectionState = newState;
+      var event = new Event('iceconnectionstatechange');
+      this.dispatchEvent(event);
+      if (typeof this.oniceconnectionstatechange === 'function') {
+        this.oniceconnectionstatechange(event);
+      }
+    }
+  };
+
+  RTCPeerConnection.prototype.createOffer = function() {
+    var self = this;
+    var args = arguments;
+
+    var offerOptions;
+    if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+      offerOptions = arguments[0];
+    } else if (arguments.length === 3) {
+      offerOptions = arguments[2];
+    }
+
+    var numAudioTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'audio';
+    }).length;
+    var numVideoTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'video';
+    }).length;
+
+    // Determine number of audio and video tracks we need to send/recv.
+    if (offerOptions) {
+      // Reject Chrome legacy constraints.
+      if (offerOptions.mandatory || offerOptions.optional) {
+        throw new TypeError(
+            'Legacy mandatory/optional constraints not supported.');
+      }
+      if (offerOptions.offerToReceiveAudio !== undefined) {
+        if (offerOptions.offerToReceiveAudio === true) {
+          numAudioTracks = 1;
+        } else if (offerOptions.offerToReceiveAudio === false) {
+          numAudioTracks = 0;
+        } else {
+          numAudioTracks = offerOptions.offerToReceiveAudio;
+        }
+      }
+      if (offerOptions.offerToReceiveVideo !== undefined) {
+        if (offerOptions.offerToReceiveVideo === true) {
+          numVideoTracks = 1;
+        } else if (offerOptions.offerToReceiveVideo === false) {
+          numVideoTracks = 0;
+        } else {
+          numVideoTracks = offerOptions.offerToReceiveVideo;
+        }
+      }
+    }
+
+    this.transceivers.forEach(function(transceiver) {
+      if (transceiver.kind === 'audio') {
+        numAudioTracks--;
+        if (numAudioTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      } else if (transceiver.kind === 'video') {
+        numVideoTracks--;
+        if (numVideoTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      }
+    });
+
+    // Create M-lines for recvonly streams.
+    while (numAudioTracks > 0 || numVideoTracks > 0) {
+      if (numAudioTracks > 0) {
+        this._createTransceiver('audio');
+        numAudioTracks--;
+      }
+      if (numVideoTracks > 0) {
+        this._createTransceiver('video');
+        numVideoTracks--;
+      }
+    }
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      // For each track, create an ice gatherer, ice transport,
+      // dtls transport, potentially rtpsender and rtpreceiver.
+      var track = transceiver.track;
+      var kind = transceiver.kind;
+      var mid = SDPUtils.generateIdentifier();
+      transceiver.mid = mid;
+
+      if (!transceiver.iceGatherer) {
+        transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+            self.usingBundle);
+      }
+
+      var localCapabilities = window.RTCRtpSender.getCapabilities(kind);
+      // filter RTX until additional stuff needed for RTX is implemented
+      // in adapter.js
+      if (edgeVersion < 15019) {
+        localCapabilities.codecs = localCapabilities.codecs.filter(
+            function(codec) {
+              return codec.name !== 'rtx';
+            });
+      }
+      localCapabilities.codecs.forEach(function(codec) {
+        // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
+        // by adding level-asymmetry-allowed=1
+        if (codec.name === 'H264' &&
+            codec.parameters['level-asymmetry-allowed'] === undefined) {
+          codec.parameters['level-asymmetry-allowed'] = '1';
+        }
+      });
+
+      // generate an ssrc now, to be used later in rtpSender.send
+      var sendEncodingParameters = transceiver.sendEncodingParameters || [{
+        ssrc: (2 * sdpMLineIndex + 1) * 1001
+      }];
+      if (track) {
+        // add RTX
+        if (edgeVersion >= 15019 && kind === 'video' &&
+            !sendEncodingParameters[0].rtx) {
+          sendEncodingParameters[0].rtx = {
+            ssrc: sendEncodingParameters[0].ssrc + 1
+          };
+        }
+      }
+
+      if (transceiver.wantReceive) {
+        transceiver.rtpReceiver = new window.RTCRtpReceiver(
+            transceiver.dtlsTransport, kind);
+      }
+
+      transceiver.localCapabilities = localCapabilities;
+      transceiver.sendEncodingParameters = sendEncodingParameters;
+    });
+
+    // always offer BUNDLE and dispose on return if not supported.
+    if (this._config.bundlePolicy !== 'max-compat') {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    sdp += 'a=ice-options:trickle\r\n';
+
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      sdp += writeMediaSection(transceiver, transceiver.localCapabilities,
+          'offer', transceiver.stream, self._dtlsRole);
+      sdp += 'a=rtcp-rsize\r\n';
+
+      if (transceiver.iceGatherer && self.iceGatheringState !== 'new' &&
+          (sdpMLineIndex === 0 || !self.usingBundle)) {
+        transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) {
+          cand.component = 1;
+          sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n';
+        });
+
+        if (transceiver.iceGatherer.state === 'completed') {
+          sdp += 'a=end-of-candidates\r\n';
+        }
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'offer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.createAnswer = function() {
+    var self = this;
+    var args = arguments;
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    if (this.usingBundle) {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    var mediaSectionsInOffer = SDPUtils.splitSections(
+        this.remoteDescription.sdp).length - 1;
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      if (sdpMLineIndex + 1 > mediaSectionsInOffer) {
+        return;
+      }
+      if (transceiver.isDatachannel) {
+        sdp += 'm=application 0 DTLS/SCTP 5000\r\n' +
+            'c=IN IP4 0.0.0.0\r\n' +
+            'a=mid:' + transceiver.mid + '\r\n';
+        return;
+      }
+
+      // FIXME: look at direction.
+      if (transceiver.stream) {
+        var localTrack;
+        if (transceiver.kind === 'audio') {
+          localTrack = transceiver.stream.getAudioTracks()[0];
+        } else if (transceiver.kind === 'video') {
+          localTrack = transceiver.stream.getVideoTracks()[0];
+        }
+        if (localTrack) {
+          // add RTX
+          if (edgeVersion >= 15019 && transceiver.kind === 'video' &&
+              !transceiver.sendEncodingParameters[0].rtx) {
+            transceiver.sendEncodingParameters[0].rtx = {
+              ssrc: transceiver.sendEncodingParameters[0].ssrc + 1
+            };
+          }
+        }
+      }
+
+      // Calculate intersection of capabilities.
+      var commonCapabilities = getCommonCapabilities(
+          transceiver.localCapabilities,
+          transceiver.remoteCapabilities);
+
+      var hasRtx = commonCapabilities.codecs.filter(function(c) {
+        return c.name.toLowerCase() === 'rtx';
+      }).length;
+      if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
+        delete transceiver.sendEncodingParameters[0].rtx;
+      }
+
+      sdp += writeMediaSection(transceiver, commonCapabilities,
+          'answer', transceiver.stream, self._dtlsRole);
+      if (transceiver.rtcpParameters &&
+          transceiver.rtcpParameters.reducedSize) {
+        sdp += 'a=rtcp-rsize\r\n';
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'answer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
+    var err;
+    var sections;
+    if (!candidate || candidate.candidate === '') {
+      for (var j = 0; j < this.transceivers.length; j++) {
+        if (this.transceivers[j].isDatachannel) {
+          continue;
+        }
+        this.transceivers[j].iceTransport.addRemoteCandidate({});
+        sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+        sections[j + 1] += 'a=end-of-candidates\r\n';
+        this.remoteDescription.sdp = sections.join('');
+        if (this.usingBundle) {
+          break;
+        }
+      }
+    } else if (!(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) {
+      throw new TypeError('sdpMLineIndex or sdpMid required');
+    } else if (!this.remoteDescription) {
+      err = new Error('Can not add ICE candidate without ' +
+          'a remote description');
+      err.name = 'InvalidStateError';
+    } else {
+      var sdpMLineIndex = candidate.sdpMLineIndex;
+      if (candidate.sdpMid) {
+        for (var i = 0; i < this.transceivers.length; i++) {
+          if (this.transceivers[i].mid === candidate.sdpMid) {
+            sdpMLineIndex = i;
+            break;
+          }
+        }
+      }
+      var transceiver = this.transceivers[sdpMLineIndex];
+      if (transceiver) {
+        if (transceiver.isDatachannel) {
+          return Promise.resolve();
+        }
+        var cand = Object.keys(candidate.candidate).length > 0 ?
+            SDPUtils.parseCandidate(candidate.candidate) : {};
+        // Ignore Chrome's invalid candidates since Edge does not like them.
+        if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
+          return Promise.resolve();
+        }
+        // Ignore RTCP candidates, we assume RTCP-MUX.
+        if (cand.component && cand.component !== 1) {
+          return Promise.resolve();
+        }
+        // when using bundle, avoid adding candidates to the wrong
+        // ice transport. And avoid adding candidates added in the SDP.
+        if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 &&
+            transceiver.iceTransport !== this.transceivers[0].iceTransport)) {
+          if (!maybeAddCandidate(transceiver.iceTransport, cand)) {
+            err = new Error('Can not add ICE candidate');
+            err.name = 'OperationError';
+          }
+        }
+
+        if (!err) {
+          // update the remoteDescription.
+          var candidateString = candidate.candidate.trim();
+          if (candidateString.indexOf('a=') === 0) {
+            candidateString = candidateString.substr(2);
+          }
+          sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+          sections[sdpMLineIndex + 1] += 'a=' +
+              (cand.type ? candidateString : 'end-of-candidates')
+              + '\r\n';
+          this.remoteDescription.sdp = sections.join('');
+        }
+      } else {
+        err = new Error('Can not add ICE candidate');
+        err.name = 'OperationError';
+      }
+    }
+    var args = arguments;
+    return new Promise(function(resolve, reject) {
+      if (err) {
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [err]);
+        }
+        reject(err);
+      } else {
+        if (args.length > 1 && typeof args[1] === 'function') {
+          args[1].apply(null);
+        }
+        resolve();
+      }
+    });
+  };
+
+  RTCPeerConnection.prototype.getStats = function() {
+    var promises = [];
+    this.transceivers.forEach(function(transceiver) {
+      ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
+          'dtlsTransport'].forEach(function(method) {
+            if (transceiver[method]) {
+              promises.push(transceiver[method].getStats());
+            }
+          });
+    });
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    var fixStatsType = function(stat) {
+      return {
+        inboundrtp: 'inbound-rtp',
+        outboundrtp: 'outbound-rtp',
+        candidatepair: 'candidate-pair',
+        localcandidate: 'local-candidate',
+        remotecandidate: 'remote-candidate'
+      }[stat.type] || stat.type;
+    };
+    return new Promise(function(resolve) {
+      // shim getStats with maplike support
+      var results = new Map();
+      Promise.all(promises).then(function(res) {
+        res.forEach(function(result) {
+          Object.keys(result).forEach(function(id) {
+            result[id].type = fixStatsType(result[id]);
+            results.set(id, result[id]);
+          });
+        });
+        if (cb) {
+          cb.apply(null, results);
+        }
+        resolve(results);
+      });
+    });
+  };
+  return RTCPeerConnection;
+};
+
+},{"sdp":2}],2:[function(require,module,exports){
+ /* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+  return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+  return blob.trim().split('\n').map(function(line) {
+    return line.trim();
+  });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+  var parts = blob.split('\nm=');
+  return parts.map(function(part, index) {
+    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+  });
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+  return SDPUtils.splitLines(blob).filter(function(line) {
+    return line.indexOf(prefix) === 0;
+  });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+  var parts;
+  // Parse both variants.
+  if (line.indexOf('a=candidate:') === 0) {
+    parts = line.substring(12).split(' ');
+  } else {
+    parts = line.substring(10).split(' ');
+  }
+
+  var candidate = {
+    foundation: parts[0],
+    component: parseInt(parts[1], 10),
+    protocol: parts[2].toLowerCase(),
+    priority: parseInt(parts[3], 10),
+    ip: parts[4],
+    port: parseInt(parts[5], 10),
+    // skip parts[6] == 'typ'
+    type: parts[7]
+  };
+
+  for (var i = 8; i < parts.length; i += 2) {
+    switch (parts[i]) {
+      case 'raddr':
+        candidate.relatedAddress = parts[i + 1];
+        break;
+      case 'rport':
+        candidate.relatedPort = parseInt(parts[i + 1], 10);
+        break;
+      case 'tcptype':
+        candidate.tcpType = parts[i + 1];
+        break;
+      case 'ufrag':
+        candidate.ufrag = parts[i + 1]; // for backward compability.
+        candidate.usernameFragment = parts[i + 1];
+        break;
+      default: // extension handling, in particular ufrag
+        candidate[parts[i]] = parts[i + 1];
+        break;
+    }
+  }
+  return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+  var sdp = [];
+  sdp.push(candidate.foundation);
+  sdp.push(candidate.component);
+  sdp.push(candidate.protocol.toUpperCase());
+  sdp.push(candidate.priority);
+  sdp.push(candidate.ip);
+  sdp.push(candidate.port);
+
+  var type = candidate.type;
+  sdp.push('typ');
+  sdp.push(type);
+  if (type !== 'host' && candidate.relatedAddress &&
+      candidate.relatedPort) {
+    sdp.push('raddr');
+    sdp.push(candidate.relatedAddress); // was: relAddr
+    sdp.push('rport');
+    sdp.push(candidate.relatedPort); // was: relPort
+  }
+  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+    sdp.push('tcptype');
+    sdp.push(candidate.tcpType);
+  }
+  if (candidate.ufrag) {
+    sdp.push('ufrag');
+    sdp.push(candidate.ufrag);
+  }
+  return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+  return line.substr(14).split(' ');
+}
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+  var parts = line.substr(9).split(' ');
+  var parsed = {
+    payloadType: parseInt(parts.shift(), 10) // was: id
+  };
+
+  parts = parts[0].split('/');
+
+  parsed.name = parts[0];
+  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+  // was: channels
+  parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+  return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+      (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+  var parts = line.substr(9).split(' ');
+  return {
+    id: parseInt(parts[0], 10),
+    direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+    uri: parts[1]
+  };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+      (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+          ? '/' + headerExtension.direction
+          : '') +
+      ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+  var parsed = {};
+  var kv;
+  var parts = line.substr(line.indexOf(' ') + 1).split(';');
+  for (var j = 0; j < parts.length; j++) {
+    kv = parts[j].trim().split('=');
+    parsed[kv[0].trim()] = kv[1];
+  }
+  return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+  var line = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.parameters && Object.keys(codec.parameters).length) {
+    var params = [];
+    Object.keys(codec.parameters).forEach(function(param) {
+      params.push(param + '=' + codec.parameters[param]);
+    });
+    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+  }
+  return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+  return {
+    type: parts.shift(),
+    parameter: parts.join(' ')
+  };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+  var lines = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+    // FIXME: special handling for trr-int?
+    codec.rtcpFeedback.forEach(function(fb) {
+      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+      (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+          '\r\n';
+    });
+  }
+  return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+  var sp = line.indexOf(' ');
+  var parts = {
+    ssrc: parseInt(line.substr(7, sp - 7), 10)
+  };
+  var colon = line.indexOf(':', sp);
+  if (colon > -1) {
+    parts.attribute = line.substr(sp + 1, colon - sp - 1);
+    parts.value = line.substr(colon + 1);
+  } else {
+    parts.attribute = line.substr(sp + 1);
+  }
+  return parts;
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+  var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+  if (mid) {
+    return mid.substr(6);
+  }
+}
+
+SDPUtils.parseFingerprint = function(line) {
+  var parts = line.substr(14).split(' ');
+  return {
+    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+    value: parts[1]
+  };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+      'a=fingerprint:');
+  // Note: a=setup line is ignored since we use the 'auto' role.
+  // Note2: 'algorithm' is not case sensitive except in Edge.
+  return {
+    role: 'auto',
+    fingerprints: lines.map(SDPUtils.parseFingerprint)
+  };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+  var sdp = 'a=setup:' + setupType + '\r\n';
+  params.fingerprints.forEach(function(fp) {
+    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+  });
+  return sdp;
+};
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var iceParameters = {
+    usernameFragment: lines.filter(function(line) {
+      return line.indexOf('a=ice-ufrag:') === 0;
+    })[0].substr(12),
+    password: lines.filter(function(line) {
+      return line.indexOf('a=ice-pwd:') === 0;
+    })[0].substr(10)
+  };
+  return iceParameters;
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+      'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+  var description = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: [],
+    rtcp: []
+  };
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+    var pt = mline[i];
+    var rtpmapline = SDPUtils.matchPrefix(
+        mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+    if (rtpmapline) {
+      var codec = SDPUtils.parseRtpMap(rtpmapline);
+      var fmtps = SDPUtils.matchPrefix(
+          mediaSection, 'a=fmtp:' + pt + ' ');
+      // Only the first a=fmtp:<pt> is considered.
+      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+      codec.rtcpFeedback = SDPUtils.matchPrefix(
+          mediaSection, 'a=rtcp-fb:' + pt + ' ')
+        .map(SDPUtils.parseRtcpFb);
+      description.codecs.push(codec);
+      // parse FEC mechanisms from rtpmap lines.
+      switch (codec.name.toUpperCase()) {
+        case 'RED':
+        case 'ULPFEC':
+          description.fecMechanisms.push(codec.name.toUpperCase());
+          break;
+        default: // only RED and ULPFEC are recognized as FEC mechanisms.
+          break;
+      }
+    }
+  }
+  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+    description.headerExtensions.push(SDPUtils.parseExtmap(line));
+  });
+  // FIXME: parse rtcp.
+  return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+  var sdp = '';
+
+  // Build the mline.
+  sdp += 'm=' + kind + ' ';
+  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+  sdp += ' UDP/TLS/RTP/SAVPF ';
+  sdp += caps.codecs.map(function(codec) {
+    if (codec.preferredPayloadType !== undefined) {
+      return codec.preferredPayloadType;
+    }
+    return codec.payloadType;
+  }).join(' ') + '\r\n';
+
+  sdp += 'c=IN IP4 0.0.0.0\r\n';
+  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+  caps.codecs.forEach(function(codec) {
+    sdp += SDPUtils.writeRtpMap(codec);
+    sdp += SDPUtils.writeFmtp(codec);
+    sdp += SDPUtils.writeRtcpFb(codec);
+  });
+  var maxptime = 0;
+  caps.codecs.forEach(function(codec) {
+    if (codec.maxptime > maxptime) {
+      maxptime = codec.maxptime;
+    }
+  });
+  if (maxptime > 0) {
+    sdp += 'a=maxptime:' + maxptime + '\r\n';
+  }
+  sdp += 'a=rtcp-mux\r\n';
+
+  caps.headerExtensions.forEach(function(extension) {
+    sdp += SDPUtils.writeExtmap(extension);
+  });
+  // FIXME: write fecMechanisms.
+  return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+  var encodingParameters = [];
+  var description = SDPUtils.parseRtpParameters(mediaSection);
+  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+  // filter a=ssrc:... cname:, ignore PlanB-msid
+  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'cname';
+  });
+  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+  var secondarySsrc;
+
+  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+  .map(function(line) {
+    var parts = line.split(' ');
+    parts.shift();
+    return parts.map(function(part) {
+      return parseInt(part, 10);
+    });
+  });
+  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+    secondarySsrc = flows[0][1];
+  }
+
+  description.codecs.forEach(function(codec) {
+    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+      var encParam = {
+        ssrc: primarySsrc,
+        codecPayloadType: parseInt(codec.parameters.apt, 10),
+        rtx: {
+          ssrc: secondarySsrc
+        }
+      };
+      encodingParameters.push(encParam);
+      if (hasRed) {
+        encParam = JSON.parse(JSON.stringify(encParam));
+        encParam.fec = {
+          ssrc: secondarySsrc,
+          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+        };
+        encodingParameters.push(encParam);
+      }
+    }
+  });
+  if (encodingParameters.length === 0 && primarySsrc) {
+    encodingParameters.push({
+      ssrc: primarySsrc
+    });
+  }
+
+  // we support both b=AS and b=TIAS but interpret AS as TIAS.
+  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+  if (bandwidth.length) {
+    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(7), 10);
+    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+      // use formula from JSEP to convert b=AS to TIAS value.
+      bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+          - (50 * 40 * 8);
+    } else {
+      bandwidth = undefined;
+    }
+    encodingParameters.forEach(function(params) {
+      params.maxBitrate = bandwidth;
+    });
+  }
+  return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+  var rtcpParameters = {};
+
+  var cname;
+  // Gets the first SSRC. Note that with RTX there might be multiple
+  // SSRCs.
+  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+      .map(function(line) {
+        return SDPUtils.parseSsrcMedia(line);
+      })
+      .filter(function(obj) {
+        return obj.attribute === 'cname';
+      })[0];
+  if (remoteSsrc) {
+    rtcpParameters.cname = remoteSsrc.value;
+    rtcpParameters.ssrc = remoteSsrc.ssrc;
+  }
+
+  // Edge uses the compound attribute instead of reducedSize
+  // compound is !reducedSize
+  var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+  rtcpParameters.reducedSize = rsize.length > 0;
+  rtcpParameters.compound = rsize.length === 0;
+
+  // parses the rtcp-mux attrіbute.
+  // Note that Edge does not support unmuxed RTCP.
+  var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+  rtcpParameters.mux = mux.length > 0;
+
+  return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+  var parts;
+  var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+  if (spec.length === 1) {
+    parts = spec[0].substr(7).split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+  var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'msid';
+  });
+  if (planB.length > 0) {
+    parts = planB[0].value.split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+  return Math.random().toString().substr(2, 21);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) {
+  var sessionId;
+  var version = sessVer !== undefined ? sessVer : 2;
+  if (sessId) {
+    sessionId = sessId;
+  } else {
+    sessionId = SDPUtils.generateSessionId();
+  }
+  // FIXME: sess-id should be an NTP timestamp.
+  return 'v=0\r\n' +
+      'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' +
+      's=-\r\n' +
+      't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  var lines = SDPUtils.splitLines(mediaSection);
+  for (var i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substr(2);
+      default:
+        // FIXME: What should happen here?
+    }
+  }
+  if (sessionpart) {
+    return SDPUtils.getDirection(sessionpart);
+  }
+  return 'sendrecv';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+  return mediaSection.split(' ', 2)[1] === '0';
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return {
+    kind: mline[0].substr(2),
+    port: parseInt(mline[1], 10),
+    protocol: mline[2],
+    fmt: mline.slice(3).join(' ')
+  };
+};
+
+// Expose public methods.
+if (typeof module === 'object') {
+  module.exports = SDPUtils;
+}
+
+},{}],3:[function(require,module,exports){
+(function (global){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var adapterFactory = require('./adapter_factory.js');
+module.exports = adapterFactory({window: global.window});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./adapter_factory.js":4}],4:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var utils = require('./utils');
+// Shimming starts here.
+module.exports = function(dependencies, opts) {
+  var window = dependencies && dependencies.window;
+
+  var options = {
+    shimChrome: true,
+    shimFirefox: true,
+    shimEdge: true,
+    shimSafari: true,
+  };
+
+  for (var key in opts) {
+    if (hasOwnProperty.call(opts, key)) {
+      options[key] = opts[key];
+    }
+  }
+
+  // Utils.
+  var logging = utils.log;
+  var browserDetails = utils.detectBrowser(window);
+
+  // Export to the adapter global object visible in the browser.
+  var adapter = {
+    browserDetails: browserDetails,
+    extractVersion: utils.extractVersion,
+    disableLog: utils.disableLog,
+    disableWarnings: utils.disableWarnings
+  };
+
+  // Uncomment the line below if you want logging to occur, including logging
+  // for the switch statement below. Can also be turned on in the browser via
+  // adapter.disableLog(false), but then logging from the switch statement below
+  // will not appear.
+  // require('./utils').disableLog(false);
+
+  // Browser shims.
+  var chromeShim = require('./chrome/chrome_shim') || null;
+  var edgeShim = require('./edge/edge_shim') || null;
+  var firefoxShim = require('./firefox/firefox_shim') || null;
+  var safariShim = require('./safari/safari_shim') || null;
+  var commonShim = require('./common_shim') || null;
+
+  // Shim browser if found.
+  switch (browserDetails.browser) {
+    case 'chrome':
+      if (!chromeShim || !chromeShim.shimPeerConnection ||
+          !options.shimChrome) {
+        logging('Chrome shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming chrome.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = chromeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      chromeShim.shimGetUserMedia(window);
+      chromeShim.shimMediaStream(window);
+      chromeShim.shimSourceObject(window);
+      chromeShim.shimPeerConnection(window);
+      chromeShim.shimOnTrack(window);
+      chromeShim.shimAddTrackRemoveTrack(window);
+      chromeShim.shimGetSendersWithDtmf(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'firefox':
+      if (!firefoxShim || !firefoxShim.shimPeerConnection ||
+          !options.shimFirefox) {
+        logging('Firefox shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming firefox.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = firefoxShim;
+      commonShim.shimCreateObjectURL(window);
+
+      firefoxShim.shimGetUserMedia(window);
+      firefoxShim.shimSourceObject(window);
+      firefoxShim.shimPeerConnection(window);
+      firefoxShim.shimOnTrack(window);
+      firefoxShim.shimRemoveStream(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'edge':
+      if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
+        logging('MS edge shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming edge.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = edgeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      edgeShim.shimGetUserMedia(window);
+      edgeShim.shimPeerConnection(window);
+      edgeShim.shimReplaceTrack(window);
+
+      // the edge shim implements the full RTCIceCandidate object.
+      break;
+    case 'safari':
+      if (!safariShim || !options.shimSafari) {
+        logging('Safari shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming safari.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = safariShim;
+      commonShim.shimCreateObjectURL(window);
+
+      safariShim.shimRTCIceServerUrls(window);
+      safariShim.shimCallbacksAPI(window);
+      safariShim.shimLocalStreamsAPI(window);
+      safariShim.shimRemoteStreamsAPI(window);
+      safariShim.shimTrackEventTransceiver(window);
+      safariShim.shimGetUserMedia(window);
+      safariShim.shimCreateOfferLegacy(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    default:
+      logging('Unsupported browser!');
+      break;
+  }
+
+  return adapter;
+};
+
+},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":10,"./safari/safari_shim":12,"./utils":13}],5:[function(require,module,exports){
+
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+var chromeShim = {
+  shimMediaStream: function(window) {
+    window.MediaStream = window.MediaStream || window.webkitMediaStream;
+  },
+
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+          }
+          this.addEventListener('track', this._ontrack = f);
+        }
+      });
+      var origSetRemoteDescription =
+          window.RTCPeerConnection.prototype.setRemoteDescription;
+      window.RTCPeerConnection.prototype.setRemoteDescription = function() {
+        var pc = this;
+        if (!pc._ontrackpoly) {
+          pc._ontrackpoly = function(e) {
+            // onaddstream does not fire when a track is added to an existing
+            // stream. But stream.onaddtrack is implemented so we use that.
+            e.stream.addEventListener('addtrack', function(te) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === te.track.id;
+                });
+              } else {
+                receiver = {track: te.track};
+              }
+
+              var event = new Event('track');
+              event.track = te.track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+            e.stream.getTracks().forEach(function(track) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === track.id;
+                });
+              } else {
+                receiver = {track: track};
+              }
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+          };
+          pc.addEventListener('addstream', pc._ontrackpoly);
+        }
+        return origSetRemoteDescription.apply(pc, arguments);
+      };
+    }
+  },
+
+  shimGetSendersWithDtmf: function(window) {
+    // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        !('getSenders' in window.RTCPeerConnection.prototype) &&
+        'createDTMFSender' in window.RTCPeerConnection.prototype) {
+      var shimSenderWithDtmf = function(pc, track) {
+        return {
+          track: track,
+          get dtmf() {
+            if (this._dtmf === undefined) {
+              if (track.kind === 'audio') {
+                this._dtmf = pc.createDTMFSender(track);
+              } else {
+                this._dtmf = null;
+              }
+            }
+            return this._dtmf;
+          },
+          _pc: pc
+        };
+      };
+
+      // augment addTrack when getSenders is not available.
+      if (!window.RTCPeerConnection.prototype.getSenders) {
+        window.RTCPeerConnection.prototype.getSenders = function() {
+          this._senders = this._senders || [];
+          return this._senders.slice(); // return a copy of the internal state.
+        };
+        var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
+        window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+          var pc = this;
+          var sender = origAddTrack.apply(pc, arguments);
+          if (!sender) {
+            sender = shimSenderWithDtmf(pc, track);
+            pc._senders.push(sender);
+          }
+          return sender;
+        };
+
+        var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
+        window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+          var pc = this;
+          origRemoveTrack.apply(pc, arguments);
+          var idx = pc._senders.indexOf(sender);
+          if (idx !== -1) {
+            pc._senders.splice(idx, 1);
+          }
+        };
+      }
+      var origAddStream = window.RTCPeerConnection.prototype.addStream;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origAddStream.apply(pc, [stream]);
+        stream.getTracks().forEach(function(track) {
+          pc._senders.push(shimSenderWithDtmf(pc, track));
+        });
+      };
+
+      var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origRemoveStream.apply(pc, [stream]);
+
+        stream.getTracks().forEach(function(track) {
+          var sender = pc._senders.find(function(s) {
+            return s.track === track;
+          });
+          if (sender) {
+            pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender
+          }
+        });
+      };
+    } else if (typeof window === 'object' && window.RTCPeerConnection &&
+               'getSenders' in window.RTCPeerConnection.prototype &&
+               'createDTMFSender' in window.RTCPeerConnection.prototype &&
+               window.RTCRtpSender &&
+               !('dtmf' in window.RTCRtpSender.prototype)) {
+      var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
+      window.RTCPeerConnection.prototype.getSenders = function() {
+        var pc = this;
+        var senders = origGetSenders.apply(pc, []);
+        senders.forEach(function(sender) {
+          sender._pc = pc;
+        });
+        return senders;
+      };
+
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = this._pc.createDTMFSender(this.track);
+            } else {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    var URL = window && window.URL;
+
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this._srcObject;
+          },
+          set: function(stream) {
+            var self = this;
+            // Use _srcObject as a private property for this shim
+            this._srcObject = stream;
+            if (this.src) {
+              URL.revokeObjectURL(this.src);
+            }
+
+            if (!stream) {
+              this.src = '';
+              return undefined;
+            }
+            this.src = URL.createObjectURL(stream);
+            // We need to recreate the blob url when a track is added or
+            // removed. Doing it manually since we want to avoid a recursion.
+            stream.addEventListener('addtrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+            stream.addEventListener('removetrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+          }
+        });
+      }
+    }
+  },
+
+  shimAddTrackRemoveTrack: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+    // shim addTrack and removeTrack.
+    if (window.RTCPeerConnection.prototype.addTrack &&
+        browserDetails.version >= 64) {
+      return;
+    }
+
+    // also shim pc.getLocalStreams when addTrack is shimmed
+    // to return the original streams.
+    var origGetLocalStreams = window.RTCPeerConnection.prototype
+        .getLocalStreams;
+    window.RTCPeerConnection.prototype.getLocalStreams = function() {
+      var self = this;
+      var nativeStreams = origGetLocalStreams.apply(this);
+      self._reverseStreams = self._reverseStreams || {};
+      return nativeStreams.map(function(stream) {
+        return self._reverseStreams[stream.id];
+      });
+    };
+
+    var origAddStream = window.RTCPeerConnection.prototype.addStream;
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      stream.getTracks().forEach(function(track) {
+        var alreadyExists = pc.getSenders().find(function(s) {
+          return s.track === track;
+        });
+        if (alreadyExists) {
+          throw new DOMException('Track already exists.',
+              'InvalidAccessError');
+        }
+      });
+      // Add identity mapping for consistency with addTrack.
+      // Unless this is being used with a stream from addTrack.
+      if (!pc._reverseStreams[stream.id]) {
+        var newStream = new window.MediaStream(stream.getTracks());
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        stream = newStream;
+      }
+      origAddStream.apply(pc, [stream]);
+    };
+
+    var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+      delete pc._reverseStreams[(pc._streams[stream.id] ?
+          pc._streams[stream.id].id : stream.id)];
+      delete pc._streams[stream.id];
+    };
+
+    window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      var streams = [].slice.call(arguments, 1);
+      if (streams.length !== 1 ||
+          !streams[0].getTracks().find(function(t) {
+            return t === track;
+          })) {
+        // this is not fully correct but all we can manage without
+        // [[associated MediaStreams]] internal slot.
+        throw new DOMException(
+          'The adapter.js addTrack polyfill only supports a single ' +
+          ' stream which is associated with the specified track.',
+          'NotSupportedError');
+      }
+
+      var alreadyExists = pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+      if (alreadyExists) {
+        throw new DOMException('Track already exists.',
+            'InvalidAccessError');
+      }
+
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+      var oldStream = pc._streams[stream.id];
+      if (oldStream) {
+        // this is using odd Chrome behaviour, use with caution:
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
+        // Note: we rely on the high-level addTrack/dtmf shim to
+        // create the sender with a dtmf sender.
+        oldStream.addTrack(track);
+
+        // Trigger ONN async.
+        Promise.resolve().then(function() {
+          pc.dispatchEvent(new Event('negotiationneeded'));
+        });
+      } else {
+        var newStream = new window.MediaStream([track]);
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        pc.addStream(newStream);
+      }
+      return pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+    };
+
+    // replace the internal stream id with the external one and
+    // vice versa.
+    function replaceInternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(internalStream.id, 'g'),
+            externalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    function replaceExternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(externalStream.id, 'g'),
+            internalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    ['createOffer', 'createAnswer'].forEach(function(method) {
+      var nativeMethod = window.RTCPeerConnection.prototype[method];
+      window.RTCPeerConnection.prototype[method] = function() {
+        var pc = this;
+        var args = arguments;
+        var isLegacyCall = arguments.length &&
+            typeof arguments[0] === 'function';
+        if (isLegacyCall) {
+          return nativeMethod.apply(pc, [
+            function(description) {
+              var desc = replaceInternalStreamId(pc, description);
+              args[0].apply(null, [desc]);
+            },
+            function(err) {
+              if (args[1]) {
+                args[1].apply(null, err);
+              }
+            }, arguments[2]
+          ]);
+        }
+        return nativeMethod.apply(pc, arguments)
+        .then(function(description) {
+          return replaceInternalStreamId(pc, description);
+        });
+      };
+    });
+
+    var origSetLocalDescription =
+        window.RTCPeerConnection.prototype.setLocalDescription;
+    window.RTCPeerConnection.prototype.setLocalDescription = function() {
+      var pc = this;
+      if (!arguments.length || !arguments[0].type) {
+        return origSetLocalDescription.apply(pc, arguments);
+      }
+      arguments[0] = replaceExternalStreamId(pc, arguments[0]);
+      return origSetLocalDescription.apply(pc, arguments);
+    };
+
+    // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier
+
+    var origLocalDescription = Object.getOwnPropertyDescriptor(
+        window.RTCPeerConnection.prototype, 'localDescription');
+    Object.defineProperty(window.RTCPeerConnection.prototype,
+        'localDescription', {
+          get: function() {
+            var pc = this;
+            var description = origLocalDescription.get.apply(this);
+            if (description.type === '') {
+              return description;
+            }
+            return replaceInternalStreamId(pc, description);
+          }
+        });
+
+    window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      // We can not yet check for sender instanceof RTCRtpSender
+      // since we shim RTPSender. So we check if sender._pc is set.
+      if (!sender._pc) {
+        throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' +
+            'does not implement interface RTCRtpSender.', 'TypeError');
+      }
+      var isLocal = sender._pc === pc;
+      if (!isLocal) {
+        throw new DOMException('Sender was not created by this connection.',
+            'InvalidAccessError');
+      }
+
+      // Search for the native stream the senders track belongs to.
+      pc._streams = pc._streams || {};
+      var stream;
+      Object.keys(pc._streams).forEach(function(streamid) {
+        var hasTrack = pc._streams[streamid].getTracks().find(function(track) {
+          return sender.track === track;
+        });
+        if (hasTrack) {
+          stream = pc._streams[streamid];
+        }
+      });
+
+      if (stream) {
+        if (stream.getTracks().length === 1) {
+          // if this is the last track of the stream, remove the stream. This
+          // takes care of any shimmed _senders.
+          pc.removeStream(pc._reverseStreams[stream.id]);
+        } else {
+          // relying on the same odd chrome behaviour as above.
+          stream.removeTrack(sender.track);
+        }
+        pc.dispatchEvent(new Event('negotiationneeded'));
+      }
+    };
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        // Translate iceTransportPolicy to iceTransports,
+        // see https://code.google.com/p/webrtc/issues/detail?id=4869
+        // this was fixed in M56 along with unprefixing RTCPeerConnection.
+        logging('PeerConnection');
+        if (pcConfig && pcConfig.iceTransportPolicy) {
+          pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+        }
+
+        return new window.webkitRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.webkitRTCPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      if (window.webkitRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.webkitRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+    } else {
+      // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+      var OrigPeerConnection = window.RTCPeerConnection;
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (pcConfig && pcConfig.iceServers) {
+          var newIceServers = [];
+          for (var i = 0; i < pcConfig.iceServers.length; i++) {
+            var server = pcConfig.iceServers[i];
+            if (!server.hasOwnProperty('urls') &&
+                server.hasOwnProperty('url')) {
+              utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+              server = JSON.parse(JSON.stringify(server));
+              server.urls = server.url;
+              newIceServers.push(server);
+            } else {
+              newIceServers.push(pcConfig.iceServers[i]);
+            }
+          }
+          pcConfig.iceServers = newIceServers;
+        }
+        return new OrigPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+
+    var origGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(selector,
+        successCallback, errorCallback) {
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats.apply(this, arguments);
+      }
+
+      // When spec-style getStats is supported, return those when called with
+      // either no arguments or the selector argument is null.
+      if (origGetStats.length === 0 && (arguments.length === 0 ||
+          typeof arguments[0] !== 'function')) {
+        return origGetStats.apply(this, []);
+      }
+
+      var fixChromeStats_ = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: {
+              localcandidate: 'local-candidate',
+              remotecandidate: 'remote-candidate'
+            }[report.type] || report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      // shim getStats with maplike support
+      var makeMapStats = function(stats) {
+        return new Map(Object.keys(stats).map(function(key) {
+          return [key, stats[key]];
+        }));
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper_ = function(response) {
+          args[1](makeMapStats(fixChromeStats_(response)));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper_,
+          arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        origGetStats.apply(self, [
+          function(response) {
+            resolve(makeMapStats(fixChromeStats_(response)));
+          }, reject]);
+      }).then(successCallback, errorCallback);
+    };
+
+    // add promise support -- natively available in Chrome 51
+    if (browserDetails.version < 51) {
+      ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+          .forEach(function(method) {
+            var nativeMethod = window.RTCPeerConnection.prototype[method];
+            window.RTCPeerConnection.prototype[method] = function() {
+              var args = arguments;
+              var self = this;
+              var promise = new Promise(function(resolve, reject) {
+                nativeMethod.apply(self, [args[0], resolve, reject]);
+              });
+              if (args.length < 2) {
+                return promise;
+              }
+              return promise.then(function() {
+                args[1].apply(null, []);
+              },
+              function(err) {
+                if (args.length >= 3) {
+                  args[2].apply(null, [err]);
+                }
+              });
+            };
+          });
+    }
+
+    // promise support for createOffer and createAnswer. Available (without
+    // bugs) since M52: crbug/619289
+    if (browserDetails.version < 52) {
+      ['createOffer', 'createAnswer'].forEach(function(method) {
+        var nativeMethod = window.RTCPeerConnection.prototype[method];
+        window.RTCPeerConnection.prototype[method] = function() {
+          var self = this;
+          if (arguments.length < 1 || (arguments.length === 1 &&
+              typeof arguments[0] === 'object')) {
+            var opts = arguments.length === 1 ? arguments[0] : undefined;
+            return new Promise(function(resolve, reject) {
+              nativeMethod.apply(self, [resolve, reject, opts]);
+            });
+          }
+          return nativeMethod.apply(this, arguments);
+        };
+      });
+    }
+
+    // shim implicit creation of RTCSessionDescription/RTCIceCandidate
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+  }
+};
+
+
+// Expose public methods.
+module.exports = {
+  shimMediaStream: chromeShim.shimMediaStream,
+  shimOnTrack: chromeShim.shimOnTrack,
+  shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack,
+  shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf,
+  shimSourceObject: chromeShim.shimSourceObject,
+  shimPeerConnection: chromeShim.shimPeerConnection,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils.js":13,"./getusermedia":6}],6:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+
+  var constraintsToChrome_ = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname_ = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname_('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname_('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname_('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname_('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname_(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  var shimConstraints_ = function(constraints, func) {
+    if (browserDetails.version >= 61) {
+      return func(constraints);
+    }
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (constraints && typeof constraints.audio === 'object') {
+      var remap = function(obj, a, b) {
+        if (a in obj && !(b in obj)) {
+          obj[b] = obj[a];
+          delete obj[a];
+        }
+      };
+      constraints = JSON.parse(JSON.stringify(constraints));
+      remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
+      remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
+      constraints.audio = constraintsToChrome_(constraints.audio);
+    }
+    if (constraints && typeof constraints.video === 'object') {
+      // Shim facingMode for mobile & surface pro.
+      var face = constraints.video.facingMode;
+      face = face && ((typeof face === 'object') ? face : {ideal: face});
+      var getSupportedFacingModeLies = browserDetails.version < 66;
+
+      if ((face && (face.exact === 'user' || face.exact === 'environment' ||
+                    face.ideal === 'user' || face.ideal === 'environment')) &&
+          !(navigator.mediaDevices.getSupportedConstraints &&
+            navigator.mediaDevices.getSupportedConstraints().facingMode &&
+            !getSupportedFacingModeLies)) {
+        delete constraints.video.facingMode;
+        var matches;
+        if (face.exact === 'environment' || face.ideal === 'environment') {
+          matches = ['back', 'rear'];
+        } else if (face.exact === 'user' || face.ideal === 'user') {
+          matches = ['front'];
+        }
+        if (matches) {
+          // Look for matches in label, or use last cam for back (typical).
+          return navigator.mediaDevices.enumerateDevices()
+          .then(function(devices) {
+            devices = devices.filter(function(d) {
+              return d.kind === 'videoinput';
+            });
+            var dev = devices.find(function(d) {
+              return matches.some(function(match) {
+                return d.label.toLowerCase().indexOf(match) !== -1;
+              });
+            });
+            if (!dev && devices.length && matches.indexOf('back') !== -1) {
+              dev = devices[devices.length - 1]; // more likely the back cam
+            }
+            if (dev) {
+              constraints.video.deviceId = face.exact ? {exact: dev.deviceId} :
+                                                        {ideal: dev.deviceId};
+            }
+            constraints.video = constraintsToChrome_(constraints.video);
+            logging('chrome: ' + JSON.stringify(constraints));
+            return func(constraints);
+          });
+        }
+      }
+      constraints.video = constraintsToChrome_(constraints.video);
+    }
+    logging('chrome: ' + JSON.stringify(constraints));
+    return func(constraints);
+  };
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        PermissionDeniedError: 'NotAllowedError',
+        InvalidStateError: 'NotReadableError',
+        DevicesNotFoundError: 'NotFoundError',
+        ConstraintNotSatisfiedError: 'OverconstrainedError',
+        TrackStartError: 'NotReadableError',
+        MediaDeviceFailedDueToShutdown: 'NotReadableError',
+        MediaDeviceKillSwitchOn: 'NotReadableError'
+      }[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraintName,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    shimConstraints_(constraints, function(c) {
+      navigator.webkitGetUserMedia(c, onSuccess, function(e) {
+        if (onError) {
+          onError(shimError_(e));
+        }
+      });
+    });
+  };
+
+  navigator.getUserMedia = getUserMedia_;
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      navigator.getUserMedia(constraints, resolve, reject);
+    });
+  };
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {
+      getUserMedia: getUserMediaPromise_,
+      enumerateDevices: function() {
+        return new Promise(function(resolve) {
+          var kinds = {audio: 'audioinput', video: 'videoinput'};
+          return window.MediaStreamTrack.getSources(function(devices) {
+            resolve(devices.map(function(device) {
+              return {label: device.label,
+                kind: kinds[device.kind],
+                deviceId: device.id,
+                groupId: ''};
+            }));
+          });
+        });
+      },
+      getSupportedConstraints: function() {
+        return {
+          deviceId: true, echoCancellation: true, facingMode: true,
+          frameRate: true, height: true, width: true
+        };
+      }
+    };
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return getUserMediaPromise_(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(cs) {
+      return shimConstraints_(cs, function(c) {
+        return origGetUserMedia(c).then(function(stream) {
+          if (c.audio && !stream.getAudioTracks().length ||
+              c.video && !stream.getVideoTracks().length) {
+            stream.getTracks().forEach(function(track) {
+              track.stop();
+            });
+            throw new DOMException('', 'NotFoundError');
+          }
+          return stream;
+        }, function(e) {
+          return Promise.reject(shimError_(e));
+        });
+      });
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      logging('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      logging('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+};
+
+},{"../utils.js":13}],7:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+var utils = require('./utils');
+
+// Wraps the peerconnection event eventNameToWrap in a function
+// which returns the modified event object.
+function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
+  if (!window.RTCPeerConnection) {
+    return;
+  }
+  var proto = window.RTCPeerConnection.prototype;
+  var nativeAddEventListener = proto.addEventListener;
+  proto.addEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap) {
+      return nativeAddEventListener.apply(this, arguments);
+    }
+    var wrappedCallback = function(e) {
+      cb(wrapper(e));
+    };
+    this._eventMap = this._eventMap || {};
+    this._eventMap[cb] = wrappedCallback;
+    return nativeAddEventListener.apply(this, [nativeEventName,
+      wrappedCallback]);
+  };
+
+  var nativeRemoveEventListener = proto.removeEventListener;
+  proto.removeEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap || !this._eventMap
+        || !this._eventMap[cb]) {
+      return nativeRemoveEventListener.apply(this, arguments);
+    }
+    var unwrappedCb = this._eventMap[cb];
+    delete this._eventMap[cb];
+    return nativeRemoveEventListener.apply(this, [nativeEventName,
+      unwrappedCb]);
+  };
+
+  Object.defineProperty(proto, 'on' + eventNameToWrap, {
+    get: function() {
+      return this['_on' + eventNameToWrap];
+    },
+    set: function(cb) {
+      if (this['_on' + eventNameToWrap]) {
+        this.removeEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap]);
+        delete this['_on' + eventNameToWrap];
+      }
+      if (cb) {
+        this.addEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap] = cb);
+      }
+    }
+  });
+}
+
+module.exports = {
+  shimRTCIceCandidate: function(window) {
+    // foundation is arbitrarily chosen as an indicator for full support for
+    // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
+    if (window.RTCIceCandidate && 'foundation' in
+        window.RTCIceCandidate.prototype) {
+      return;
+    }
+
+    var NativeRTCIceCandidate = window.RTCIceCandidate;
+    window.RTCIceCandidate = function(args) {
+      // Remove the a= which shouldn't be part of the candidate string.
+      if (typeof args === 'object' && args.candidate &&
+          args.candidate.indexOf('a=') === 0) {
+        args = JSON.parse(JSON.stringify(args));
+        args.candidate = args.candidate.substr(2);
+      }
+
+      // Augment the native candidate with the parsed fields.
+      var nativeCandidate = new NativeRTCIceCandidate(args);
+      var parsedCandidate = SDPUtils.parseCandidate(args.candidate);
+      var augmentedCandidate = Object.assign(nativeCandidate,
+          parsedCandidate);
+
+      // Add a serializer that does not serialize the extra attributes.
+      augmentedCandidate.toJSON = function() {
+        return {
+          candidate: augmentedCandidate.candidate,
+          sdpMid: augmentedCandidate.sdpMid,
+          sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
+          usernameFragment: augmentedCandidate.usernameFragment,
+        };
+      };
+      return augmentedCandidate;
+    };
+
+    // Hook up the augmented candidate in onicecandidate and
+    // addEventListener('icecandidate', ...)
+    wrapPeerConnectionEvent(window, 'icecandidate', function(e) {
+      if (e.candidate) {
+        Object.defineProperty(e, 'candidate', {
+          value: new window.RTCIceCandidate(e.candidate),
+          writable: 'false'
+        });
+      }
+      return e;
+    });
+  },
+
+  // shimCreateObjectURL must be called before shimSourceObject to avoid loop.
+
+  shimCreateObjectURL: function(window) {
+    var URL = window && window.URL;
+
+    if (!(typeof window === 'object' && window.HTMLMediaElement &&
+          'srcObject' in window.HTMLMediaElement.prototype &&
+        URL.createObjectURL && URL.revokeObjectURL)) {
+      // Only shim CreateObjectURL using srcObject if srcObject exists.
+      return undefined;
+    }
+
+    var nativeCreateObjectURL = URL.createObjectURL.bind(URL);
+    var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL);
+    var streams = new Map(), newId = 0;
+
+    URL.createObjectURL = function(stream) {
+      if ('getTracks' in stream) {
+        var url = 'polyblob:' + (++newId);
+        streams.set(url, stream);
+        utils.deprecated('URL.createObjectURL(stream)',
+            'elem.srcObject = stream');
+        return url;
+      }
+      return nativeCreateObjectURL(stream);
+    };
+    URL.revokeObjectURL = function(url) {
+      nativeRevokeObjectURL(url);
+      streams.delete(url);
+    };
+
+    var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,
+                                              'src');
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'src', {
+      get: function() {
+        return dsc.get.apply(this);
+      },
+      set: function(url) {
+        this.srcObject = streams.get(url) || null;
+        return dsc.set.apply(this, [url]);
+      }
+    });
+
+    var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute;
+    window.HTMLMediaElement.prototype.setAttribute = function() {
+      if (arguments.length === 2 &&
+          ('' + arguments[0]).toLowerCase() === 'src') {
+        this.srcObject = streams.get(arguments[1]) || null;
+      }
+      return nativeSetAttribute.apply(this, arguments);
+    };
+  }
+};
+
+},{"./utils":13,"sdp":2}],8:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var shimRTCPeerConnection = require('rtcpeerconnection-shim');
+
+module.exports = {
+  shimGetUserMedia: require('./getusermedia'),
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (window.RTCIceGatherer) {
+      // ORTC defines an RTCIceCandidate object but no constructor.
+      // Not implemented in Edge.
+      if (!window.RTCIceCandidate) {
+        window.RTCIceCandidate = function(args) {
+          return args;
+        };
+      }
+      // ORTC does not have a session description object but
+      // other browsers (i.e. Chrome) that will support both PC and ORTC
+      // in the future might have this defined already.
+      if (!window.RTCSessionDescription) {
+        window.RTCSessionDescription = function(args) {
+          return args;
+        };
+      }
+      // this adds an additional event listener to MediaStrackTrack that signals
+      // when a tracks enabled property was changed. Workaround for a bug in
+      // addStream, see below. No longer required in 15025+
+      if (browserDetails.version < 15025) {
+        var origMSTEnabled = Object.getOwnPropertyDescriptor(
+            window.MediaStreamTrack.prototype, 'enabled');
+        Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', {
+          set: function(value) {
+            origMSTEnabled.set.call(this, value);
+            var ev = new Event('enabled');
+            ev.enabled = value;
+            this.dispatchEvent(ev);
+          }
+        });
+      }
+    }
+
+    // ORTC defines the DTMF sender a bit different.
+    // https://github.com/w3c/ortc/issues/714
+    if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = new window.RTCDtmfSender(this);
+            } else if (this.track.kind === 'video') {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+
+    window.RTCPeerConnection =
+        shimRTCPeerConnection(window, browserDetails.version);
+  },
+  shimReplaceTrack: function(window) {
+    // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614
+    if (window.RTCRtpSender &&
+        !('replaceTrack' in window.RTCRtpSender.prototype)) {
+      window.RTCRtpSender.prototype.replaceTrack =
+          window.RTCRtpSender.prototype.setTrack;
+    }
+  }
+};
+
+},{"../utils":13,"./getusermedia":9,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+// Expose public methods.
+module.exports = function(window) {
+  var navigator = window && window.navigator;
+
+  var shimError_ = function(e) {
+    return {
+      name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name;
+      }
+    };
+  };
+
+  // getUserMedia error shim.
+  var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+      bind(navigator.mediaDevices);
+  navigator.mediaDevices.getUserMedia = function(c) {
+    return origGetUserMedia(c).catch(function(e) {
+      return Promise.reject(shimError_(e));
+    });
+  };
+};
+
+},{}],10:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+
+var firefoxShim = {
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+            this.removeEventListener('addstream', this._ontrackpoly);
+          }
+          this.addEventListener('track', this._ontrack = f);
+          this.addEventListener('addstream', this._ontrackpoly = function(e) {
+            e.stream.getTracks().forEach(function(track) {
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = {track: track};
+              event.transceiver = {receiver: event.receiver};
+              event.streams = [e.stream];
+              this.dispatchEvent(event);
+            }.bind(this));
+          }.bind(this));
+        }
+      });
+    }
+    if (typeof window === 'object' && window.RTCTrackEvent &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        !('transceiver' in window.RTCTrackEvent.prototype)) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    // Firefox has supported mozSrcObject since FF22, unprefixed in 42.
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this.mozSrcObject;
+          },
+          set: function(stream) {
+            this.mozSrcObject = stream;
+          }
+        });
+      }
+    }
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (typeof window !== 'object' || !(window.RTCPeerConnection ||
+        window.mozRTCPeerConnection)) {
+      return; // probably media.peerconnection.enabled=false in about:config
+    }
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (browserDetails.version < 38) {
+          // .urls is not supported in FF < 38.
+          // create RTCIceServers with a single url.
+          if (pcConfig && pcConfig.iceServers) {
+            var newIceServers = [];
+            for (var i = 0; i < pcConfig.iceServers.length; i++) {
+              var server = pcConfig.iceServers[i];
+              if (server.hasOwnProperty('urls')) {
+                for (var j = 0; j < server.urls.length; j++) {
+                  var newServer = {
+                    url: server.urls[j]
+                  };
+                  if (server.urls[j].indexOf('turn') === 0) {
+                    newServer.username = server.username;
+                    newServer.credential = server.credential;
+                  }
+                  newIceServers.push(newServer);
+                }
+              } else {
+                newIceServers.push(pcConfig.iceServers[i]);
+              }
+            }
+            pcConfig.iceServers = newIceServers;
+          }
+        }
+        return new window.mozRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.mozRTCPeerConnection.prototype;
+
+      // wrap static methods. Currently just generateCertificate.
+      if (window.mozRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.mozRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+
+      window.RTCSessionDescription = window.mozRTCSessionDescription;
+      window.RTCIceCandidate = window.mozRTCIceCandidate;
+    }
+
+    // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+
+    // shim getStats with maplike support
+    var makeMapStats = function(stats) {
+      var map = new Map();
+      Object.keys(stats).forEach(function(key) {
+        map.set(key, stats[key]);
+        map[key] = stats[key];
+      });
+      return map;
+    };
+
+    var modernStatsTypes = {
+      inboundrtp: 'inbound-rtp',
+      outboundrtp: 'outbound-rtp',
+      candidatepair: 'candidate-pair',
+      localcandidate: 'local-candidate',
+      remotecandidate: 'remote-candidate'
+    };
+
+    var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(
+      selector,
+      onSucc,
+      onErr
+    ) {
+      return nativeGetStats.apply(this, [selector || null])
+        .then(function(stats) {
+          if (browserDetails.version < 48) {
+            stats = makeMapStats(stats);
+          }
+          if (browserDetails.version < 53 && !onSucc) {
+            // Shim only promise getStats with spec-hyphens in type names
+            // Leave callback version alone; misc old uses of forEach before Map
+            try {
+              stats.forEach(function(stat) {
+                stat.type = modernStatsTypes[stat.type] || stat.type;
+              });
+            } catch (e) {
+              if (e.name !== 'TypeError') {
+                throw e;
+              }
+              // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
+              stats.forEach(function(stat, i) {
+                stats.set(i, Object.assign({}, stat, {
+                  type: modernStatsTypes[stat.type] || stat.type
+                }));
+              });
+            }
+          }
+          return stats;
+        })
+        .then(onSucc, onErr);
+    };
+  },
+
+  shimRemoveStream: function(window) {
+    if (!window.RTCPeerConnection ||
+        'removeStream' in window.RTCPeerConnection.prototype) {
+      return;
+    }
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      utils.deprecated('removeStream', 'removeTrack');
+      this.getSenders().forEach(function(sender) {
+        if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) {
+          pc.removeTrack(sender);
+        }
+      });
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimOnTrack: firefoxShim.shimOnTrack,
+  shimSourceObject: firefoxShim.shimSourceObject,
+  shimPeerConnection: firefoxShim.shimPeerConnection,
+  shimRemoveStream: firefoxShim.shimRemoveStream,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils":13,"./getusermedia":11}],11:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+  var MediaStreamTrack = window && window.MediaStreamTrack;
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        InternalError: 'NotReadableError',
+        NotSupportedError: 'TypeError',
+        PermissionDeniedError: 'NotAllowedError',
+        SecurityError: 'NotAllowedError'
+      }[e.name] || e.name,
+      message: {
+        'The operation is insecure.': 'The request is not allowed by the ' +
+        'user agent or the platform in the current context.'
+      }[e.message] || e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  // getUserMedia constraints shim.
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    var constraintsToFF37_ = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r. min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (browserDetails.version < 38) {
+      logging('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37_(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37_(constraints.video);
+      }
+      logging('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
+      onError(shimError_(e));
+    });
+  };
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      getUserMedia_(constraints, resolve, reject);
+    });
+  };
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+        return new Promise(function(resolve) {
+          var infos = [
+            {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+            {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+          ];
+          resolve(infos);
+        });
+      };
+
+  if (browserDetails.version < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+  if (browserDetails.version < 49) {
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      return origGetUserMedia(c).then(function(stream) {
+        // Work around https://bugzil.la/802326
+        if (c.audio && !stream.getAudioTracks().length ||
+            c.video && !stream.getVideoTracks().length) {
+          stream.getTracks().forEach(function(track) {
+            track.stop();
+          });
+          throw new DOMException('The object can not be found here.',
+                                 'NotFoundError');
+        }
+        return stream;
+      }, function(e) {
+        return Promise.reject(shimError_(e));
+      });
+    };
+  }
+  if (!(browserDetails.version > 55 &&
+      'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
+    var remap = function(obj, a, b) {
+      if (a in obj && !(b in obj)) {
+        obj[b] = obj[a];
+        delete obj[a];
+      }
+    };
+
+    var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      if (typeof c === 'object' && typeof c.audio === 'object') {
+        c = JSON.parse(JSON.stringify(c));
+        remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
+        remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
+      }
+      return nativeGetUserMedia(c);
+    };
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
+      var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
+      MediaStreamTrack.prototype.getSettings = function() {
+        var obj = nativeGetSettings.apply(this, arguments);
+        remap(obj, 'mozAutoGainControl', 'autoGainControl');
+        remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
+        return obj;
+      };
+    }
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
+      var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
+      MediaStreamTrack.prototype.applyConstraints = function(c) {
+        if (this.kind === 'audio' && typeof c === 'object') {
+          c = JSON.parse(JSON.stringify(c));
+          remap(c, 'autoGainControl', 'mozAutoGainControl');
+          remap(c, 'noiseSuppression', 'mozNoiseSuppression');
+        }
+        return nativeApplyConstraints.apply(this, [c]);
+      };
+    }
+  }
+  navigator.getUserMedia = function(constraints, onSuccess, onError) {
+    if (browserDetails.version < 44) {
+      return getUserMedia_(constraints, onSuccess, onError);
+    }
+    // Replace Firefox 44+'s deprecation warning with unprefixed version.
+    utils.deprecated('navigator.getUserMedia',
+        'navigator.mediaDevices.getUserMedia');
+    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
+  };
+};
+
+},{"../utils":13}],12:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+'use strict';
+var utils = require('../utils');
+
+var safariShim = {
+  // TODO: DrAlex, should be here, double check against LayoutTests
+
+  // TODO: once the back-end for the mac port is done, add.
+  // TODO: check for webkitGTK+
+  // shimPeerConnection: function() { },
+
+  shimLocalStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getLocalStreams = function() {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        return this._localStreams;
+      };
+    }
+    if (!('getStreamById' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getStreamById = function(id) {
+        var result = null;
+        if (this._localStreams) {
+          this._localStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        if (this._remoteStreams) {
+          this._remoteStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        return result;
+      };
+    }
+    if (!('addStream' in window.RTCPeerConnection.prototype)) {
+      var _addTrack = window.RTCPeerConnection.prototype.addTrack;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        if (this._localStreams.indexOf(stream) === -1) {
+          this._localStreams.push(stream);
+        }
+        var self = this;
+        stream.getTracks().forEach(function(track) {
+          _addTrack.call(self, track, stream);
+        });
+      };
+
+      window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+        if (stream) {
+          if (!this._localStreams) {
+            this._localStreams = [stream];
+          } else if (this._localStreams.indexOf(stream) === -1) {
+            this._localStreams.push(stream);
+          }
+        }
+        return _addTrack.call(this, track, stream);
+      };
+    }
+    if (!('removeStream' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        var index = this._localStreams.indexOf(stream);
+        if (index === -1) {
+          return;
+        }
+        this._localStreams.splice(index, 1);
+        var self = this;
+        var tracks = stream.getTracks();
+        this.getSenders().forEach(function(sender) {
+          if (tracks.indexOf(sender.track) !== -1) {
+            self.removeTrack(sender);
+          }
+        });
+      };
+    }
+  },
+  shimRemoteStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getRemoteStreams = function() {
+        return this._remoteStreams ? this._remoteStreams : [];
+      };
+    }
+    if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
+        get: function() {
+          return this._onaddstream;
+        },
+        set: function(f) {
+          if (this._onaddstream) {
+            this.removeEventListener('addstream', this._onaddstream);
+            this.removeEventListener('track', this._onaddstreampoly);
+          }
+          this.addEventListener('addstream', this._onaddstream = f);
+          this.addEventListener('track', this._onaddstreampoly = function(e) {
+            var stream = e.streams[0];
+            if (!this._remoteStreams) {
+              this._remoteStreams = [];
+            }
+            if (this._remoteStreams.indexOf(stream) >= 0) {
+              return;
+            }
+            this._remoteStreams.push(stream);
+            var event = new Event('addstream');
+            event.stream = e.streams[0];
+            this.dispatchEvent(event);
+          }.bind(this));
+        }
+      });
+    }
+  },
+  shimCallbacksAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    var prototype = window.RTCPeerConnection.prototype;
+    var createOffer = prototype.createOffer;
+    var createAnswer = prototype.createAnswer;
+    var setLocalDescription = prototype.setLocalDescription;
+    var setRemoteDescription = prototype.setRemoteDescription;
+    var addIceCandidate = prototype.addIceCandidate;
+
+    prototype.createOffer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createOffer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    prototype.createAnswer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createAnswer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    var withCallback = function(description, successCallback, failureCallback) {
+      var promise = setLocalDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setLocalDescription = withCallback;
+
+    withCallback = function(description, successCallback, failureCallback) {
+      var promise = setRemoteDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setRemoteDescription = withCallback;
+
+    withCallback = function(candidate, successCallback, failureCallback) {
+      var promise = addIceCandidate.apply(this, [candidate]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.addIceCandidate = withCallback;
+  },
+  shimGetUserMedia: function(window) {
+    var navigator = window && window.navigator;
+
+    if (!navigator.getUserMedia) {
+      if (navigator.webkitGetUserMedia) {
+        navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+      } else if (navigator.mediaDevices &&
+          navigator.mediaDevices.getUserMedia) {
+        navigator.getUserMedia = function(constraints, cb, errcb) {
+          navigator.mediaDevices.getUserMedia(constraints)
+          .then(cb, errcb);
+        }.bind(navigator);
+      }
+    }
+  },
+  shimRTCIceServerUrls: function(window) {
+    // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+    var OrigPeerConnection = window.RTCPeerConnection;
+    window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (!server.hasOwnProperty('urls') &&
+              server.hasOwnProperty('url')) {
+            utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+            server = JSON.parse(JSON.stringify(server));
+            server.urls = server.url;
+            delete server.url;
+            newIceServers.push(server);
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+      return new OrigPeerConnection(pcConfig, pcConstraints);
+    };
+    window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+    // wrap static methods. Currently just generateCertificate.
+    if ('generateCertificate' in window.RTCPeerConnection) {
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+  },
+  shimTrackEventTransceiver: function(window) {
+    // Add event.transceiver member over deprecated event.receiver
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is
+        // defined for some reason even when window.RTCTransceiver is not.
+        !window.RTCTransceiver) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimCreateOfferLegacy: function(window) {
+    var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
+    window.RTCPeerConnection.prototype.createOffer = function(offerOptions) {
+      var pc = this;
+      if (offerOptions) {
+        var audioTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'audio';
+        });
+        if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
+          if (audioTransceiver.direction === 'sendrecv') {
+            audioTransceiver.setDirection('sendonly');
+          } else if (audioTransceiver.direction === 'recvonly') {
+            audioTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveAudio === true &&
+            !audioTransceiver) {
+          pc.addTransceiver('audio');
+        }
+
+        var videoTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'video';
+        });
+        if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
+          if (videoTransceiver.direction === 'sendrecv') {
+            videoTransceiver.setDirection('sendonly');
+          } else if (videoTransceiver.direction === 'recvonly') {
+            videoTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveVideo === true &&
+            !videoTransceiver) {
+          pc.addTransceiver('video');
+        }
+      }
+      return origCreateOffer.apply(pc, arguments);
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimCallbacksAPI: safariShim.shimCallbacksAPI,
+  shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI,
+  shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI,
+  shimGetUserMedia: safariShim.shimGetUserMedia,
+  shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls,
+  shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver,
+  shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy
+  // TODO
+  // shimPeerConnection: safariShim.shimPeerConnection
+};
+
+},{"../utils":13}],13:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var logDisabled_ = true;
+var deprecationWarnings_ = true;
+
+// Utility methods.
+var utils = {
+  disableLog: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    logDisabled_ = bool;
+    return (bool) ? 'adapter.js logging disabled' :
+        'adapter.js logging enabled';
+  },
+
+  /**
+   * Disable or enable deprecation warnings
+   * @param {!boolean} bool set to true to disable warnings.
+   */
+  disableWarnings: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    deprecationWarnings_ = !bool;
+    return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
+  },
+
+  log: function() {
+    if (typeof window === 'object') {
+      if (logDisabled_) {
+        return;
+      }
+      if (typeof console !== 'undefined' && typeof console.log === 'function') {
+        console.log.apply(console, arguments);
+      }
+    }
+  },
+
+  /**
+   * Shows a deprecation warning suggesting the modern and spec-compatible API.
+   */
+  deprecated: function(oldMethod, newMethod) {
+    if (!deprecationWarnings_) {
+      return;
+    }
+    console.warn(oldMethod + ' is deprecated, please use ' + newMethod +
+        ' instead.');
+  },
+
+  /**
+   * Extract browser version out of the provided user agent string.
+   *
+   * @param {!string} uastring userAgent string.
+   * @param {!string} expr Regular expression used as match criteria.
+   * @param {!number} pos position in the version string to be returned.
+   * @return {!number} browser version.
+   */
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos], 10);
+  },
+
+  /**
+   * Browser detector.
+   *
+   * @return {object} result containing browser and version
+   *     properties.
+   */
+  detectBrowser: function(window) {
+    var navigator = window && window.navigator;
+
+    // Returned result object.
+    var result = {};
+    result.browser = null;
+    result.version = null;
+
+    // Fail early if it's not a browser
+    if (typeof window === 'undefined' || !window.navigator) {
+      result.browser = 'Not a browser.';
+      return result;
+    }
+
+    // Firefox.
+    if (navigator.mozGetUserMedia) {
+      result.browser = 'firefox';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Firefox\/(\d+)\./, 1);
+    } else if (navigator.webkitGetUserMedia) {
+      // Chrome, Chromium, Webview, Opera, all use the chrome shim for now
+      if (window.webkitRTCPeerConnection) {
+        result.browser = 'chrome';
+        result.version = this.extractVersion(navigator.userAgent,
+          /Chrom(e|ium)\/(\d+)\./, 2);
+      } else { // Safari (in an unpublished version) or unknown webkit-based.
+        if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
+          result.browser = 'safari';
+          result.version = this.extractVersion(navigator.userAgent,
+            /AppleWebKit\/(\d+)\./, 1);
+        } else { // unknown webkit-based browser.
+          result.browser = 'Unsupported webkit-based browser ' +
+              'with GUM support but no WebRTC support.';
+          return result;
+        }
+      }
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
+      result.browser = 'edge';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Edge\/(\d+).(\d+)$/, 2);
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
+        // Safari, with webkitGetUserMedia removed.
+      result.browser = 'safari';
+      result.version = this.extractVersion(navigator.userAgent,
+          /AppleWebKit\/(\d+)\./, 1);
+    } else { // Default fallthrough: not supported.
+      result.browser = 'Not a supported browser.';
+      return result;
+    }
+
+    return result;
+  },
+
+};
+
+// Export.
+module.exports = {
+  log: utils.log,
+  deprecated: utils.deprecated,
+  disableLog: utils.disableLog,
+  disableWarnings: utils.disableWarnings,
+  extractVersion: utils.extractVersion,
+  shimCreateObjectURL: utils.shimCreateObjectURL,
+  detectBrowser: utils.detectBrowser.bind(utils)
+};
+
+},{}]},{},[3])(3)
+});

+ 4471 - 0
support/client/lib/vwf/view/webrtc/dist/adapter.js

@@ -0,0 +1,4471 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+
+function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : dtlsRole || 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+}
+
+// Edge does not like
+// 1) stun: filtered after 14393 unless ?transport=udp is present
+// 2) turn: that does not have all of turn:host:port?transport=udp
+// 3) turn: with ipv6 addresses
+// 4) turn: occurring muliple times
+function filterIceServers(iceServers, edgeVersion) {
+  var hasTurn = false;
+  iceServers = JSON.parse(JSON.stringify(iceServers));
+  return iceServers.filter(function(server) {
+    if (server && (server.urls || server.url)) {
+      var urls = server.urls || server.url;
+      if (server.url && !server.urls) {
+        console.warn('RTCIceServer.url is deprecated! Use urls instead.');
+      }
+      var isString = typeof urls === 'string';
+      if (isString) {
+        urls = [urls];
+      }
+      urls = urls.filter(function(url) {
+        var validTurn = url.indexOf('turn:') === 0 &&
+            url.indexOf('transport=udp') !== -1 &&
+            url.indexOf('turn:[') === -1 &&
+            !hasTurn;
+
+        if (validTurn) {
+          hasTurn = true;
+          return true;
+        }
+        return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
+            url.indexOf('?transport=udp') === -1;
+      });
+
+      delete server.url;
+      server.urls = isString ? urls[0] : urls;
+      return !!urls.length;
+    }
+    return false;
+  });
+}
+
+// Determines the intersection of local and remote capabilities.
+function getCommonCapabilities(localCapabilities, remoteCapabilities) {
+  var commonCapabilities = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: []
+  };
+
+  var findCodecByPayloadType = function(pt, codecs) {
+    pt = parseInt(pt, 10);
+    for (var i = 0; i < codecs.length; i++) {
+      if (codecs[i].payloadType === pt ||
+          codecs[i].preferredPayloadType === pt) {
+        return codecs[i];
+      }
+    }
+  };
+
+  var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
+    var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
+    var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
+    return lCodec && rCodec &&
+        lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
+  };
+
+  localCapabilities.codecs.forEach(function(lCodec) {
+    for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+      var rCodec = remoteCapabilities.codecs[i];
+      if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
+          lCodec.clockRate === rCodec.clockRate) {
+        if (lCodec.name.toLowerCase() === 'rtx' &&
+            lCodec.parameters && rCodec.parameters.apt) {
+          // for RTX we need to find the local rtx that has a apt
+          // which points to the same local codec as the remote one.
+          if (!rtxCapabilityMatches(lCodec, rCodec,
+              localCapabilities.codecs, remoteCapabilities.codecs)) {
+            continue;
+          }
+        }
+        rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
+        // number of channels is the highest common number of channels
+        rCodec.numChannels = Math.min(lCodec.numChannels,
+            rCodec.numChannels);
+        // push rCodec so we reply with offerer payload type
+        commonCapabilities.codecs.push(rCodec);
+
+        // determine common feedback mechanisms
+        rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
+          for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
+            if (lCodec.rtcpFeedback[j].type === fb.type &&
+                lCodec.rtcpFeedback[j].parameter === fb.parameter) {
+              return true;
+            }
+          }
+          return false;
+        });
+        // FIXME: also need to determine .parameters
+        //  see https://github.com/openpeer/ortc/issues/569
+        break;
+      }
+    }
+  });
+
+  localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+    for (var i = 0; i < remoteCapabilities.headerExtensions.length;
+         i++) {
+      var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+      if (lHeaderExtension.uri === rHeaderExtension.uri) {
+        commonCapabilities.headerExtensions.push(rHeaderExtension);
+        break;
+      }
+    }
+  });
+
+  // FIXME: fecMechanisms
+  return commonCapabilities;
+}
+
+// is action=setLocalDescription with type allowed in signalingState
+function isActionAllowedInSignalingState(action, type, signalingState) {
+  return {
+    offer: {
+      setLocalDescription: ['stable', 'have-local-offer'],
+      setRemoteDescription: ['stable', 'have-remote-offer']
+    },
+    answer: {
+      setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
+      setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
+    }
+  }[type][action].indexOf(signalingState) !== -1;
+}
+
+function maybeAddCandidate(iceTransport, candidate) {
+  // Edge's internal representation adds some fields therefore
+  // not all fieldѕ are taken into account.
+  var alreadyAdded = iceTransport.getRemoteCandidates()
+      .find(function(remoteCandidate) {
+        return candidate.foundation === remoteCandidate.foundation &&
+            candidate.ip === remoteCandidate.ip &&
+            candidate.port === remoteCandidate.port &&
+            candidate.priority === remoteCandidate.priority &&
+            candidate.protocol === remoteCandidate.protocol &&
+            candidate.type === remoteCandidate.type;
+      });
+  if (!alreadyAdded) {
+    iceTransport.addRemoteCandidate(candidate);
+  }
+  return !alreadyAdded;
+}
+
+module.exports = function(window, edgeVersion) {
+  var RTCPeerConnection = function(config) {
+    var self = this;
+
+    var _eventTarget = document.createDocumentFragment();
+    ['addEventListener', 'removeEventListener', 'dispatchEvent']
+        .forEach(function(method) {
+          self[method] = _eventTarget[method].bind(_eventTarget);
+        });
+
+    this.onicecandidate = null;
+    this.onaddstream = null;
+    this.ontrack = null;
+    this.onremovestream = null;
+    this.onsignalingstatechange = null;
+    this.oniceconnectionstatechange = null;
+    this.onicegatheringstatechange = null;
+    this.onnegotiationneeded = null;
+    this.ondatachannel = null;
+    this.canTrickleIceCandidates = null;
+
+    this.needNegotiation = false;
+
+    this.localStreams = [];
+    this.remoteStreams = [];
+
+    this.localDescription = null;
+    this.remoteDescription = null;
+
+    this.signalingState = 'stable';
+    this.iceConnectionState = 'new';
+    this.iceGatheringState = 'new';
+
+    config = JSON.parse(JSON.stringify(config || {}));
+
+    this.usingBundle = config.bundlePolicy === 'max-bundle';
+    if (config.rtcpMuxPolicy === 'negotiate') {
+      var e = new Error('rtcpMuxPolicy \'negotiate\' is not supported');
+      e.name = 'NotSupportedError';
+      throw(e);
+    } else if (!config.rtcpMuxPolicy) {
+      config.rtcpMuxPolicy = 'require';
+    }
+
+    switch (config.iceTransportPolicy) {
+      case 'all':
+      case 'relay':
+        break;
+      default:
+        config.iceTransportPolicy = 'all';
+        break;
+    }
+
+    switch (config.bundlePolicy) {
+      case 'balanced':
+      case 'max-compat':
+      case 'max-bundle':
+        break;
+      default:
+        config.bundlePolicy = 'balanced';
+        break;
+    }
+
+    config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);
+
+    this._iceGatherers = [];
+    if (config.iceCandidatePoolSize) {
+      for (var i = config.iceCandidatePoolSize; i > 0; i--) {
+        this._iceGatherers = new window.RTCIceGatherer({
+          iceServers: config.iceServers,
+          gatherPolicy: config.iceTransportPolicy
+        });
+      }
+    } else {
+      config.iceCandidatePoolSize = 0;
+    }
+
+    this._config = config;
+
+    // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
+    // everything that is needed to describe a SDP m-line.
+    this.transceivers = [];
+
+    this._sdpSessionId = SDPUtils.generateSessionId();
+    this._sdpSessionVersion = 0;
+
+    this._dtlsRole = undefined; // role for a=setup to use in answers.
+  };
+
+  RTCPeerConnection.prototype._emitGatheringStateChange = function() {
+    var event = new Event('icegatheringstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onicegatheringstatechange === 'function') {
+      this.onicegatheringstatechange(event);
+    }
+  };
+
+  RTCPeerConnection.prototype.getConfiguration = function() {
+    return this._config;
+  };
+
+  RTCPeerConnection.prototype.getLocalStreams = function() {
+    return this.localStreams;
+  };
+
+  RTCPeerConnection.prototype.getRemoteStreams = function() {
+    return this.remoteStreams;
+  };
+
+  // internal helper to create a transceiver object.
+  // (whih is not yet the same as the WebRTC 1.0 transceiver)
+  RTCPeerConnection.prototype._createTransceiver = function(kind) {
+    var hasBundleTransport = this.transceivers.length > 0;
+    var transceiver = {
+      track: null,
+      iceGatherer: null,
+      iceTransport: null,
+      dtlsTransport: null,
+      localCapabilities: null,
+      remoteCapabilities: null,
+      rtpSender: null,
+      rtpReceiver: null,
+      kind: kind,
+      mid: null,
+      sendEncodingParameters: null,
+      recvEncodingParameters: null,
+      stream: null,
+      wantReceive: true
+    };
+    if (this.usingBundle && hasBundleTransport) {
+      transceiver.iceTransport = this.transceivers[0].iceTransport;
+      transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
+    } else {
+      var transports = this._createIceAndDtlsTransports();
+      transceiver.iceTransport = transports.iceTransport;
+      transceiver.dtlsTransport = transports.dtlsTransport;
+    }
+    this.transceivers.push(transceiver);
+    return transceiver;
+  };
+
+  RTCPeerConnection.prototype.addTrack = function(track, stream) {
+    var transceiver;
+    for (var i = 0; i < this.transceivers.length; i++) {
+      if (!this.transceivers[i].track &&
+          this.transceivers[i].kind === track.kind) {
+        transceiver = this.transceivers[i];
+      }
+    }
+    if (!transceiver) {
+      transceiver = this._createTransceiver(track.kind);
+    }
+
+    this._maybeFireNegotiationNeeded();
+
+    if (this.localStreams.indexOf(stream) === -1) {
+      this.localStreams.push(stream);
+    }
+
+    transceiver.track = track;
+    transceiver.stream = stream;
+    transceiver.rtpSender = new window.RTCRtpSender(track,
+        transceiver.dtlsTransport);
+    return transceiver.rtpSender;
+  };
+
+  RTCPeerConnection.prototype.addStream = function(stream) {
+    var self = this;
+    if (edgeVersion >= 15025) {
+      stream.getTracks().forEach(function(track) {
+        self.addTrack(track, stream);
+      });
+    } else {
+      // Clone is necessary for local demos mostly, attaching directly
+      // to two different senders does not work (build 10547).
+      // Fixed in 15025 (or earlier)
+      var clonedStream = stream.clone();
+      stream.getTracks().forEach(function(track, idx) {
+        var clonedTrack = clonedStream.getTracks()[idx];
+        track.addEventListener('enabled', function(event) {
+          clonedTrack.enabled = event.enabled;
+        });
+      });
+      clonedStream.getTracks().forEach(function(track) {
+        self.addTrack(track, clonedStream);
+      });
+    }
+  };
+
+  RTCPeerConnection.prototype.removeStream = function(stream) {
+    var idx = this.localStreams.indexOf(stream);
+    if (idx > -1) {
+      this.localStreams.splice(idx, 1);
+      this._maybeFireNegotiationNeeded();
+    }
+  };
+
+  RTCPeerConnection.prototype.getSenders = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpSender;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpSender;
+    });
+  };
+
+  RTCPeerConnection.prototype.getReceivers = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpReceiver;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpReceiver;
+    });
+  };
+
+
+  RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex,
+      usingBundle) {
+    var self = this;
+    if (usingBundle && sdpMLineIndex > 0) {
+      return this.transceivers[0].iceGatherer;
+    } else if (this._iceGatherers.length) {
+      return this._iceGatherers.shift();
+    }
+    var iceGatherer = new window.RTCIceGatherer({
+      iceServers: this._config.iceServers,
+      gatherPolicy: this._config.iceTransportPolicy
+    });
+    Object.defineProperty(iceGatherer, 'state',
+        {value: 'new', writable: true}
+    );
+
+    this.transceivers[sdpMLineIndex].candidates = [];
+    this.transceivers[sdpMLineIndex].bufferCandidates = function(event) {
+      var end = !event.candidate || Object.keys(event.candidate).length === 0;
+      // polyfill since RTCIceGatherer.state is not implemented in
+      // Edge 10547 yet.
+      iceGatherer.state = end ? 'completed' : 'gathering';
+      if (self.transceivers[sdpMLineIndex].candidates !== null) {
+        self.transceivers[sdpMLineIndex].candidates.push(event.candidate);
+      }
+    };
+    iceGatherer.addEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    return iceGatherer;
+  };
+
+  // start gathering from an RTCIceGatherer.
+  RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) {
+    var self = this;
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer.onlocalcandidate) {
+      return;
+    }
+    var candidates = this.transceivers[sdpMLineIndex].candidates;
+    this.transceivers[sdpMLineIndex].candidates = null;
+    iceGatherer.removeEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    iceGatherer.onlocalcandidate = function(evt) {
+      if (self.usingBundle && sdpMLineIndex > 0) {
+        // if we know that we use bundle we can drop candidates with
+        // ѕdpMLineIndex > 0. If we don't do this then our state gets
+        // confused since we dispose the extra ice gatherer.
+        return;
+      }
+      var event = new Event('icecandidate');
+      event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
+
+      var cand = evt.candidate;
+      // Edge emits an empty object for RTCIceCandidateComplete‥
+      var end = !cand || Object.keys(cand).length === 0;
+      if (end) {
+        // polyfill since RTCIceGatherer.state is not implemented in
+        // Edge 10547 yet.
+        if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') {
+          iceGatherer.state = 'completed';
+        }
+      } else {
+        if (iceGatherer.state === 'new') {
+          iceGatherer.state = 'gathering';
+        }
+        // RTCIceCandidate doesn't have a component, needs to be added
+        cand.component = 1;
+        event.candidate.candidate = SDPUtils.writeCandidate(cand);
+      }
+
+      // update local description.
+      var sections = SDPUtils.splitSections(self.localDescription.sdp);
+      if (!end) {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=' + event.candidate.candidate + '\r\n';
+      } else {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=end-of-candidates\r\n';
+      }
+      self.localDescription.sdp = sections.join('');
+      var complete = self.transceivers.every(function(transceiver) {
+        return transceiver.iceGatherer &&
+            transceiver.iceGatherer.state === 'completed';
+      });
+
+      if (self.iceGatheringState !== 'gathering') {
+        self.iceGatheringState = 'gathering';
+        self._emitGatheringStateChange();
+      }
+
+      // Emit candidate. Also emit null candidate when all gatherers are
+      // complete.
+      if (!end) {
+        self.dispatchEvent(event);
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(event);
+        }
+      }
+      if (complete) {
+        self.dispatchEvent(new Event('icecandidate'));
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(new Event('icecandidate'));
+        }
+        self.iceGatheringState = 'complete';
+        self._emitGatheringStateChange();
+      }
+    };
+
+    // emit already gathered candidates.
+    window.setTimeout(function() {
+      candidates.forEach(function(candidate) {
+        var e = new Event('RTCIceGatherEvent');
+        e.candidate = candidate;
+        iceGatherer.onlocalcandidate(e);
+      });
+    }, 0);
+  };
+
+  // Create ICE transport and DTLS transport.
+  RTCPeerConnection.prototype._createIceAndDtlsTransports = function() {
+    var self = this;
+    var iceTransport = new window.RTCIceTransport(null);
+    iceTransport.onicestatechange = function() {
+      self._updateConnectionState();
+    };
+
+    var dtlsTransport = new window.RTCDtlsTransport(iceTransport);
+    dtlsTransport.ondtlsstatechange = function() {
+      self._updateConnectionState();
+    };
+    dtlsTransport.onerror = function() {
+      // onerror does not set state to failed by itself.
+      Object.defineProperty(dtlsTransport, 'state',
+          {value: 'failed', writable: true});
+      self._updateConnectionState();
+    };
+
+    return {
+      iceTransport: iceTransport,
+      dtlsTransport: dtlsTransport
+    };
+  };
+
+  // Destroy ICE gatherer, ICE transport and DTLS transport.
+  // Without triggering the callbacks.
+  RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function(
+      sdpMLineIndex) {
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer) {
+      delete iceGatherer.onlocalcandidate;
+      delete this.transceivers[sdpMLineIndex].iceGatherer;
+    }
+    var iceTransport = this.transceivers[sdpMLineIndex].iceTransport;
+    if (iceTransport) {
+      delete iceTransport.onicestatechange;
+      delete this.transceivers[sdpMLineIndex].iceTransport;
+    }
+    var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport;
+    if (dtlsTransport) {
+      delete dtlsTransport.ondtlsstatechange;
+      delete dtlsTransport.onerror;
+      delete this.transceivers[sdpMLineIndex].dtlsTransport;
+    }
+  };
+
+  // Start the RTP Sender and Receiver for a transceiver.
+  RTCPeerConnection.prototype._transceive = function(transceiver,
+      send, recv) {
+    var params = getCommonCapabilities(transceiver.localCapabilities,
+        transceiver.remoteCapabilities);
+    if (send && transceiver.rtpSender) {
+      params.encodings = transceiver.sendEncodingParameters;
+      params.rtcp = {
+        cname: SDPUtils.localCName,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.recvEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpSender.send(params);
+    }
+    if (recv && transceiver.rtpReceiver && params.codecs.length > 0) {
+      // remove RTX field in Edge 14942
+      if (transceiver.kind === 'video'
+          && transceiver.recvEncodingParameters
+          && edgeVersion < 15019) {
+        transceiver.recvEncodingParameters.forEach(function(p) {
+          delete p.rtx;
+        });
+      }
+      params.encodings = transceiver.recvEncodingParameters;
+      params.rtcp = {
+        cname: transceiver.rtcpParameters.cname,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.sendEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpReceiver.receive(params);
+    }
+  };
+
+  RTCPeerConnection.prototype.setLocalDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setLocalDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set local ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var sections;
+    var sessionpart;
+    if (description.type === 'offer') {
+      // VERY limited support for SDP munging. Limited to:
+      // * changing the order of codecs
+      sections = SDPUtils.splitSections(description.sdp);
+      sessionpart = sections.shift();
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var caps = SDPUtils.parseRtpParameters(mediaSection);
+        self.transceivers[sdpMLineIndex].localCapabilities = caps;
+      });
+
+      this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+        self._gather(transceiver.mid, sdpMLineIndex);
+      });
+    } else if (description.type === 'answer') {
+      sections = SDPUtils.splitSections(self.remoteDescription.sdp);
+      sessionpart = sections.shift();
+      var isIceLite = SDPUtils.matchPrefix(sessionpart,
+          'a=ice-lite').length > 0;
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var transceiver = self.transceivers[sdpMLineIndex];
+        var iceGatherer = transceiver.iceGatherer;
+        var iceTransport = transceiver.iceTransport;
+        var dtlsTransport = transceiver.dtlsTransport;
+        var localCapabilities = transceiver.localCapabilities;
+        var remoteCapabilities = transceiver.remoteCapabilities;
+
+        // treat bundle-only as not-rejected.
+        var rejected = SDPUtils.isRejected(mediaSection) &&
+            !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+
+        if (!rejected && !transceiver.isDatachannel) {
+          var remoteIceParameters = SDPUtils.getIceParameters(
+              mediaSection, sessionpart);
+          var remoteDtlsParameters = SDPUtils.getDtlsParameters(
+              mediaSection, sessionpart);
+          if (isIceLite) {
+            remoteDtlsParameters.role = 'server';
+          }
+
+          if (!self.usingBundle || sdpMLineIndex === 0) {
+            self._gather(transceiver.mid, sdpMLineIndex);
+            if (iceTransport.state === 'new') {
+              iceTransport.start(iceGatherer, remoteIceParameters,
+                  isIceLite ? 'controlling' : 'controlled');
+            }
+            if (dtlsTransport.state === 'new') {
+              dtlsTransport.start(remoteDtlsParameters);
+            }
+          }
+
+          // Calculate intersection of capabilities.
+          var params = getCommonCapabilities(localCapabilities,
+              remoteCapabilities);
+
+          // Start the RTCRtpSender. The RTCRtpReceiver for this
+          // transceiver has already been started in setRemoteDescription.
+          self._transceive(transceiver,
+              params.codecs.length > 0,
+              false);
+        }
+      });
+    }
+
+    this.localDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-local-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+
+    // If a success callback was provided, emit ICE candidates after it
+    // has been executed. Otherwise, emit callback after the Promise is
+    // resolved.
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    return new Promise(function(resolve) {
+      if (cb) {
+        cb.apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.setRemoteDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setRemoteDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set remote ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var streams = {};
+    this.remoteStreams.forEach(function(stream) {
+      streams[stream.id] = stream;
+    });
+    var receiverList = [];
+    var sections = SDPUtils.splitSections(description.sdp);
+    var sessionpart = sections.shift();
+    var isIceLite = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-lite').length > 0;
+    var usingBundle = SDPUtils.matchPrefix(sessionpart,
+        'a=group:BUNDLE ').length > 0;
+    this.usingBundle = usingBundle;
+    var iceOptions = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-options:')[0];
+    if (iceOptions) {
+      this.canTrickleIceCandidates = iceOptions.substr(14).split(' ')
+          .indexOf('trickle') >= 0;
+    } else {
+      this.canTrickleIceCandidates = false;
+    }
+
+    sections.forEach(function(mediaSection, sdpMLineIndex) {
+      var lines = SDPUtils.splitLines(mediaSection);
+      var kind = SDPUtils.getKind(mediaSection);
+      // treat bundle-only as not-rejected.
+      var rejected = SDPUtils.isRejected(mediaSection) &&
+          !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+      var protocol = lines[0].substr(2).split(' ')[2];
+
+      var direction = SDPUtils.getDirection(mediaSection, sessionpart);
+      var remoteMsid = SDPUtils.parseMsid(mediaSection);
+
+      var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier();
+
+      // Reject datachannels which are not implemented yet.
+      if (kind === 'application' && protocol === 'DTLS/SCTP') {
+        self.transceivers[sdpMLineIndex] = {
+          mid: mid,
+          isDatachannel: true
+        };
+        return;
+      }
+
+      var transceiver;
+      var iceGatherer;
+      var iceTransport;
+      var dtlsTransport;
+      var rtpReceiver;
+      var sendEncodingParameters;
+      var recvEncodingParameters;
+      var localCapabilities;
+
+      var track;
+      // FIXME: ensure the mediaSection has rtcp-mux set.
+      var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
+      var remoteIceParameters;
+      var remoteDtlsParameters;
+      if (!rejected) {
+        remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters.role = 'client';
+      }
+      recvEncodingParameters =
+          SDPUtils.parseRtpEncodingParameters(mediaSection);
+
+      var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection);
+
+      var isComplete = SDPUtils.matchPrefix(mediaSection,
+          'a=end-of-candidates', sessionpart).length > 0;
+      var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
+          .map(function(cand) {
+            return SDPUtils.parseCandidate(cand);
+          })
+          .filter(function(cand) {
+            return cand.component === 1;
+          });
+
+      // Check if we can use BUNDLE and dispose transports.
+      if ((description.type === 'offer' || description.type === 'answer') &&
+          !rejected && usingBundle && sdpMLineIndex > 0 &&
+          self.transceivers[sdpMLineIndex]) {
+        self._disposeIceAndDtlsTransports(sdpMLineIndex);
+        self.transceivers[sdpMLineIndex].iceGatherer =
+            self.transceivers[0].iceGatherer;
+        self.transceivers[sdpMLineIndex].iceTransport =
+            self.transceivers[0].iceTransport;
+        self.transceivers[sdpMLineIndex].dtlsTransport =
+            self.transceivers[0].dtlsTransport;
+        if (self.transceivers[sdpMLineIndex].rtpSender) {
+          self.transceivers[sdpMLineIndex].rtpSender.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+        if (self.transceivers[sdpMLineIndex].rtpReceiver) {
+          self.transceivers[sdpMLineIndex].rtpReceiver.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+      }
+      if (description.type === 'offer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex] ||
+            self._createTransceiver(kind);
+        transceiver.mid = mid;
+
+        if (!transceiver.iceGatherer) {
+          transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+              usingBundle);
+        }
+
+        if (cands.length && transceiver.iceTransport.state === 'new') {
+          if (isComplete && (!usingBundle || sdpMLineIndex === 0)) {
+            transceiver.iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        localCapabilities = window.RTCRtpReceiver.getCapabilities(kind);
+
+        // filter RTX until additional stuff needed for RTX is implemented
+        // in adapter.js
+        if (edgeVersion < 15019) {
+          localCapabilities.codecs = localCapabilities.codecs.filter(
+              function(codec) {
+                return codec.name !== 'rtx';
+              });
+        }
+
+        sendEncodingParameters = transceiver.sendEncodingParameters || [{
+          ssrc: (2 * sdpMLineIndex + 2) * 1001
+        }];
+
+        var isNewTrack = false;
+        if (direction === 'sendrecv' || direction === 'sendonly') {
+          isNewTrack = !transceiver.rtpReceiver;
+          rtpReceiver = transceiver.rtpReceiver ||
+              new window.RTCRtpReceiver(transceiver.dtlsTransport, kind);
+
+          if (isNewTrack) {
+            var stream;
+            track = rtpReceiver.track;
+            // FIXME: does not work with Plan B.
+            if (remoteMsid) {
+              if (!streams[remoteMsid.stream]) {
+                streams[remoteMsid.stream] = new window.MediaStream();
+                Object.defineProperty(streams[remoteMsid.stream], 'id', {
+                  get: function() {
+                    return remoteMsid.stream;
+                  }
+                });
+              }
+              Object.defineProperty(track, 'id', {
+                get: function() {
+                  return remoteMsid.track;
+                }
+              });
+              stream = streams[remoteMsid.stream];
+            } else {
+              if (!streams.default) {
+                streams.default = new window.MediaStream();
+              }
+              stream = streams.default;
+            }
+            stream.addTrack(track);
+            receiverList.push([track, rtpReceiver, stream]);
+          }
+        }
+
+        transceiver.localCapabilities = localCapabilities;
+        transceiver.remoteCapabilities = remoteCapabilities;
+        transceiver.rtpReceiver = rtpReceiver;
+        transceiver.rtcpParameters = rtcpParameters;
+        transceiver.sendEncodingParameters = sendEncodingParameters;
+        transceiver.recvEncodingParameters = recvEncodingParameters;
+
+        // Start the RTCRtpReceiver now. The RTPSender is started in
+        // setLocalDescription.
+        self._transceive(self.transceivers[sdpMLineIndex],
+            false,
+            isNewTrack);
+      } else if (description.type === 'answer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex];
+        iceGatherer = transceiver.iceGatherer;
+        iceTransport = transceiver.iceTransport;
+        dtlsTransport = transceiver.dtlsTransport;
+        rtpReceiver = transceiver.rtpReceiver;
+        sendEncodingParameters = transceiver.sendEncodingParameters;
+        localCapabilities = transceiver.localCapabilities;
+
+        self.transceivers[sdpMLineIndex].recvEncodingParameters =
+            recvEncodingParameters;
+        self.transceivers[sdpMLineIndex].remoteCapabilities =
+            remoteCapabilities;
+        self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters;
+
+        if (cands.length && iceTransport.state === 'new') {
+          if ((isIceLite || isComplete) &&
+              (!usingBundle || sdpMLineIndex === 0)) {
+            iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        if (!usingBundle || sdpMLineIndex === 0) {
+          if (iceTransport.state === 'new') {
+            iceTransport.start(iceGatherer, remoteIceParameters,
+                'controlling');
+          }
+          if (dtlsTransport.state === 'new') {
+            dtlsTransport.start(remoteDtlsParameters);
+          }
+        }
+
+        self._transceive(transceiver,
+            direction === 'sendrecv' || direction === 'recvonly',
+            direction === 'sendrecv' || direction === 'sendonly');
+
+        if (rtpReceiver &&
+            (direction === 'sendrecv' || direction === 'sendonly')) {
+          track = rtpReceiver.track;
+          if (remoteMsid) {
+            if (!streams[remoteMsid.stream]) {
+              streams[remoteMsid.stream] = new window.MediaStream();
+            }
+            streams[remoteMsid.stream].addTrack(track);
+            receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]);
+          } else {
+            if (!streams.default) {
+              streams.default = new window.MediaStream();
+            }
+            streams.default.addTrack(track);
+            receiverList.push([track, rtpReceiver, streams.default]);
+          }
+        } else {
+          // FIXME: actually the receiver should be created later.
+          delete transceiver.rtpReceiver;
+        }
+      }
+    });
+
+    if (this._dtlsRole === undefined) {
+      this._dtlsRole = description.type === 'offer' ? 'active' : 'passive';
+    }
+
+    this.remoteDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-remote-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+    Object.keys(streams).forEach(function(sid) {
+      var stream = streams[sid];
+      if (stream.getTracks().length) {
+        if (self.remoteStreams.indexOf(stream) === -1) {
+          self.remoteStreams.push(stream);
+          var event = new Event('addstream');
+          event.stream = stream;
+          window.setTimeout(function() {
+            self.dispatchEvent(event);
+            if (typeof self.onaddstream === 'function') {
+              self.onaddstream(event);
+            }
+          });
+        }
+
+        receiverList.forEach(function(item) {
+          var track = item[0];
+          var receiver = item[1];
+          if (stream.id !== item[2].id) {
+            return;
+          }
+          var trackEvent = new Event('track');
+          trackEvent.track = track;
+          trackEvent.receiver = receiver;
+          trackEvent.transceiver = {receiver: receiver};
+          trackEvent.streams = [stream];
+          window.setTimeout(function() {
+            self.dispatchEvent(trackEvent);
+            if (typeof self.ontrack === 'function') {
+              self.ontrack(trackEvent);
+            }
+          });
+        });
+      }
+    });
+
+    // check whether addIceCandidate({}) was called within four seconds after
+    // setRemoteDescription.
+    window.setTimeout(function() {
+      if (!(self && self.transceivers)) {
+        return;
+      }
+      self.transceivers.forEach(function(transceiver) {
+        if (transceiver.iceTransport &&
+            transceiver.iceTransport.state === 'new' &&
+            transceiver.iceTransport.getRemoteCandidates().length > 0) {
+          console.warn('Timeout for addRemoteCandidate. Consider sending ' +
+              'an end-of-candidates notification');
+          transceiver.iceTransport.addRemoteCandidate({});
+        }
+      });
+    }, 4000);
+
+    return new Promise(function(resolve) {
+      if (args.length > 1 && typeof args[1] === 'function') {
+        args[1].apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.close = function() {
+    this.transceivers.forEach(function(transceiver) {
+      /* not yet
+      if (transceiver.iceGatherer) {
+        transceiver.iceGatherer.close();
+      }
+      */
+      if (transceiver.iceTransport) {
+        transceiver.iceTransport.stop();
+      }
+      if (transceiver.dtlsTransport) {
+        transceiver.dtlsTransport.stop();
+      }
+      if (transceiver.rtpSender) {
+        transceiver.rtpSender.stop();
+      }
+      if (transceiver.rtpReceiver) {
+        transceiver.rtpReceiver.stop();
+      }
+    });
+    // FIXME: clean up tracks, local streams, remote streams, etc
+    this._updateSignalingState('closed');
+  };
+
+  // Update the signaling state.
+  RTCPeerConnection.prototype._updateSignalingState = function(newState) {
+    this.signalingState = newState;
+    var event = new Event('signalingstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onsignalingstatechange === 'function') {
+      this.onsignalingstatechange(event);
+    }
+  };
+
+  // Determine whether to fire the negotiationneeded event.
+  RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() {
+    var self = this;
+    if (this.signalingState !== 'stable' || this.needNegotiation === true) {
+      return;
+    }
+    this.needNegotiation = true;
+    window.setTimeout(function() {
+      if (self.needNegotiation === false) {
+        return;
+      }
+      self.needNegotiation = false;
+      var event = new Event('negotiationneeded');
+      self.dispatchEvent(event);
+      if (typeof self.onnegotiationneeded === 'function') {
+        self.onnegotiationneeded(event);
+      }
+    }, 0);
+  };
+
+  // Update the connection state.
+  RTCPeerConnection.prototype._updateConnectionState = function() {
+    var newState;
+    var states = {
+      'new': 0,
+      closed: 0,
+      connecting: 0,
+      checking: 0,
+      connected: 0,
+      completed: 0,
+      disconnected: 0,
+      failed: 0
+    };
+    this.transceivers.forEach(function(transceiver) {
+      states[transceiver.iceTransport.state]++;
+      states[transceiver.dtlsTransport.state]++;
+    });
+    // ICETransport.completed and connected are the same for this purpose.
+    states.connected += states.completed;
+
+    newState = 'new';
+    if (states.failed > 0) {
+      newState = 'failed';
+    } else if (states.connecting > 0 || states.checking > 0) {
+      newState = 'connecting';
+    } else if (states.disconnected > 0) {
+      newState = 'disconnected';
+    } else if (states.new > 0) {
+      newState = 'new';
+    } else if (states.connected > 0 || states.completed > 0) {
+      newState = 'connected';
+    }
+
+    if (newState !== this.iceConnectionState) {
+      this.iceConnectionState = newState;
+      var event = new Event('iceconnectionstatechange');
+      this.dispatchEvent(event);
+      if (typeof this.oniceconnectionstatechange === 'function') {
+        this.oniceconnectionstatechange(event);
+      }
+    }
+  };
+
+  RTCPeerConnection.prototype.createOffer = function() {
+    var self = this;
+    var args = arguments;
+
+    var offerOptions;
+    if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+      offerOptions = arguments[0];
+    } else if (arguments.length === 3) {
+      offerOptions = arguments[2];
+    }
+
+    var numAudioTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'audio';
+    }).length;
+    var numVideoTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'video';
+    }).length;
+
+    // Determine number of audio and video tracks we need to send/recv.
+    if (offerOptions) {
+      // Reject Chrome legacy constraints.
+      if (offerOptions.mandatory || offerOptions.optional) {
+        throw new TypeError(
+            'Legacy mandatory/optional constraints not supported.');
+      }
+      if (offerOptions.offerToReceiveAudio !== undefined) {
+        if (offerOptions.offerToReceiveAudio === true) {
+          numAudioTracks = 1;
+        } else if (offerOptions.offerToReceiveAudio === false) {
+          numAudioTracks = 0;
+        } else {
+          numAudioTracks = offerOptions.offerToReceiveAudio;
+        }
+      }
+      if (offerOptions.offerToReceiveVideo !== undefined) {
+        if (offerOptions.offerToReceiveVideo === true) {
+          numVideoTracks = 1;
+        } else if (offerOptions.offerToReceiveVideo === false) {
+          numVideoTracks = 0;
+        } else {
+          numVideoTracks = offerOptions.offerToReceiveVideo;
+        }
+      }
+    }
+
+    this.transceivers.forEach(function(transceiver) {
+      if (transceiver.kind === 'audio') {
+        numAudioTracks--;
+        if (numAudioTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      } else if (transceiver.kind === 'video') {
+        numVideoTracks--;
+        if (numVideoTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      }
+    });
+
+    // Create M-lines for recvonly streams.
+    while (numAudioTracks > 0 || numVideoTracks > 0) {
+      if (numAudioTracks > 0) {
+        this._createTransceiver('audio');
+        numAudioTracks--;
+      }
+      if (numVideoTracks > 0) {
+        this._createTransceiver('video');
+        numVideoTracks--;
+      }
+    }
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      // For each track, create an ice gatherer, ice transport,
+      // dtls transport, potentially rtpsender and rtpreceiver.
+      var track = transceiver.track;
+      var kind = transceiver.kind;
+      var mid = SDPUtils.generateIdentifier();
+      transceiver.mid = mid;
+
+      if (!transceiver.iceGatherer) {
+        transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+            self.usingBundle);
+      }
+
+      var localCapabilities = window.RTCRtpSender.getCapabilities(kind);
+      // filter RTX until additional stuff needed for RTX is implemented
+      // in adapter.js
+      if (edgeVersion < 15019) {
+        localCapabilities.codecs = localCapabilities.codecs.filter(
+            function(codec) {
+              return codec.name !== 'rtx';
+            });
+      }
+      localCapabilities.codecs.forEach(function(codec) {
+        // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
+        // by adding level-asymmetry-allowed=1
+        if (codec.name === 'H264' &&
+            codec.parameters['level-asymmetry-allowed'] === undefined) {
+          codec.parameters['level-asymmetry-allowed'] = '1';
+        }
+      });
+
+      // generate an ssrc now, to be used later in rtpSender.send
+      var sendEncodingParameters = transceiver.sendEncodingParameters || [{
+        ssrc: (2 * sdpMLineIndex + 1) * 1001
+      }];
+      if (track) {
+        // add RTX
+        if (edgeVersion >= 15019 && kind === 'video' &&
+            !sendEncodingParameters[0].rtx) {
+          sendEncodingParameters[0].rtx = {
+            ssrc: sendEncodingParameters[0].ssrc + 1
+          };
+        }
+      }
+
+      if (transceiver.wantReceive) {
+        transceiver.rtpReceiver = new window.RTCRtpReceiver(
+            transceiver.dtlsTransport, kind);
+      }
+
+      transceiver.localCapabilities = localCapabilities;
+      transceiver.sendEncodingParameters = sendEncodingParameters;
+    });
+
+    // always offer BUNDLE and dispose on return if not supported.
+    if (this._config.bundlePolicy !== 'max-compat') {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    sdp += 'a=ice-options:trickle\r\n';
+
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      sdp += writeMediaSection(transceiver, transceiver.localCapabilities,
+          'offer', transceiver.stream, self._dtlsRole);
+      sdp += 'a=rtcp-rsize\r\n';
+
+      if (transceiver.iceGatherer && self.iceGatheringState !== 'new' &&
+          (sdpMLineIndex === 0 || !self.usingBundle)) {
+        transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) {
+          cand.component = 1;
+          sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n';
+        });
+
+        if (transceiver.iceGatherer.state === 'completed') {
+          sdp += 'a=end-of-candidates\r\n';
+        }
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'offer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.createAnswer = function() {
+    var self = this;
+    var args = arguments;
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    if (this.usingBundle) {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    var mediaSectionsInOffer = SDPUtils.splitSections(
+        this.remoteDescription.sdp).length - 1;
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      if (sdpMLineIndex + 1 > mediaSectionsInOffer) {
+        return;
+      }
+      if (transceiver.isDatachannel) {
+        sdp += 'm=application 0 DTLS/SCTP 5000\r\n' +
+            'c=IN IP4 0.0.0.0\r\n' +
+            'a=mid:' + transceiver.mid + '\r\n';
+        return;
+      }
+
+      // FIXME: look at direction.
+      if (transceiver.stream) {
+        var localTrack;
+        if (transceiver.kind === 'audio') {
+          localTrack = transceiver.stream.getAudioTracks()[0];
+        } else if (transceiver.kind === 'video') {
+          localTrack = transceiver.stream.getVideoTracks()[0];
+        }
+        if (localTrack) {
+          // add RTX
+          if (edgeVersion >= 15019 && transceiver.kind === 'video' &&
+              !transceiver.sendEncodingParameters[0].rtx) {
+            transceiver.sendEncodingParameters[0].rtx = {
+              ssrc: transceiver.sendEncodingParameters[0].ssrc + 1
+            };
+          }
+        }
+      }
+
+      // Calculate intersection of capabilities.
+      var commonCapabilities = getCommonCapabilities(
+          transceiver.localCapabilities,
+          transceiver.remoteCapabilities);
+
+      var hasRtx = commonCapabilities.codecs.filter(function(c) {
+        return c.name.toLowerCase() === 'rtx';
+      }).length;
+      if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
+        delete transceiver.sendEncodingParameters[0].rtx;
+      }
+
+      sdp += writeMediaSection(transceiver, commonCapabilities,
+          'answer', transceiver.stream, self._dtlsRole);
+      if (transceiver.rtcpParameters &&
+          transceiver.rtcpParameters.reducedSize) {
+        sdp += 'a=rtcp-rsize\r\n';
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'answer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
+    var err;
+    var sections;
+    if (!candidate || candidate.candidate === '') {
+      for (var j = 0; j < this.transceivers.length; j++) {
+        if (this.transceivers[j].isDatachannel) {
+          continue;
+        }
+        this.transceivers[j].iceTransport.addRemoteCandidate({});
+        sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+        sections[j + 1] += 'a=end-of-candidates\r\n';
+        this.remoteDescription.sdp = sections.join('');
+        if (this.usingBundle) {
+          break;
+        }
+      }
+    } else if (!(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) {
+      throw new TypeError('sdpMLineIndex or sdpMid required');
+    } else if (!this.remoteDescription) {
+      err = new Error('Can not add ICE candidate without ' +
+          'a remote description');
+      err.name = 'InvalidStateError';
+    } else {
+      var sdpMLineIndex = candidate.sdpMLineIndex;
+      if (candidate.sdpMid) {
+        for (var i = 0; i < this.transceivers.length; i++) {
+          if (this.transceivers[i].mid === candidate.sdpMid) {
+            sdpMLineIndex = i;
+            break;
+          }
+        }
+      }
+      var transceiver = this.transceivers[sdpMLineIndex];
+      if (transceiver) {
+        if (transceiver.isDatachannel) {
+          return Promise.resolve();
+        }
+        var cand = Object.keys(candidate.candidate).length > 0 ?
+            SDPUtils.parseCandidate(candidate.candidate) : {};
+        // Ignore Chrome's invalid candidates since Edge does not like them.
+        if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
+          return Promise.resolve();
+        }
+        // Ignore RTCP candidates, we assume RTCP-MUX.
+        if (cand.component && cand.component !== 1) {
+          return Promise.resolve();
+        }
+        // when using bundle, avoid adding candidates to the wrong
+        // ice transport. And avoid adding candidates added in the SDP.
+        if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 &&
+            transceiver.iceTransport !== this.transceivers[0].iceTransport)) {
+          if (!maybeAddCandidate(transceiver.iceTransport, cand)) {
+            err = new Error('Can not add ICE candidate');
+            err.name = 'OperationError';
+          }
+        }
+
+        if (!err) {
+          // update the remoteDescription.
+          var candidateString = candidate.candidate.trim();
+          if (candidateString.indexOf('a=') === 0) {
+            candidateString = candidateString.substr(2);
+          }
+          sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+          sections[sdpMLineIndex + 1] += 'a=' +
+              (cand.type ? candidateString : 'end-of-candidates')
+              + '\r\n';
+          this.remoteDescription.sdp = sections.join('');
+        }
+      } else {
+        err = new Error('Can not add ICE candidate');
+        err.name = 'OperationError';
+      }
+    }
+    var args = arguments;
+    return new Promise(function(resolve, reject) {
+      if (err) {
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [err]);
+        }
+        reject(err);
+      } else {
+        if (args.length > 1 && typeof args[1] === 'function') {
+          args[1].apply(null);
+        }
+        resolve();
+      }
+    });
+  };
+
+  RTCPeerConnection.prototype.getStats = function() {
+    var promises = [];
+    this.transceivers.forEach(function(transceiver) {
+      ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
+          'dtlsTransport'].forEach(function(method) {
+            if (transceiver[method]) {
+              promises.push(transceiver[method].getStats());
+            }
+          });
+    });
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    var fixStatsType = function(stat) {
+      return {
+        inboundrtp: 'inbound-rtp',
+        outboundrtp: 'outbound-rtp',
+        candidatepair: 'candidate-pair',
+        localcandidate: 'local-candidate',
+        remotecandidate: 'remote-candidate'
+      }[stat.type] || stat.type;
+    };
+    return new Promise(function(resolve) {
+      // shim getStats with maplike support
+      var results = new Map();
+      Promise.all(promises).then(function(res) {
+        res.forEach(function(result) {
+          Object.keys(result).forEach(function(id) {
+            result[id].type = fixStatsType(result[id]);
+            results.set(id, result[id]);
+          });
+        });
+        if (cb) {
+          cb.apply(null, results);
+        }
+        resolve(results);
+      });
+    });
+  };
+  return RTCPeerConnection;
+};
+
+},{"sdp":2}],2:[function(require,module,exports){
+ /* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+  return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+  return blob.trim().split('\n').map(function(line) {
+    return line.trim();
+  });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+  var parts = blob.split('\nm=');
+  return parts.map(function(part, index) {
+    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+  });
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+  return SDPUtils.splitLines(blob).filter(function(line) {
+    return line.indexOf(prefix) === 0;
+  });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+  var parts;
+  // Parse both variants.
+  if (line.indexOf('a=candidate:') === 0) {
+    parts = line.substring(12).split(' ');
+  } else {
+    parts = line.substring(10).split(' ');
+  }
+
+  var candidate = {
+    foundation: parts[0],
+    component: parseInt(parts[1], 10),
+    protocol: parts[2].toLowerCase(),
+    priority: parseInt(parts[3], 10),
+    ip: parts[4],
+    port: parseInt(parts[5], 10),
+    // skip parts[6] == 'typ'
+    type: parts[7]
+  };
+
+  for (var i = 8; i < parts.length; i += 2) {
+    switch (parts[i]) {
+      case 'raddr':
+        candidate.relatedAddress = parts[i + 1];
+        break;
+      case 'rport':
+        candidate.relatedPort = parseInt(parts[i + 1], 10);
+        break;
+      case 'tcptype':
+        candidate.tcpType = parts[i + 1];
+        break;
+      case 'ufrag':
+        candidate.ufrag = parts[i + 1]; // for backward compability.
+        candidate.usernameFragment = parts[i + 1];
+        break;
+      default: // extension handling, in particular ufrag
+        candidate[parts[i]] = parts[i + 1];
+        break;
+    }
+  }
+  return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+  var sdp = [];
+  sdp.push(candidate.foundation);
+  sdp.push(candidate.component);
+  sdp.push(candidate.protocol.toUpperCase());
+  sdp.push(candidate.priority);
+  sdp.push(candidate.ip);
+  sdp.push(candidate.port);
+
+  var type = candidate.type;
+  sdp.push('typ');
+  sdp.push(type);
+  if (type !== 'host' && candidate.relatedAddress &&
+      candidate.relatedPort) {
+    sdp.push('raddr');
+    sdp.push(candidate.relatedAddress); // was: relAddr
+    sdp.push('rport');
+    sdp.push(candidate.relatedPort); // was: relPort
+  }
+  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+    sdp.push('tcptype');
+    sdp.push(candidate.tcpType);
+  }
+  if (candidate.ufrag) {
+    sdp.push('ufrag');
+    sdp.push(candidate.ufrag);
+  }
+  return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+  return line.substr(14).split(' ');
+}
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+  var parts = line.substr(9).split(' ');
+  var parsed = {
+    payloadType: parseInt(parts.shift(), 10) // was: id
+  };
+
+  parts = parts[0].split('/');
+
+  parsed.name = parts[0];
+  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+  // was: channels
+  parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+  return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+      (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+  var parts = line.substr(9).split(' ');
+  return {
+    id: parseInt(parts[0], 10),
+    direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+    uri: parts[1]
+  };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+      (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+          ? '/' + headerExtension.direction
+          : '') +
+      ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+  var parsed = {};
+  var kv;
+  var parts = line.substr(line.indexOf(' ') + 1).split(';');
+  for (var j = 0; j < parts.length; j++) {
+    kv = parts[j].trim().split('=');
+    parsed[kv[0].trim()] = kv[1];
+  }
+  return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+  var line = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.parameters && Object.keys(codec.parameters).length) {
+    var params = [];
+    Object.keys(codec.parameters).forEach(function(param) {
+      params.push(param + '=' + codec.parameters[param]);
+    });
+    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+  }
+  return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+  return {
+    type: parts.shift(),
+    parameter: parts.join(' ')
+  };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+  var lines = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+    // FIXME: special handling for trr-int?
+    codec.rtcpFeedback.forEach(function(fb) {
+      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+      (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+          '\r\n';
+    });
+  }
+  return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+  var sp = line.indexOf(' ');
+  var parts = {
+    ssrc: parseInt(line.substr(7, sp - 7), 10)
+  };
+  var colon = line.indexOf(':', sp);
+  if (colon > -1) {
+    parts.attribute = line.substr(sp + 1, colon - sp - 1);
+    parts.value = line.substr(colon + 1);
+  } else {
+    parts.attribute = line.substr(sp + 1);
+  }
+  return parts;
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+  var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+  if (mid) {
+    return mid.substr(6);
+  }
+}
+
+SDPUtils.parseFingerprint = function(line) {
+  var parts = line.substr(14).split(' ');
+  return {
+    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+    value: parts[1]
+  };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+      'a=fingerprint:');
+  // Note: a=setup line is ignored since we use the 'auto' role.
+  // Note2: 'algorithm' is not case sensitive except in Edge.
+  return {
+    role: 'auto',
+    fingerprints: lines.map(SDPUtils.parseFingerprint)
+  };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+  var sdp = 'a=setup:' + setupType + '\r\n';
+  params.fingerprints.forEach(function(fp) {
+    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+  });
+  return sdp;
+};
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var iceParameters = {
+    usernameFragment: lines.filter(function(line) {
+      return line.indexOf('a=ice-ufrag:') === 0;
+    })[0].substr(12),
+    password: lines.filter(function(line) {
+      return line.indexOf('a=ice-pwd:') === 0;
+    })[0].substr(10)
+  };
+  return iceParameters;
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+      'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+  var description = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: [],
+    rtcp: []
+  };
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+    var pt = mline[i];
+    var rtpmapline = SDPUtils.matchPrefix(
+        mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+    if (rtpmapline) {
+      var codec = SDPUtils.parseRtpMap(rtpmapline);
+      var fmtps = SDPUtils.matchPrefix(
+          mediaSection, 'a=fmtp:' + pt + ' ');
+      // Only the first a=fmtp:<pt> is considered.
+      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+      codec.rtcpFeedback = SDPUtils.matchPrefix(
+          mediaSection, 'a=rtcp-fb:' + pt + ' ')
+        .map(SDPUtils.parseRtcpFb);
+      description.codecs.push(codec);
+      // parse FEC mechanisms from rtpmap lines.
+      switch (codec.name.toUpperCase()) {
+        case 'RED':
+        case 'ULPFEC':
+          description.fecMechanisms.push(codec.name.toUpperCase());
+          break;
+        default: // only RED and ULPFEC are recognized as FEC mechanisms.
+          break;
+      }
+    }
+  }
+  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+    description.headerExtensions.push(SDPUtils.parseExtmap(line));
+  });
+  // FIXME: parse rtcp.
+  return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+  var sdp = '';
+
+  // Build the mline.
+  sdp += 'm=' + kind + ' ';
+  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+  sdp += ' UDP/TLS/RTP/SAVPF ';
+  sdp += caps.codecs.map(function(codec) {
+    if (codec.preferredPayloadType !== undefined) {
+      return codec.preferredPayloadType;
+    }
+    return codec.payloadType;
+  }).join(' ') + '\r\n';
+
+  sdp += 'c=IN IP4 0.0.0.0\r\n';
+  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+  caps.codecs.forEach(function(codec) {
+    sdp += SDPUtils.writeRtpMap(codec);
+    sdp += SDPUtils.writeFmtp(codec);
+    sdp += SDPUtils.writeRtcpFb(codec);
+  });
+  var maxptime = 0;
+  caps.codecs.forEach(function(codec) {
+    if (codec.maxptime > maxptime) {
+      maxptime = codec.maxptime;
+    }
+  });
+  if (maxptime > 0) {
+    sdp += 'a=maxptime:' + maxptime + '\r\n';
+  }
+  sdp += 'a=rtcp-mux\r\n';
+
+  caps.headerExtensions.forEach(function(extension) {
+    sdp += SDPUtils.writeExtmap(extension);
+  });
+  // FIXME: write fecMechanisms.
+  return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+  var encodingParameters = [];
+  var description = SDPUtils.parseRtpParameters(mediaSection);
+  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+  // filter a=ssrc:... cname:, ignore PlanB-msid
+  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'cname';
+  });
+  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+  var secondarySsrc;
+
+  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+  .map(function(line) {
+    var parts = line.split(' ');
+    parts.shift();
+    return parts.map(function(part) {
+      return parseInt(part, 10);
+    });
+  });
+  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+    secondarySsrc = flows[0][1];
+  }
+
+  description.codecs.forEach(function(codec) {
+    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+      var encParam = {
+        ssrc: primarySsrc,
+        codecPayloadType: parseInt(codec.parameters.apt, 10),
+        rtx: {
+          ssrc: secondarySsrc
+        }
+      };
+      encodingParameters.push(encParam);
+      if (hasRed) {
+        encParam = JSON.parse(JSON.stringify(encParam));
+        encParam.fec = {
+          ssrc: secondarySsrc,
+          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+        };
+        encodingParameters.push(encParam);
+      }
+    }
+  });
+  if (encodingParameters.length === 0 && primarySsrc) {
+    encodingParameters.push({
+      ssrc: primarySsrc
+    });
+  }
+
+  // we support both b=AS and b=TIAS but interpret AS as TIAS.
+  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+  if (bandwidth.length) {
+    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(7), 10);
+    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+      // use formula from JSEP to convert b=AS to TIAS value.
+      bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+          - (50 * 40 * 8);
+    } else {
+      bandwidth = undefined;
+    }
+    encodingParameters.forEach(function(params) {
+      params.maxBitrate = bandwidth;
+    });
+  }
+  return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+  var rtcpParameters = {};
+
+  var cname;
+  // Gets the first SSRC. Note that with RTX there might be multiple
+  // SSRCs.
+  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+      .map(function(line) {
+        return SDPUtils.parseSsrcMedia(line);
+      })
+      .filter(function(obj) {
+        return obj.attribute === 'cname';
+      })[0];
+  if (remoteSsrc) {
+    rtcpParameters.cname = remoteSsrc.value;
+    rtcpParameters.ssrc = remoteSsrc.ssrc;
+  }
+
+  // Edge uses the compound attribute instead of reducedSize
+  // compound is !reducedSize
+  var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+  rtcpParameters.reducedSize = rsize.length > 0;
+  rtcpParameters.compound = rsize.length === 0;
+
+  // parses the rtcp-mux attrіbute.
+  // Note that Edge does not support unmuxed RTCP.
+  var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+  rtcpParameters.mux = mux.length > 0;
+
+  return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+  var parts;
+  var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+  if (spec.length === 1) {
+    parts = spec[0].substr(7).split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+  var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'msid';
+  });
+  if (planB.length > 0) {
+    parts = planB[0].value.split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+  return Math.random().toString().substr(2, 21);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) {
+  var sessionId;
+  var version = sessVer !== undefined ? sessVer : 2;
+  if (sessId) {
+    sessionId = sessId;
+  } else {
+    sessionId = SDPUtils.generateSessionId();
+  }
+  // FIXME: sess-id should be an NTP timestamp.
+  return 'v=0\r\n' +
+      'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' +
+      's=-\r\n' +
+      't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  var lines = SDPUtils.splitLines(mediaSection);
+  for (var i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substr(2);
+      default:
+        // FIXME: What should happen here?
+    }
+  }
+  if (sessionpart) {
+    return SDPUtils.getDirection(sessionpart);
+  }
+  return 'sendrecv';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+  return mediaSection.split(' ', 2)[1] === '0';
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return {
+    kind: mline[0].substr(2),
+    port: parseInt(mline[1], 10),
+    protocol: mline[2],
+    fmt: mline.slice(3).join(' ')
+  };
+};
+
+// Expose public methods.
+if (typeof module === 'object') {
+  module.exports = SDPUtils;
+}
+
+},{}],3:[function(require,module,exports){
+(function (global){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var adapterFactory = require('./adapter_factory.js');
+module.exports = adapterFactory({window: global.window});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./adapter_factory.js":4}],4:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var utils = require('./utils');
+// Shimming starts here.
+module.exports = function(dependencies, opts) {
+  var window = dependencies && dependencies.window;
+
+  var options = {
+    shimChrome: true,
+    shimFirefox: true,
+    shimEdge: true,
+    shimSafari: true,
+  };
+
+  for (var key in opts) {
+    if (hasOwnProperty.call(opts, key)) {
+      options[key] = opts[key];
+    }
+  }
+
+  // Utils.
+  var logging = utils.log;
+  var browserDetails = utils.detectBrowser(window);
+
+  // Export to the adapter global object visible in the browser.
+  var adapter = {
+    browserDetails: browserDetails,
+    extractVersion: utils.extractVersion,
+    disableLog: utils.disableLog,
+    disableWarnings: utils.disableWarnings
+  };
+
+  // Uncomment the line below if you want logging to occur, including logging
+  // for the switch statement below. Can also be turned on in the browser via
+  // adapter.disableLog(false), but then logging from the switch statement below
+  // will not appear.
+  // require('./utils').disableLog(false);
+
+  // Browser shims.
+  var chromeShim = require('./chrome/chrome_shim') || null;
+  var edgeShim = require('./edge/edge_shim') || null;
+  var firefoxShim = require('./firefox/firefox_shim') || null;
+  var safariShim = require('./safari/safari_shim') || null;
+  var commonShim = require('./common_shim') || null;
+
+  // Shim browser if found.
+  switch (browserDetails.browser) {
+    case 'chrome':
+      if (!chromeShim || !chromeShim.shimPeerConnection ||
+          !options.shimChrome) {
+        logging('Chrome shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming chrome.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = chromeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      chromeShim.shimGetUserMedia(window);
+      chromeShim.shimMediaStream(window);
+      chromeShim.shimSourceObject(window);
+      chromeShim.shimPeerConnection(window);
+      chromeShim.shimOnTrack(window);
+      chromeShim.shimAddTrackRemoveTrack(window);
+      chromeShim.shimGetSendersWithDtmf(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'firefox':
+      if (!firefoxShim || !firefoxShim.shimPeerConnection ||
+          !options.shimFirefox) {
+        logging('Firefox shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming firefox.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = firefoxShim;
+      commonShim.shimCreateObjectURL(window);
+
+      firefoxShim.shimGetUserMedia(window);
+      firefoxShim.shimSourceObject(window);
+      firefoxShim.shimPeerConnection(window);
+      firefoxShim.shimOnTrack(window);
+      firefoxShim.shimRemoveStream(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'edge':
+      if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
+        logging('MS edge shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming edge.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = edgeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      edgeShim.shimGetUserMedia(window);
+      edgeShim.shimPeerConnection(window);
+      edgeShim.shimReplaceTrack(window);
+
+      // the edge shim implements the full RTCIceCandidate object.
+      break;
+    case 'safari':
+      if (!safariShim || !options.shimSafari) {
+        logging('Safari shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming safari.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = safariShim;
+      commonShim.shimCreateObjectURL(window);
+
+      safariShim.shimRTCIceServerUrls(window);
+      safariShim.shimCallbacksAPI(window);
+      safariShim.shimLocalStreamsAPI(window);
+      safariShim.shimRemoteStreamsAPI(window);
+      safariShim.shimTrackEventTransceiver(window);
+      safariShim.shimGetUserMedia(window);
+      safariShim.shimCreateOfferLegacy(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    default:
+      logging('Unsupported browser!');
+      break;
+  }
+
+  return adapter;
+};
+
+},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":10,"./safari/safari_shim":12,"./utils":13}],5:[function(require,module,exports){
+
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+var chromeShim = {
+  shimMediaStream: function(window) {
+    window.MediaStream = window.MediaStream || window.webkitMediaStream;
+  },
+
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+          }
+          this.addEventListener('track', this._ontrack = f);
+        }
+      });
+      var origSetRemoteDescription =
+          window.RTCPeerConnection.prototype.setRemoteDescription;
+      window.RTCPeerConnection.prototype.setRemoteDescription = function() {
+        var pc = this;
+        if (!pc._ontrackpoly) {
+          pc._ontrackpoly = function(e) {
+            // onaddstream does not fire when a track is added to an existing
+            // stream. But stream.onaddtrack is implemented so we use that.
+            e.stream.addEventListener('addtrack', function(te) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === te.track.id;
+                });
+              } else {
+                receiver = {track: te.track};
+              }
+
+              var event = new Event('track');
+              event.track = te.track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+            e.stream.getTracks().forEach(function(track) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === track.id;
+                });
+              } else {
+                receiver = {track: track};
+              }
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+          };
+          pc.addEventListener('addstream', pc._ontrackpoly);
+        }
+        return origSetRemoteDescription.apply(pc, arguments);
+      };
+    }
+  },
+
+  shimGetSendersWithDtmf: function(window) {
+    // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        !('getSenders' in window.RTCPeerConnection.prototype) &&
+        'createDTMFSender' in window.RTCPeerConnection.prototype) {
+      var shimSenderWithDtmf = function(pc, track) {
+        return {
+          track: track,
+          get dtmf() {
+            if (this._dtmf === undefined) {
+              if (track.kind === 'audio') {
+                this._dtmf = pc.createDTMFSender(track);
+              } else {
+                this._dtmf = null;
+              }
+            }
+            return this._dtmf;
+          },
+          _pc: pc
+        };
+      };
+
+      // augment addTrack when getSenders is not available.
+      if (!window.RTCPeerConnection.prototype.getSenders) {
+        window.RTCPeerConnection.prototype.getSenders = function() {
+          this._senders = this._senders || [];
+          return this._senders.slice(); // return a copy of the internal state.
+        };
+        var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
+        window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+          var pc = this;
+          var sender = origAddTrack.apply(pc, arguments);
+          if (!sender) {
+            sender = shimSenderWithDtmf(pc, track);
+            pc._senders.push(sender);
+          }
+          return sender;
+        };
+
+        var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
+        window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+          var pc = this;
+          origRemoveTrack.apply(pc, arguments);
+          var idx = pc._senders.indexOf(sender);
+          if (idx !== -1) {
+            pc._senders.splice(idx, 1);
+          }
+        };
+      }
+      var origAddStream = window.RTCPeerConnection.prototype.addStream;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origAddStream.apply(pc, [stream]);
+        stream.getTracks().forEach(function(track) {
+          pc._senders.push(shimSenderWithDtmf(pc, track));
+        });
+      };
+
+      var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origRemoveStream.apply(pc, [stream]);
+
+        stream.getTracks().forEach(function(track) {
+          var sender = pc._senders.find(function(s) {
+            return s.track === track;
+          });
+          if (sender) {
+            pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender
+          }
+        });
+      };
+    } else if (typeof window === 'object' && window.RTCPeerConnection &&
+               'getSenders' in window.RTCPeerConnection.prototype &&
+               'createDTMFSender' in window.RTCPeerConnection.prototype &&
+               window.RTCRtpSender &&
+               !('dtmf' in window.RTCRtpSender.prototype)) {
+      var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
+      window.RTCPeerConnection.prototype.getSenders = function() {
+        var pc = this;
+        var senders = origGetSenders.apply(pc, []);
+        senders.forEach(function(sender) {
+          sender._pc = pc;
+        });
+        return senders;
+      };
+
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = this._pc.createDTMFSender(this.track);
+            } else {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    var URL = window && window.URL;
+
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this._srcObject;
+          },
+          set: function(stream) {
+            var self = this;
+            // Use _srcObject as a private property for this shim
+            this._srcObject = stream;
+            if (this.src) {
+              URL.revokeObjectURL(this.src);
+            }
+
+            if (!stream) {
+              this.src = '';
+              return undefined;
+            }
+            this.src = URL.createObjectURL(stream);
+            // We need to recreate the blob url when a track is added or
+            // removed. Doing it manually since we want to avoid a recursion.
+            stream.addEventListener('addtrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+            stream.addEventListener('removetrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+          }
+        });
+      }
+    }
+  },
+
+  shimAddTrackRemoveTrack: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+    // shim addTrack and removeTrack.
+    if (window.RTCPeerConnection.prototype.addTrack &&
+        browserDetails.version >= 64) {
+      return;
+    }
+
+    // also shim pc.getLocalStreams when addTrack is shimmed
+    // to return the original streams.
+    var origGetLocalStreams = window.RTCPeerConnection.prototype
+        .getLocalStreams;
+    window.RTCPeerConnection.prototype.getLocalStreams = function() {
+      var self = this;
+      var nativeStreams = origGetLocalStreams.apply(this);
+      self._reverseStreams = self._reverseStreams || {};
+      return nativeStreams.map(function(stream) {
+        return self._reverseStreams[stream.id];
+      });
+    };
+
+    var origAddStream = window.RTCPeerConnection.prototype.addStream;
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      stream.getTracks().forEach(function(track) {
+        var alreadyExists = pc.getSenders().find(function(s) {
+          return s.track === track;
+        });
+        if (alreadyExists) {
+          throw new DOMException('Track already exists.',
+              'InvalidAccessError');
+        }
+      });
+      // Add identity mapping for consistency with addTrack.
+      // Unless this is being used with a stream from addTrack.
+      if (!pc._reverseStreams[stream.id]) {
+        var newStream = new window.MediaStream(stream.getTracks());
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        stream = newStream;
+      }
+      origAddStream.apply(pc, [stream]);
+    };
+
+    var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+      delete pc._reverseStreams[(pc._streams[stream.id] ?
+          pc._streams[stream.id].id : stream.id)];
+      delete pc._streams[stream.id];
+    };
+
+    window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      var streams = [].slice.call(arguments, 1);
+      if (streams.length !== 1 ||
+          !streams[0].getTracks().find(function(t) {
+            return t === track;
+          })) {
+        // this is not fully correct but all we can manage without
+        // [[associated MediaStreams]] internal slot.
+        throw new DOMException(
+          'The adapter.js addTrack polyfill only supports a single ' +
+          ' stream which is associated with the specified track.',
+          'NotSupportedError');
+      }
+
+      var alreadyExists = pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+      if (alreadyExists) {
+        throw new DOMException('Track already exists.',
+            'InvalidAccessError');
+      }
+
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+      var oldStream = pc._streams[stream.id];
+      if (oldStream) {
+        // this is using odd Chrome behaviour, use with caution:
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
+        // Note: we rely on the high-level addTrack/dtmf shim to
+        // create the sender with a dtmf sender.
+        oldStream.addTrack(track);
+
+        // Trigger ONN async.
+        Promise.resolve().then(function() {
+          pc.dispatchEvent(new Event('negotiationneeded'));
+        });
+      } else {
+        var newStream = new window.MediaStream([track]);
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        pc.addStream(newStream);
+      }
+      return pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+    };
+
+    // replace the internal stream id with the external one and
+    // vice versa.
+    function replaceInternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(internalStream.id, 'g'),
+            externalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    function replaceExternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(externalStream.id, 'g'),
+            internalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    ['createOffer', 'createAnswer'].forEach(function(method) {
+      var nativeMethod = window.RTCPeerConnection.prototype[method];
+      window.RTCPeerConnection.prototype[method] = function() {
+        var pc = this;
+        var args = arguments;
+        var isLegacyCall = arguments.length &&
+            typeof arguments[0] === 'function';
+        if (isLegacyCall) {
+          return nativeMethod.apply(pc, [
+            function(description) {
+              var desc = replaceInternalStreamId(pc, description);
+              args[0].apply(null, [desc]);
+            },
+            function(err) {
+              if (args[1]) {
+                args[1].apply(null, err);
+              }
+            }, arguments[2]
+          ]);
+        }
+        return nativeMethod.apply(pc, arguments)
+        .then(function(description) {
+          return replaceInternalStreamId(pc, description);
+        });
+      };
+    });
+
+    var origSetLocalDescription =
+        window.RTCPeerConnection.prototype.setLocalDescription;
+    window.RTCPeerConnection.prototype.setLocalDescription = function() {
+      var pc = this;
+      if (!arguments.length || !arguments[0].type) {
+        return origSetLocalDescription.apply(pc, arguments);
+      }
+      arguments[0] = replaceExternalStreamId(pc, arguments[0]);
+      return origSetLocalDescription.apply(pc, arguments);
+    };
+
+    // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier
+
+    var origLocalDescription = Object.getOwnPropertyDescriptor(
+        window.RTCPeerConnection.prototype, 'localDescription');
+    Object.defineProperty(window.RTCPeerConnection.prototype,
+        'localDescription', {
+          get: function() {
+            var pc = this;
+            var description = origLocalDescription.get.apply(this);
+            if (description.type === '') {
+              return description;
+            }
+            return replaceInternalStreamId(pc, description);
+          }
+        });
+
+    window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      // We can not yet check for sender instanceof RTCRtpSender
+      // since we shim RTPSender. So we check if sender._pc is set.
+      if (!sender._pc) {
+        throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' +
+            'does not implement interface RTCRtpSender.', 'TypeError');
+      }
+      var isLocal = sender._pc === pc;
+      if (!isLocal) {
+        throw new DOMException('Sender was not created by this connection.',
+            'InvalidAccessError');
+      }
+
+      // Search for the native stream the senders track belongs to.
+      pc._streams = pc._streams || {};
+      var stream;
+      Object.keys(pc._streams).forEach(function(streamid) {
+        var hasTrack = pc._streams[streamid].getTracks().find(function(track) {
+          return sender.track === track;
+        });
+        if (hasTrack) {
+          stream = pc._streams[streamid];
+        }
+      });
+
+      if (stream) {
+        if (stream.getTracks().length === 1) {
+          // if this is the last track of the stream, remove the stream. This
+          // takes care of any shimmed _senders.
+          pc.removeStream(pc._reverseStreams[stream.id]);
+        } else {
+          // relying on the same odd chrome behaviour as above.
+          stream.removeTrack(sender.track);
+        }
+        pc.dispatchEvent(new Event('negotiationneeded'));
+      }
+    };
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        // Translate iceTransportPolicy to iceTransports,
+        // see https://code.google.com/p/webrtc/issues/detail?id=4869
+        // this was fixed in M56 along with unprefixing RTCPeerConnection.
+        logging('PeerConnection');
+        if (pcConfig && pcConfig.iceTransportPolicy) {
+          pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+        }
+
+        return new window.webkitRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.webkitRTCPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      if (window.webkitRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.webkitRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+    } else {
+      // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+      var OrigPeerConnection = window.RTCPeerConnection;
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (pcConfig && pcConfig.iceServers) {
+          var newIceServers = [];
+          for (var i = 0; i < pcConfig.iceServers.length; i++) {
+            var server = pcConfig.iceServers[i];
+            if (!server.hasOwnProperty('urls') &&
+                server.hasOwnProperty('url')) {
+              utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+              server = JSON.parse(JSON.stringify(server));
+              server.urls = server.url;
+              newIceServers.push(server);
+            } else {
+              newIceServers.push(pcConfig.iceServers[i]);
+            }
+          }
+          pcConfig.iceServers = newIceServers;
+        }
+        return new OrigPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+
+    var origGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(selector,
+        successCallback, errorCallback) {
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats.apply(this, arguments);
+      }
+
+      // When spec-style getStats is supported, return those when called with
+      // either no arguments or the selector argument is null.
+      if (origGetStats.length === 0 && (arguments.length === 0 ||
+          typeof arguments[0] !== 'function')) {
+        return origGetStats.apply(this, []);
+      }
+
+      var fixChromeStats_ = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: {
+              localcandidate: 'local-candidate',
+              remotecandidate: 'remote-candidate'
+            }[report.type] || report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      // shim getStats with maplike support
+      var makeMapStats = function(stats) {
+        return new Map(Object.keys(stats).map(function(key) {
+          return [key, stats[key]];
+        }));
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper_ = function(response) {
+          args[1](makeMapStats(fixChromeStats_(response)));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper_,
+          arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        origGetStats.apply(self, [
+          function(response) {
+            resolve(makeMapStats(fixChromeStats_(response)));
+          }, reject]);
+      }).then(successCallback, errorCallback);
+    };
+
+    // add promise support -- natively available in Chrome 51
+    if (browserDetails.version < 51) {
+      ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+          .forEach(function(method) {
+            var nativeMethod = window.RTCPeerConnection.prototype[method];
+            window.RTCPeerConnection.prototype[method] = function() {
+              var args = arguments;
+              var self = this;
+              var promise = new Promise(function(resolve, reject) {
+                nativeMethod.apply(self, [args[0], resolve, reject]);
+              });
+              if (args.length < 2) {
+                return promise;
+              }
+              return promise.then(function() {
+                args[1].apply(null, []);
+              },
+              function(err) {
+                if (args.length >= 3) {
+                  args[2].apply(null, [err]);
+                }
+              });
+            };
+          });
+    }
+
+    // promise support for createOffer and createAnswer. Available (without
+    // bugs) since M52: crbug/619289
+    if (browserDetails.version < 52) {
+      ['createOffer', 'createAnswer'].forEach(function(method) {
+        var nativeMethod = window.RTCPeerConnection.prototype[method];
+        window.RTCPeerConnection.prototype[method] = function() {
+          var self = this;
+          if (arguments.length < 1 || (arguments.length === 1 &&
+              typeof arguments[0] === 'object')) {
+            var opts = arguments.length === 1 ? arguments[0] : undefined;
+            return new Promise(function(resolve, reject) {
+              nativeMethod.apply(self, [resolve, reject, opts]);
+            });
+          }
+          return nativeMethod.apply(this, arguments);
+        };
+      });
+    }
+
+    // shim implicit creation of RTCSessionDescription/RTCIceCandidate
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+  }
+};
+
+
+// Expose public methods.
+module.exports = {
+  shimMediaStream: chromeShim.shimMediaStream,
+  shimOnTrack: chromeShim.shimOnTrack,
+  shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack,
+  shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf,
+  shimSourceObject: chromeShim.shimSourceObject,
+  shimPeerConnection: chromeShim.shimPeerConnection,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils.js":13,"./getusermedia":6}],6:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+
+  var constraintsToChrome_ = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname_ = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname_('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname_('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname_('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname_('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname_(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  var shimConstraints_ = function(constraints, func) {
+    if (browserDetails.version >= 61) {
+      return func(constraints);
+    }
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (constraints && typeof constraints.audio === 'object') {
+      var remap = function(obj, a, b) {
+        if (a in obj && !(b in obj)) {
+          obj[b] = obj[a];
+          delete obj[a];
+        }
+      };
+      constraints = JSON.parse(JSON.stringify(constraints));
+      remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
+      remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
+      constraints.audio = constraintsToChrome_(constraints.audio);
+    }
+    if (constraints && typeof constraints.video === 'object') {
+      // Shim facingMode for mobile & surface pro.
+      var face = constraints.video.facingMode;
+      face = face && ((typeof face === 'object') ? face : {ideal: face});
+      var getSupportedFacingModeLies = browserDetails.version < 66;
+
+      if ((face && (face.exact === 'user' || face.exact === 'environment' ||
+                    face.ideal === 'user' || face.ideal === 'environment')) &&
+          !(navigator.mediaDevices.getSupportedConstraints &&
+            navigator.mediaDevices.getSupportedConstraints().facingMode &&
+            !getSupportedFacingModeLies)) {
+        delete constraints.video.facingMode;
+        var matches;
+        if (face.exact === 'environment' || face.ideal === 'environment') {
+          matches = ['back', 'rear'];
+        } else if (face.exact === 'user' || face.ideal === 'user') {
+          matches = ['front'];
+        }
+        if (matches) {
+          // Look for matches in label, or use last cam for back (typical).
+          return navigator.mediaDevices.enumerateDevices()
+          .then(function(devices) {
+            devices = devices.filter(function(d) {
+              return d.kind === 'videoinput';
+            });
+            var dev = devices.find(function(d) {
+              return matches.some(function(match) {
+                return d.label.toLowerCase().indexOf(match) !== -1;
+              });
+            });
+            if (!dev && devices.length && matches.indexOf('back') !== -1) {
+              dev = devices[devices.length - 1]; // more likely the back cam
+            }
+            if (dev) {
+              constraints.video.deviceId = face.exact ? {exact: dev.deviceId} :
+                                                        {ideal: dev.deviceId};
+            }
+            constraints.video = constraintsToChrome_(constraints.video);
+            logging('chrome: ' + JSON.stringify(constraints));
+            return func(constraints);
+          });
+        }
+      }
+      constraints.video = constraintsToChrome_(constraints.video);
+    }
+    logging('chrome: ' + JSON.stringify(constraints));
+    return func(constraints);
+  };
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        PermissionDeniedError: 'NotAllowedError',
+        InvalidStateError: 'NotReadableError',
+        DevicesNotFoundError: 'NotFoundError',
+        ConstraintNotSatisfiedError: 'OverconstrainedError',
+        TrackStartError: 'NotReadableError',
+        MediaDeviceFailedDueToShutdown: 'NotReadableError',
+        MediaDeviceKillSwitchOn: 'NotReadableError'
+      }[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraintName,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    shimConstraints_(constraints, function(c) {
+      navigator.webkitGetUserMedia(c, onSuccess, function(e) {
+        if (onError) {
+          onError(shimError_(e));
+        }
+      });
+    });
+  };
+
+  navigator.getUserMedia = getUserMedia_;
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      navigator.getUserMedia(constraints, resolve, reject);
+    });
+  };
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {
+      getUserMedia: getUserMediaPromise_,
+      enumerateDevices: function() {
+        return new Promise(function(resolve) {
+          var kinds = {audio: 'audioinput', video: 'videoinput'};
+          return window.MediaStreamTrack.getSources(function(devices) {
+            resolve(devices.map(function(device) {
+              return {label: device.label,
+                kind: kinds[device.kind],
+                deviceId: device.id,
+                groupId: ''};
+            }));
+          });
+        });
+      },
+      getSupportedConstraints: function() {
+        return {
+          deviceId: true, echoCancellation: true, facingMode: true,
+          frameRate: true, height: true, width: true
+        };
+      }
+    };
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return getUserMediaPromise_(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(cs) {
+      return shimConstraints_(cs, function(c) {
+        return origGetUserMedia(c).then(function(stream) {
+          if (c.audio && !stream.getAudioTracks().length ||
+              c.video && !stream.getVideoTracks().length) {
+            stream.getTracks().forEach(function(track) {
+              track.stop();
+            });
+            throw new DOMException('', 'NotFoundError');
+          }
+          return stream;
+        }, function(e) {
+          return Promise.reject(shimError_(e));
+        });
+      });
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      logging('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      logging('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+};
+
+},{"../utils.js":13}],7:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+var utils = require('./utils');
+
+// Wraps the peerconnection event eventNameToWrap in a function
+// which returns the modified event object.
+function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
+  if (!window.RTCPeerConnection) {
+    return;
+  }
+  var proto = window.RTCPeerConnection.prototype;
+  var nativeAddEventListener = proto.addEventListener;
+  proto.addEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap) {
+      return nativeAddEventListener.apply(this, arguments);
+    }
+    var wrappedCallback = function(e) {
+      cb(wrapper(e));
+    };
+    this._eventMap = this._eventMap || {};
+    this._eventMap[cb] = wrappedCallback;
+    return nativeAddEventListener.apply(this, [nativeEventName,
+      wrappedCallback]);
+  };
+
+  var nativeRemoveEventListener = proto.removeEventListener;
+  proto.removeEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap || !this._eventMap
+        || !this._eventMap[cb]) {
+      return nativeRemoveEventListener.apply(this, arguments);
+    }
+    var unwrappedCb = this._eventMap[cb];
+    delete this._eventMap[cb];
+    return nativeRemoveEventListener.apply(this, [nativeEventName,
+      unwrappedCb]);
+  };
+
+  Object.defineProperty(proto, 'on' + eventNameToWrap, {
+    get: function() {
+      return this['_on' + eventNameToWrap];
+    },
+    set: function(cb) {
+      if (this['_on' + eventNameToWrap]) {
+        this.removeEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap]);
+        delete this['_on' + eventNameToWrap];
+      }
+      if (cb) {
+        this.addEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap] = cb);
+      }
+    }
+  });
+}
+
+module.exports = {
+  shimRTCIceCandidate: function(window) {
+    // foundation is arbitrarily chosen as an indicator for full support for
+    // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
+    if (window.RTCIceCandidate && 'foundation' in
+        window.RTCIceCandidate.prototype) {
+      return;
+    }
+
+    var NativeRTCIceCandidate = window.RTCIceCandidate;
+    window.RTCIceCandidate = function(args) {
+      // Remove the a= which shouldn't be part of the candidate string.
+      if (typeof args === 'object' && args.candidate &&
+          args.candidate.indexOf('a=') === 0) {
+        args = JSON.parse(JSON.stringify(args));
+        args.candidate = args.candidate.substr(2);
+      }
+
+      // Augment the native candidate with the parsed fields.
+      var nativeCandidate = new NativeRTCIceCandidate(args);
+      var parsedCandidate = SDPUtils.parseCandidate(args.candidate);
+      var augmentedCandidate = Object.assign(nativeCandidate,
+          parsedCandidate);
+
+      // Add a serializer that does not serialize the extra attributes.
+      augmentedCandidate.toJSON = function() {
+        return {
+          candidate: augmentedCandidate.candidate,
+          sdpMid: augmentedCandidate.sdpMid,
+          sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
+          usernameFragment: augmentedCandidate.usernameFragment,
+        };
+      };
+      return augmentedCandidate;
+    };
+
+    // Hook up the augmented candidate in onicecandidate and
+    // addEventListener('icecandidate', ...)
+    wrapPeerConnectionEvent(window, 'icecandidate', function(e) {
+      if (e.candidate) {
+        Object.defineProperty(e, 'candidate', {
+          value: new window.RTCIceCandidate(e.candidate),
+          writable: 'false'
+        });
+      }
+      return e;
+    });
+  },
+
+  // shimCreateObjectURL must be called before shimSourceObject to avoid loop.
+
+  shimCreateObjectURL: function(window) {
+    var URL = window && window.URL;
+
+    if (!(typeof window === 'object' && window.HTMLMediaElement &&
+          'srcObject' in window.HTMLMediaElement.prototype &&
+        URL.createObjectURL && URL.revokeObjectURL)) {
+      // Only shim CreateObjectURL using srcObject if srcObject exists.
+      return undefined;
+    }
+
+    var nativeCreateObjectURL = URL.createObjectURL.bind(URL);
+    var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL);
+    var streams = new Map(), newId = 0;
+
+    URL.createObjectURL = function(stream) {
+      if ('getTracks' in stream) {
+        var url = 'polyblob:' + (++newId);
+        streams.set(url, stream);
+        utils.deprecated('URL.createObjectURL(stream)',
+            'elem.srcObject = stream');
+        return url;
+      }
+      return nativeCreateObjectURL(stream);
+    };
+    URL.revokeObjectURL = function(url) {
+      nativeRevokeObjectURL(url);
+      streams.delete(url);
+    };
+
+    var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,
+                                              'src');
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'src', {
+      get: function() {
+        return dsc.get.apply(this);
+      },
+      set: function(url) {
+        this.srcObject = streams.get(url) || null;
+        return dsc.set.apply(this, [url]);
+      }
+    });
+
+    var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute;
+    window.HTMLMediaElement.prototype.setAttribute = function() {
+      if (arguments.length === 2 &&
+          ('' + arguments[0]).toLowerCase() === 'src') {
+        this.srcObject = streams.get(arguments[1]) || null;
+      }
+      return nativeSetAttribute.apply(this, arguments);
+    };
+  }
+};
+
+},{"./utils":13,"sdp":2}],8:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var shimRTCPeerConnection = require('rtcpeerconnection-shim');
+
+module.exports = {
+  shimGetUserMedia: require('./getusermedia'),
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (window.RTCIceGatherer) {
+      // ORTC defines an RTCIceCandidate object but no constructor.
+      // Not implemented in Edge.
+      if (!window.RTCIceCandidate) {
+        window.RTCIceCandidate = function(args) {
+          return args;
+        };
+      }
+      // ORTC does not have a session description object but
+      // other browsers (i.e. Chrome) that will support both PC and ORTC
+      // in the future might have this defined already.
+      if (!window.RTCSessionDescription) {
+        window.RTCSessionDescription = function(args) {
+          return args;
+        };
+      }
+      // this adds an additional event listener to MediaStrackTrack that signals
+      // when a tracks enabled property was changed. Workaround for a bug in
+      // addStream, see below. No longer required in 15025+
+      if (browserDetails.version < 15025) {
+        var origMSTEnabled = Object.getOwnPropertyDescriptor(
+            window.MediaStreamTrack.prototype, 'enabled');
+        Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', {
+          set: function(value) {
+            origMSTEnabled.set.call(this, value);
+            var ev = new Event('enabled');
+            ev.enabled = value;
+            this.dispatchEvent(ev);
+          }
+        });
+      }
+    }
+
+    // ORTC defines the DTMF sender a bit different.
+    // https://github.com/w3c/ortc/issues/714
+    if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = new window.RTCDtmfSender(this);
+            } else if (this.track.kind === 'video') {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+
+    window.RTCPeerConnection =
+        shimRTCPeerConnection(window, browserDetails.version);
+  },
+  shimReplaceTrack: function(window) {
+    // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614
+    if (window.RTCRtpSender &&
+        !('replaceTrack' in window.RTCRtpSender.prototype)) {
+      window.RTCRtpSender.prototype.replaceTrack =
+          window.RTCRtpSender.prototype.setTrack;
+    }
+  }
+};
+
+},{"../utils":13,"./getusermedia":9,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+// Expose public methods.
+module.exports = function(window) {
+  var navigator = window && window.navigator;
+
+  var shimError_ = function(e) {
+    return {
+      name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name;
+      }
+    };
+  };
+
+  // getUserMedia error shim.
+  var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+      bind(navigator.mediaDevices);
+  navigator.mediaDevices.getUserMedia = function(c) {
+    return origGetUserMedia(c).catch(function(e) {
+      return Promise.reject(shimError_(e));
+    });
+  };
+};
+
+},{}],10:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+
+var firefoxShim = {
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+            this.removeEventListener('addstream', this._ontrackpoly);
+          }
+          this.addEventListener('track', this._ontrack = f);
+          this.addEventListener('addstream', this._ontrackpoly = function(e) {
+            e.stream.getTracks().forEach(function(track) {
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = {track: track};
+              event.transceiver = {receiver: event.receiver};
+              event.streams = [e.stream];
+              this.dispatchEvent(event);
+            }.bind(this));
+          }.bind(this));
+        }
+      });
+    }
+    if (typeof window === 'object' && window.RTCTrackEvent &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        !('transceiver' in window.RTCTrackEvent.prototype)) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    // Firefox has supported mozSrcObject since FF22, unprefixed in 42.
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this.mozSrcObject;
+          },
+          set: function(stream) {
+            this.mozSrcObject = stream;
+          }
+        });
+      }
+    }
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (typeof window !== 'object' || !(window.RTCPeerConnection ||
+        window.mozRTCPeerConnection)) {
+      return; // probably media.peerconnection.enabled=false in about:config
+    }
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (browserDetails.version < 38) {
+          // .urls is not supported in FF < 38.
+          // create RTCIceServers with a single url.
+          if (pcConfig && pcConfig.iceServers) {
+            var newIceServers = [];
+            for (var i = 0; i < pcConfig.iceServers.length; i++) {
+              var server = pcConfig.iceServers[i];
+              if (server.hasOwnProperty('urls')) {
+                for (var j = 0; j < server.urls.length; j++) {
+                  var newServer = {
+                    url: server.urls[j]
+                  };
+                  if (server.urls[j].indexOf('turn') === 0) {
+                    newServer.username = server.username;
+                    newServer.credential = server.credential;
+                  }
+                  newIceServers.push(newServer);
+                }
+              } else {
+                newIceServers.push(pcConfig.iceServers[i]);
+              }
+            }
+            pcConfig.iceServers = newIceServers;
+          }
+        }
+        return new window.mozRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.mozRTCPeerConnection.prototype;
+
+      // wrap static methods. Currently just generateCertificate.
+      if (window.mozRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.mozRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+
+      window.RTCSessionDescription = window.mozRTCSessionDescription;
+      window.RTCIceCandidate = window.mozRTCIceCandidate;
+    }
+
+    // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+
+    // shim getStats with maplike support
+    var makeMapStats = function(stats) {
+      var map = new Map();
+      Object.keys(stats).forEach(function(key) {
+        map.set(key, stats[key]);
+        map[key] = stats[key];
+      });
+      return map;
+    };
+
+    var modernStatsTypes = {
+      inboundrtp: 'inbound-rtp',
+      outboundrtp: 'outbound-rtp',
+      candidatepair: 'candidate-pair',
+      localcandidate: 'local-candidate',
+      remotecandidate: 'remote-candidate'
+    };
+
+    var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(
+      selector,
+      onSucc,
+      onErr
+    ) {
+      return nativeGetStats.apply(this, [selector || null])
+        .then(function(stats) {
+          if (browserDetails.version < 48) {
+            stats = makeMapStats(stats);
+          }
+          if (browserDetails.version < 53 && !onSucc) {
+            // Shim only promise getStats with spec-hyphens in type names
+            // Leave callback version alone; misc old uses of forEach before Map
+            try {
+              stats.forEach(function(stat) {
+                stat.type = modernStatsTypes[stat.type] || stat.type;
+              });
+            } catch (e) {
+              if (e.name !== 'TypeError') {
+                throw e;
+              }
+              // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
+              stats.forEach(function(stat, i) {
+                stats.set(i, Object.assign({}, stat, {
+                  type: modernStatsTypes[stat.type] || stat.type
+                }));
+              });
+            }
+          }
+          return stats;
+        })
+        .then(onSucc, onErr);
+    };
+  },
+
+  shimRemoveStream: function(window) {
+    if (!window.RTCPeerConnection ||
+        'removeStream' in window.RTCPeerConnection.prototype) {
+      return;
+    }
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      utils.deprecated('removeStream', 'removeTrack');
+      this.getSenders().forEach(function(sender) {
+        if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) {
+          pc.removeTrack(sender);
+        }
+      });
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimOnTrack: firefoxShim.shimOnTrack,
+  shimSourceObject: firefoxShim.shimSourceObject,
+  shimPeerConnection: firefoxShim.shimPeerConnection,
+  shimRemoveStream: firefoxShim.shimRemoveStream,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils":13,"./getusermedia":11}],11:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+  var MediaStreamTrack = window && window.MediaStreamTrack;
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        InternalError: 'NotReadableError',
+        NotSupportedError: 'TypeError',
+        PermissionDeniedError: 'NotAllowedError',
+        SecurityError: 'NotAllowedError'
+      }[e.name] || e.name,
+      message: {
+        'The operation is insecure.': 'The request is not allowed by the ' +
+        'user agent or the platform in the current context.'
+      }[e.message] || e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  // getUserMedia constraints shim.
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    var constraintsToFF37_ = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r. min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (browserDetails.version < 38) {
+      logging('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37_(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37_(constraints.video);
+      }
+      logging('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
+      onError(shimError_(e));
+    });
+  };
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      getUserMedia_(constraints, resolve, reject);
+    });
+  };
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+        return new Promise(function(resolve) {
+          var infos = [
+            {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+            {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+          ];
+          resolve(infos);
+        });
+      };
+
+  if (browserDetails.version < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+  if (browserDetails.version < 49) {
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      return origGetUserMedia(c).then(function(stream) {
+        // Work around https://bugzil.la/802326
+        if (c.audio && !stream.getAudioTracks().length ||
+            c.video && !stream.getVideoTracks().length) {
+          stream.getTracks().forEach(function(track) {
+            track.stop();
+          });
+          throw new DOMException('The object can not be found here.',
+                                 'NotFoundError');
+        }
+        return stream;
+      }, function(e) {
+        return Promise.reject(shimError_(e));
+      });
+    };
+  }
+  if (!(browserDetails.version > 55 &&
+      'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
+    var remap = function(obj, a, b) {
+      if (a in obj && !(b in obj)) {
+        obj[b] = obj[a];
+        delete obj[a];
+      }
+    };
+
+    var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      if (typeof c === 'object' && typeof c.audio === 'object') {
+        c = JSON.parse(JSON.stringify(c));
+        remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
+        remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
+      }
+      return nativeGetUserMedia(c);
+    };
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
+      var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
+      MediaStreamTrack.prototype.getSettings = function() {
+        var obj = nativeGetSettings.apply(this, arguments);
+        remap(obj, 'mozAutoGainControl', 'autoGainControl');
+        remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
+        return obj;
+      };
+    }
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
+      var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
+      MediaStreamTrack.prototype.applyConstraints = function(c) {
+        if (this.kind === 'audio' && typeof c === 'object') {
+          c = JSON.parse(JSON.stringify(c));
+          remap(c, 'autoGainControl', 'mozAutoGainControl');
+          remap(c, 'noiseSuppression', 'mozNoiseSuppression');
+        }
+        return nativeApplyConstraints.apply(this, [c]);
+      };
+    }
+  }
+  navigator.getUserMedia = function(constraints, onSuccess, onError) {
+    if (browserDetails.version < 44) {
+      return getUserMedia_(constraints, onSuccess, onError);
+    }
+    // Replace Firefox 44+'s deprecation warning with unprefixed version.
+    utils.deprecated('navigator.getUserMedia',
+        'navigator.mediaDevices.getUserMedia');
+    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
+  };
+};
+
+},{"../utils":13}],12:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+'use strict';
+var utils = require('../utils');
+
+var safariShim = {
+  // TODO: DrAlex, should be here, double check against LayoutTests
+
+  // TODO: once the back-end for the mac port is done, add.
+  // TODO: check for webkitGTK+
+  // shimPeerConnection: function() { },
+
+  shimLocalStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getLocalStreams = function() {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        return this._localStreams;
+      };
+    }
+    if (!('getStreamById' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getStreamById = function(id) {
+        var result = null;
+        if (this._localStreams) {
+          this._localStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        if (this._remoteStreams) {
+          this._remoteStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        return result;
+      };
+    }
+    if (!('addStream' in window.RTCPeerConnection.prototype)) {
+      var _addTrack = window.RTCPeerConnection.prototype.addTrack;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        if (this._localStreams.indexOf(stream) === -1) {
+          this._localStreams.push(stream);
+        }
+        var self = this;
+        stream.getTracks().forEach(function(track) {
+          _addTrack.call(self, track, stream);
+        });
+      };
+
+      window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+        if (stream) {
+          if (!this._localStreams) {
+            this._localStreams = [stream];
+          } else if (this._localStreams.indexOf(stream) === -1) {
+            this._localStreams.push(stream);
+          }
+        }
+        return _addTrack.call(this, track, stream);
+      };
+    }
+    if (!('removeStream' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        var index = this._localStreams.indexOf(stream);
+        if (index === -1) {
+          return;
+        }
+        this._localStreams.splice(index, 1);
+        var self = this;
+        var tracks = stream.getTracks();
+        this.getSenders().forEach(function(sender) {
+          if (tracks.indexOf(sender.track) !== -1) {
+            self.removeTrack(sender);
+          }
+        });
+      };
+    }
+  },
+  shimRemoteStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getRemoteStreams = function() {
+        return this._remoteStreams ? this._remoteStreams : [];
+      };
+    }
+    if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
+        get: function() {
+          return this._onaddstream;
+        },
+        set: function(f) {
+          if (this._onaddstream) {
+            this.removeEventListener('addstream', this._onaddstream);
+            this.removeEventListener('track', this._onaddstreampoly);
+          }
+          this.addEventListener('addstream', this._onaddstream = f);
+          this.addEventListener('track', this._onaddstreampoly = function(e) {
+            var stream = e.streams[0];
+            if (!this._remoteStreams) {
+              this._remoteStreams = [];
+            }
+            if (this._remoteStreams.indexOf(stream) >= 0) {
+              return;
+            }
+            this._remoteStreams.push(stream);
+            var event = new Event('addstream');
+            event.stream = e.streams[0];
+            this.dispatchEvent(event);
+          }.bind(this));
+        }
+      });
+    }
+  },
+  shimCallbacksAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    var prototype = window.RTCPeerConnection.prototype;
+    var createOffer = prototype.createOffer;
+    var createAnswer = prototype.createAnswer;
+    var setLocalDescription = prototype.setLocalDescription;
+    var setRemoteDescription = prototype.setRemoteDescription;
+    var addIceCandidate = prototype.addIceCandidate;
+
+    prototype.createOffer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createOffer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    prototype.createAnswer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createAnswer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    var withCallback = function(description, successCallback, failureCallback) {
+      var promise = setLocalDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setLocalDescription = withCallback;
+
+    withCallback = function(description, successCallback, failureCallback) {
+      var promise = setRemoteDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setRemoteDescription = withCallback;
+
+    withCallback = function(candidate, successCallback, failureCallback) {
+      var promise = addIceCandidate.apply(this, [candidate]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.addIceCandidate = withCallback;
+  },
+  shimGetUserMedia: function(window) {
+    var navigator = window && window.navigator;
+
+    if (!navigator.getUserMedia) {
+      if (navigator.webkitGetUserMedia) {
+        navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+      } else if (navigator.mediaDevices &&
+          navigator.mediaDevices.getUserMedia) {
+        navigator.getUserMedia = function(constraints, cb, errcb) {
+          navigator.mediaDevices.getUserMedia(constraints)
+          .then(cb, errcb);
+        }.bind(navigator);
+      }
+    }
+  },
+  shimRTCIceServerUrls: function(window) {
+    // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+    var OrigPeerConnection = window.RTCPeerConnection;
+    window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (!server.hasOwnProperty('urls') &&
+              server.hasOwnProperty('url')) {
+            utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+            server = JSON.parse(JSON.stringify(server));
+            server.urls = server.url;
+            delete server.url;
+            newIceServers.push(server);
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+      return new OrigPeerConnection(pcConfig, pcConstraints);
+    };
+    window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+    // wrap static methods. Currently just generateCertificate.
+    if ('generateCertificate' in window.RTCPeerConnection) {
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+  },
+  shimTrackEventTransceiver: function(window) {
+    // Add event.transceiver member over deprecated event.receiver
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is
+        // defined for some reason even when window.RTCTransceiver is not.
+        !window.RTCTransceiver) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimCreateOfferLegacy: function(window) {
+    var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
+    window.RTCPeerConnection.prototype.createOffer = function(offerOptions) {
+      var pc = this;
+      if (offerOptions) {
+        var audioTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'audio';
+        });
+        if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
+          if (audioTransceiver.direction === 'sendrecv') {
+            audioTransceiver.setDirection('sendonly');
+          } else if (audioTransceiver.direction === 'recvonly') {
+            audioTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveAudio === true &&
+            !audioTransceiver) {
+          pc.addTransceiver('audio');
+        }
+
+        var videoTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'video';
+        });
+        if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
+          if (videoTransceiver.direction === 'sendrecv') {
+            videoTransceiver.setDirection('sendonly');
+          } else if (videoTransceiver.direction === 'recvonly') {
+            videoTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveVideo === true &&
+            !videoTransceiver) {
+          pc.addTransceiver('video');
+        }
+      }
+      return origCreateOffer.apply(pc, arguments);
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimCallbacksAPI: safariShim.shimCallbacksAPI,
+  shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI,
+  shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI,
+  shimGetUserMedia: safariShim.shimGetUserMedia,
+  shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls,
+  shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver,
+  shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy
+  // TODO
+  // shimPeerConnection: safariShim.shimPeerConnection
+};
+
+},{"../utils":13}],13:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var logDisabled_ = true;
+var deprecationWarnings_ = true;
+
+// Utility methods.
+var utils = {
+  disableLog: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    logDisabled_ = bool;
+    return (bool) ? 'adapter.js logging disabled' :
+        'adapter.js logging enabled';
+  },
+
+  /**
+   * Disable or enable deprecation warnings
+   * @param {!boolean} bool set to true to disable warnings.
+   */
+  disableWarnings: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    deprecationWarnings_ = !bool;
+    return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
+  },
+
+  log: function() {
+    if (typeof window === 'object') {
+      if (logDisabled_) {
+        return;
+      }
+      if (typeof console !== 'undefined' && typeof console.log === 'function') {
+        console.log.apply(console, arguments);
+      }
+    }
+  },
+
+  /**
+   * Shows a deprecation warning suggesting the modern and spec-compatible API.
+   */
+  deprecated: function(oldMethod, newMethod) {
+    if (!deprecationWarnings_) {
+      return;
+    }
+    console.warn(oldMethod + ' is deprecated, please use ' + newMethod +
+        ' instead.');
+  },
+
+  /**
+   * Extract browser version out of the provided user agent string.
+   *
+   * @param {!string} uastring userAgent string.
+   * @param {!string} expr Regular expression used as match criteria.
+   * @param {!number} pos position in the version string to be returned.
+   * @return {!number} browser version.
+   */
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos], 10);
+  },
+
+  /**
+   * Browser detector.
+   *
+   * @return {object} result containing browser and version
+   *     properties.
+   */
+  detectBrowser: function(window) {
+    var navigator = window && window.navigator;
+
+    // Returned result object.
+    var result = {};
+    result.browser = null;
+    result.version = null;
+
+    // Fail early if it's not a browser
+    if (typeof window === 'undefined' || !window.navigator) {
+      result.browser = 'Not a browser.';
+      return result;
+    }
+
+    // Firefox.
+    if (navigator.mozGetUserMedia) {
+      result.browser = 'firefox';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Firefox\/(\d+)\./, 1);
+    } else if (navigator.webkitGetUserMedia) {
+      // Chrome, Chromium, Webview, Opera, all use the chrome shim for now
+      if (window.webkitRTCPeerConnection) {
+        result.browser = 'chrome';
+        result.version = this.extractVersion(navigator.userAgent,
+          /Chrom(e|ium)\/(\d+)\./, 2);
+      } else { // Safari (in an unpublished version) or unknown webkit-based.
+        if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
+          result.browser = 'safari';
+          result.version = this.extractVersion(navigator.userAgent,
+            /AppleWebKit\/(\d+)\./, 1);
+        } else { // unknown webkit-based browser.
+          result.browser = 'Unsupported webkit-based browser ' +
+              'with GUM support but no WebRTC support.';
+          return result;
+        }
+      }
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
+      result.browser = 'edge';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Edge\/(\d+).(\d+)$/, 2);
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
+        // Safari, with webkitGetUserMedia removed.
+      result.browser = 'safari';
+      result.version = this.extractVersion(navigator.userAgent,
+          /AppleWebKit\/(\d+)\./, 1);
+    } else { // Default fallthrough: not supported.
+      result.browser = 'Not a supported browser.';
+      return result;
+    }
+
+    return result;
+  },
+
+};
+
+// Export.
+module.exports = {
+  log: utils.log,
+  deprecated: utils.deprecated,
+  disableLog: utils.disableLog,
+  disableWarnings: utils.disableWarnings,
+  extractVersion: utils.extractVersion,
+  shimCreateObjectURL: utils.shimCreateObjectURL,
+  detectBrowser: utils.detectBrowser.bind(utils)
+};
+
+},{}]},{},[3])(3)
+});

+ 2795 - 0
support/client/lib/vwf/view/webrtc/dist/adapter_no_edge.js

@@ -0,0 +1,2795 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+
+},{}],2:[function(require,module,exports){
+ /* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+  return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+  return blob.trim().split('\n').map(function(line) {
+    return line.trim();
+  });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+  var parts = blob.split('\nm=');
+  return parts.map(function(part, index) {
+    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+  });
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+  return SDPUtils.splitLines(blob).filter(function(line) {
+    return line.indexOf(prefix) === 0;
+  });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+  var parts;
+  // Parse both variants.
+  if (line.indexOf('a=candidate:') === 0) {
+    parts = line.substring(12).split(' ');
+  } else {
+    parts = line.substring(10).split(' ');
+  }
+
+  var candidate = {
+    foundation: parts[0],
+    component: parseInt(parts[1], 10),
+    protocol: parts[2].toLowerCase(),
+    priority: parseInt(parts[3], 10),
+    ip: parts[4],
+    port: parseInt(parts[5], 10),
+    // skip parts[6] == 'typ'
+    type: parts[7]
+  };
+
+  for (var i = 8; i < parts.length; i += 2) {
+    switch (parts[i]) {
+      case 'raddr':
+        candidate.relatedAddress = parts[i + 1];
+        break;
+      case 'rport':
+        candidate.relatedPort = parseInt(parts[i + 1], 10);
+        break;
+      case 'tcptype':
+        candidate.tcpType = parts[i + 1];
+        break;
+      case 'ufrag':
+        candidate.ufrag = parts[i + 1]; // for backward compability.
+        candidate.usernameFragment = parts[i + 1];
+        break;
+      default: // extension handling, in particular ufrag
+        candidate[parts[i]] = parts[i + 1];
+        break;
+    }
+  }
+  return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+  var sdp = [];
+  sdp.push(candidate.foundation);
+  sdp.push(candidate.component);
+  sdp.push(candidate.protocol.toUpperCase());
+  sdp.push(candidate.priority);
+  sdp.push(candidate.ip);
+  sdp.push(candidate.port);
+
+  var type = candidate.type;
+  sdp.push('typ');
+  sdp.push(type);
+  if (type !== 'host' && candidate.relatedAddress &&
+      candidate.relatedPort) {
+    sdp.push('raddr');
+    sdp.push(candidate.relatedAddress); // was: relAddr
+    sdp.push('rport');
+    sdp.push(candidate.relatedPort); // was: relPort
+  }
+  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+    sdp.push('tcptype');
+    sdp.push(candidate.tcpType);
+  }
+  if (candidate.ufrag) {
+    sdp.push('ufrag');
+    sdp.push(candidate.ufrag);
+  }
+  return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+  return line.substr(14).split(' ');
+}
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+  var parts = line.substr(9).split(' ');
+  var parsed = {
+    payloadType: parseInt(parts.shift(), 10) // was: id
+  };
+
+  parts = parts[0].split('/');
+
+  parsed.name = parts[0];
+  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+  // was: channels
+  parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+  return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+      (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+  var parts = line.substr(9).split(' ');
+  return {
+    id: parseInt(parts[0], 10),
+    direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+    uri: parts[1]
+  };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+      (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+          ? '/' + headerExtension.direction
+          : '') +
+      ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+  var parsed = {};
+  var kv;
+  var parts = line.substr(line.indexOf(' ') + 1).split(';');
+  for (var j = 0; j < parts.length; j++) {
+    kv = parts[j].trim().split('=');
+    parsed[kv[0].trim()] = kv[1];
+  }
+  return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+  var line = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.parameters && Object.keys(codec.parameters).length) {
+    var params = [];
+    Object.keys(codec.parameters).forEach(function(param) {
+      params.push(param + '=' + codec.parameters[param]);
+    });
+    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+  }
+  return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+  return {
+    type: parts.shift(),
+    parameter: parts.join(' ')
+  };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+  var lines = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+    // FIXME: special handling for trr-int?
+    codec.rtcpFeedback.forEach(function(fb) {
+      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+      (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+          '\r\n';
+    });
+  }
+  return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+  var sp = line.indexOf(' ');
+  var parts = {
+    ssrc: parseInt(line.substr(7, sp - 7), 10)
+  };
+  var colon = line.indexOf(':', sp);
+  if (colon > -1) {
+    parts.attribute = line.substr(sp + 1, colon - sp - 1);
+    parts.value = line.substr(colon + 1);
+  } else {
+    parts.attribute = line.substr(sp + 1);
+  }
+  return parts;
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+  var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+  if (mid) {
+    return mid.substr(6);
+  }
+}
+
+SDPUtils.parseFingerprint = function(line) {
+  var parts = line.substr(14).split(' ');
+  return {
+    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+    value: parts[1]
+  };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+      'a=fingerprint:');
+  // Note: a=setup line is ignored since we use the 'auto' role.
+  // Note2: 'algorithm' is not case sensitive except in Edge.
+  return {
+    role: 'auto',
+    fingerprints: lines.map(SDPUtils.parseFingerprint)
+  };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+  var sdp = 'a=setup:' + setupType + '\r\n';
+  params.fingerprints.forEach(function(fp) {
+    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+  });
+  return sdp;
+};
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var iceParameters = {
+    usernameFragment: lines.filter(function(line) {
+      return line.indexOf('a=ice-ufrag:') === 0;
+    })[0].substr(12),
+    password: lines.filter(function(line) {
+      return line.indexOf('a=ice-pwd:') === 0;
+    })[0].substr(10)
+  };
+  return iceParameters;
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+      'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+  var description = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: [],
+    rtcp: []
+  };
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+    var pt = mline[i];
+    var rtpmapline = SDPUtils.matchPrefix(
+        mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+    if (rtpmapline) {
+      var codec = SDPUtils.parseRtpMap(rtpmapline);
+      var fmtps = SDPUtils.matchPrefix(
+          mediaSection, 'a=fmtp:' + pt + ' ');
+      // Only the first a=fmtp:<pt> is considered.
+      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+      codec.rtcpFeedback = SDPUtils.matchPrefix(
+          mediaSection, 'a=rtcp-fb:' + pt + ' ')
+        .map(SDPUtils.parseRtcpFb);
+      description.codecs.push(codec);
+      // parse FEC mechanisms from rtpmap lines.
+      switch (codec.name.toUpperCase()) {
+        case 'RED':
+        case 'ULPFEC':
+          description.fecMechanisms.push(codec.name.toUpperCase());
+          break;
+        default: // only RED and ULPFEC are recognized as FEC mechanisms.
+          break;
+      }
+    }
+  }
+  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+    description.headerExtensions.push(SDPUtils.parseExtmap(line));
+  });
+  // FIXME: parse rtcp.
+  return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+  var sdp = '';
+
+  // Build the mline.
+  sdp += 'm=' + kind + ' ';
+  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+  sdp += ' UDP/TLS/RTP/SAVPF ';
+  sdp += caps.codecs.map(function(codec) {
+    if (codec.preferredPayloadType !== undefined) {
+      return codec.preferredPayloadType;
+    }
+    return codec.payloadType;
+  }).join(' ') + '\r\n';
+
+  sdp += 'c=IN IP4 0.0.0.0\r\n';
+  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+  caps.codecs.forEach(function(codec) {
+    sdp += SDPUtils.writeRtpMap(codec);
+    sdp += SDPUtils.writeFmtp(codec);
+    sdp += SDPUtils.writeRtcpFb(codec);
+  });
+  var maxptime = 0;
+  caps.codecs.forEach(function(codec) {
+    if (codec.maxptime > maxptime) {
+      maxptime = codec.maxptime;
+    }
+  });
+  if (maxptime > 0) {
+    sdp += 'a=maxptime:' + maxptime + '\r\n';
+  }
+  sdp += 'a=rtcp-mux\r\n';
+
+  caps.headerExtensions.forEach(function(extension) {
+    sdp += SDPUtils.writeExtmap(extension);
+  });
+  // FIXME: write fecMechanisms.
+  return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+  var encodingParameters = [];
+  var description = SDPUtils.parseRtpParameters(mediaSection);
+  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+  // filter a=ssrc:... cname:, ignore PlanB-msid
+  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'cname';
+  });
+  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+  var secondarySsrc;
+
+  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+  .map(function(line) {
+    var parts = line.split(' ');
+    parts.shift();
+    return parts.map(function(part) {
+      return parseInt(part, 10);
+    });
+  });
+  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+    secondarySsrc = flows[0][1];
+  }
+
+  description.codecs.forEach(function(codec) {
+    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+      var encParam = {
+        ssrc: primarySsrc,
+        codecPayloadType: parseInt(codec.parameters.apt, 10),
+        rtx: {
+          ssrc: secondarySsrc
+        }
+      };
+      encodingParameters.push(encParam);
+      if (hasRed) {
+        encParam = JSON.parse(JSON.stringify(encParam));
+        encParam.fec = {
+          ssrc: secondarySsrc,
+          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+        };
+        encodingParameters.push(encParam);
+      }
+    }
+  });
+  if (encodingParameters.length === 0 && primarySsrc) {
+    encodingParameters.push({
+      ssrc: primarySsrc
+    });
+  }
+
+  // we support both b=AS and b=TIAS but interpret AS as TIAS.
+  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+  if (bandwidth.length) {
+    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(7), 10);
+    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+      // use formula from JSEP to convert b=AS to TIAS value.
+      bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+          - (50 * 40 * 8);
+    } else {
+      bandwidth = undefined;
+    }
+    encodingParameters.forEach(function(params) {
+      params.maxBitrate = bandwidth;
+    });
+  }
+  return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+  var rtcpParameters = {};
+
+  var cname;
+  // Gets the first SSRC. Note that with RTX there might be multiple
+  // SSRCs.
+  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+      .map(function(line) {
+        return SDPUtils.parseSsrcMedia(line);
+      })
+      .filter(function(obj) {
+        return obj.attribute === 'cname';
+      })[0];
+  if (remoteSsrc) {
+    rtcpParameters.cname = remoteSsrc.value;
+    rtcpParameters.ssrc = remoteSsrc.ssrc;
+  }
+
+  // Edge uses the compound attribute instead of reducedSize
+  // compound is !reducedSize
+  var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+  rtcpParameters.reducedSize = rsize.length > 0;
+  rtcpParameters.compound = rsize.length === 0;
+
+  // parses the rtcp-mux attrіbute.
+  // Note that Edge does not support unmuxed RTCP.
+  var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+  rtcpParameters.mux = mux.length > 0;
+
+  return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+  var parts;
+  var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+  if (spec.length === 1) {
+    parts = spec[0].substr(7).split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+  var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'msid';
+  });
+  if (planB.length > 0) {
+    parts = planB[0].value.split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+  return Math.random().toString().substr(2, 21);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) {
+  var sessionId;
+  var version = sessVer !== undefined ? sessVer : 2;
+  if (sessId) {
+    sessionId = sessId;
+  } else {
+    sessionId = SDPUtils.generateSessionId();
+  }
+  // FIXME: sess-id should be an NTP timestamp.
+  return 'v=0\r\n' +
+      'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' +
+      's=-\r\n' +
+      't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  var lines = SDPUtils.splitLines(mediaSection);
+  for (var i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substr(2);
+      default:
+        // FIXME: What should happen here?
+    }
+  }
+  if (sessionpart) {
+    return SDPUtils.getDirection(sessionpart);
+  }
+  return 'sendrecv';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+  return mediaSection.split(' ', 2)[1] === '0';
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return {
+    kind: mline[0].substr(2),
+    port: parseInt(mline[1], 10),
+    protocol: mline[2],
+    fmt: mline.slice(3).join(' ')
+  };
+};
+
+// Expose public methods.
+if (typeof module === 'object') {
+  module.exports = SDPUtils;
+}
+
+},{}],3:[function(require,module,exports){
+(function (global){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var adapterFactory = require('./adapter_factory.js');
+module.exports = adapterFactory({window: global.window});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./adapter_factory.js":4}],4:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var utils = require('./utils');
+// Shimming starts here.
+module.exports = function(dependencies, opts) {
+  var window = dependencies && dependencies.window;
+
+  var options = {
+    shimChrome: true,
+    shimFirefox: true,
+    shimEdge: true,
+    shimSafari: true,
+  };
+
+  for (var key in opts) {
+    if (hasOwnProperty.call(opts, key)) {
+      options[key] = opts[key];
+    }
+  }
+
+  // Utils.
+  var logging = utils.log;
+  var browserDetails = utils.detectBrowser(window);
+
+  // Export to the adapter global object visible in the browser.
+  var adapter = {
+    browserDetails: browserDetails,
+    extractVersion: utils.extractVersion,
+    disableLog: utils.disableLog,
+    disableWarnings: utils.disableWarnings
+  };
+
+  // Uncomment the line below if you want logging to occur, including logging
+  // for the switch statement below. Can also be turned on in the browser via
+  // adapter.disableLog(false), but then logging from the switch statement below
+  // will not appear.
+  // require('./utils').disableLog(false);
+
+  // Browser shims.
+  var chromeShim = require('./chrome/chrome_shim') || null;
+  var edgeShim = require('./edge/edge_shim') || null;
+  var firefoxShim = require('./firefox/firefox_shim') || null;
+  var safariShim = require('./safari/safari_shim') || null;
+  var commonShim = require('./common_shim') || null;
+
+  // Shim browser if found.
+  switch (browserDetails.browser) {
+    case 'chrome':
+      if (!chromeShim || !chromeShim.shimPeerConnection ||
+          !options.shimChrome) {
+        logging('Chrome shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming chrome.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = chromeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      chromeShim.shimGetUserMedia(window);
+      chromeShim.shimMediaStream(window);
+      chromeShim.shimSourceObject(window);
+      chromeShim.shimPeerConnection(window);
+      chromeShim.shimOnTrack(window);
+      chromeShim.shimAddTrackRemoveTrack(window);
+      chromeShim.shimGetSendersWithDtmf(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'firefox':
+      if (!firefoxShim || !firefoxShim.shimPeerConnection ||
+          !options.shimFirefox) {
+        logging('Firefox shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming firefox.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = firefoxShim;
+      commonShim.shimCreateObjectURL(window);
+
+      firefoxShim.shimGetUserMedia(window);
+      firefoxShim.shimSourceObject(window);
+      firefoxShim.shimPeerConnection(window);
+      firefoxShim.shimOnTrack(window);
+      firefoxShim.shimRemoveStream(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'edge':
+      if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
+        logging('MS edge shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming edge.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = edgeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      edgeShim.shimGetUserMedia(window);
+      edgeShim.shimPeerConnection(window);
+      edgeShim.shimReplaceTrack(window);
+
+      // the edge shim implements the full RTCIceCandidate object.
+      break;
+    case 'safari':
+      if (!safariShim || !options.shimSafari) {
+        logging('Safari shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming safari.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = safariShim;
+      commonShim.shimCreateObjectURL(window);
+
+      safariShim.shimRTCIceServerUrls(window);
+      safariShim.shimCallbacksAPI(window);
+      safariShim.shimLocalStreamsAPI(window);
+      safariShim.shimRemoteStreamsAPI(window);
+      safariShim.shimTrackEventTransceiver(window);
+      safariShim.shimGetUserMedia(window);
+      safariShim.shimCreateOfferLegacy(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    default:
+      logging('Unsupported browser!');
+      break;
+  }
+
+  return adapter;
+};
+
+},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":1,"./firefox/firefox_shim":8,"./safari/safari_shim":10,"./utils":11}],5:[function(require,module,exports){
+
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+var chromeShim = {
+  shimMediaStream: function(window) {
+    window.MediaStream = window.MediaStream || window.webkitMediaStream;
+  },
+
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+          }
+          this.addEventListener('track', this._ontrack = f);
+        }
+      });
+      var origSetRemoteDescription =
+          window.RTCPeerConnection.prototype.setRemoteDescription;
+      window.RTCPeerConnection.prototype.setRemoteDescription = function() {
+        var pc = this;
+        if (!pc._ontrackpoly) {
+          pc._ontrackpoly = function(e) {
+            // onaddstream does not fire when a track is added to an existing
+            // stream. But stream.onaddtrack is implemented so we use that.
+            e.stream.addEventListener('addtrack', function(te) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === te.track.id;
+                });
+              } else {
+                receiver = {track: te.track};
+              }
+
+              var event = new Event('track');
+              event.track = te.track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+            e.stream.getTracks().forEach(function(track) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === track.id;
+                });
+              } else {
+                receiver = {track: track};
+              }
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+          };
+          pc.addEventListener('addstream', pc._ontrackpoly);
+        }
+        return origSetRemoteDescription.apply(pc, arguments);
+      };
+    }
+  },
+
+  shimGetSendersWithDtmf: function(window) {
+    // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        !('getSenders' in window.RTCPeerConnection.prototype) &&
+        'createDTMFSender' in window.RTCPeerConnection.prototype) {
+      var shimSenderWithDtmf = function(pc, track) {
+        return {
+          track: track,
+          get dtmf() {
+            if (this._dtmf === undefined) {
+              if (track.kind === 'audio') {
+                this._dtmf = pc.createDTMFSender(track);
+              } else {
+                this._dtmf = null;
+              }
+            }
+            return this._dtmf;
+          },
+          _pc: pc
+        };
+      };
+
+      // augment addTrack when getSenders is not available.
+      if (!window.RTCPeerConnection.prototype.getSenders) {
+        window.RTCPeerConnection.prototype.getSenders = function() {
+          this._senders = this._senders || [];
+          return this._senders.slice(); // return a copy of the internal state.
+        };
+        var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
+        window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+          var pc = this;
+          var sender = origAddTrack.apply(pc, arguments);
+          if (!sender) {
+            sender = shimSenderWithDtmf(pc, track);
+            pc._senders.push(sender);
+          }
+          return sender;
+        };
+
+        var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
+        window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+          var pc = this;
+          origRemoveTrack.apply(pc, arguments);
+          var idx = pc._senders.indexOf(sender);
+          if (idx !== -1) {
+            pc._senders.splice(idx, 1);
+          }
+        };
+      }
+      var origAddStream = window.RTCPeerConnection.prototype.addStream;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origAddStream.apply(pc, [stream]);
+        stream.getTracks().forEach(function(track) {
+          pc._senders.push(shimSenderWithDtmf(pc, track));
+        });
+      };
+
+      var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origRemoveStream.apply(pc, [stream]);
+
+        stream.getTracks().forEach(function(track) {
+          var sender = pc._senders.find(function(s) {
+            return s.track === track;
+          });
+          if (sender) {
+            pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender
+          }
+        });
+      };
+    } else if (typeof window === 'object' && window.RTCPeerConnection &&
+               'getSenders' in window.RTCPeerConnection.prototype &&
+               'createDTMFSender' in window.RTCPeerConnection.prototype &&
+               window.RTCRtpSender &&
+               !('dtmf' in window.RTCRtpSender.prototype)) {
+      var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
+      window.RTCPeerConnection.prototype.getSenders = function() {
+        var pc = this;
+        var senders = origGetSenders.apply(pc, []);
+        senders.forEach(function(sender) {
+          sender._pc = pc;
+        });
+        return senders;
+      };
+
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = this._pc.createDTMFSender(this.track);
+            } else {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    var URL = window && window.URL;
+
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this._srcObject;
+          },
+          set: function(stream) {
+            var self = this;
+            // Use _srcObject as a private property for this shim
+            this._srcObject = stream;
+            if (this.src) {
+              URL.revokeObjectURL(this.src);
+            }
+
+            if (!stream) {
+              this.src = '';
+              return undefined;
+            }
+            this.src = URL.createObjectURL(stream);
+            // We need to recreate the blob url when a track is added or
+            // removed. Doing it manually since we want to avoid a recursion.
+            stream.addEventListener('addtrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+            stream.addEventListener('removetrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+          }
+        });
+      }
+    }
+  },
+
+  shimAddTrackRemoveTrack: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+    // shim addTrack and removeTrack.
+    if (window.RTCPeerConnection.prototype.addTrack &&
+        browserDetails.version >= 64) {
+      return;
+    }
+
+    // also shim pc.getLocalStreams when addTrack is shimmed
+    // to return the original streams.
+    var origGetLocalStreams = window.RTCPeerConnection.prototype
+        .getLocalStreams;
+    window.RTCPeerConnection.prototype.getLocalStreams = function() {
+      var self = this;
+      var nativeStreams = origGetLocalStreams.apply(this);
+      self._reverseStreams = self._reverseStreams || {};
+      return nativeStreams.map(function(stream) {
+        return self._reverseStreams[stream.id];
+      });
+    };
+
+    var origAddStream = window.RTCPeerConnection.prototype.addStream;
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      stream.getTracks().forEach(function(track) {
+        var alreadyExists = pc.getSenders().find(function(s) {
+          return s.track === track;
+        });
+        if (alreadyExists) {
+          throw new DOMException('Track already exists.',
+              'InvalidAccessError');
+        }
+      });
+      // Add identity mapping for consistency with addTrack.
+      // Unless this is being used with a stream from addTrack.
+      if (!pc._reverseStreams[stream.id]) {
+        var newStream = new window.MediaStream(stream.getTracks());
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        stream = newStream;
+      }
+      origAddStream.apply(pc, [stream]);
+    };
+
+    var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+      delete pc._reverseStreams[(pc._streams[stream.id] ?
+          pc._streams[stream.id].id : stream.id)];
+      delete pc._streams[stream.id];
+    };
+
+    window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      var streams = [].slice.call(arguments, 1);
+      if (streams.length !== 1 ||
+          !streams[0].getTracks().find(function(t) {
+            return t === track;
+          })) {
+        // this is not fully correct but all we can manage without
+        // [[associated MediaStreams]] internal slot.
+        throw new DOMException(
+          'The adapter.js addTrack polyfill only supports a single ' +
+          ' stream which is associated with the specified track.',
+          'NotSupportedError');
+      }
+
+      var alreadyExists = pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+      if (alreadyExists) {
+        throw new DOMException('Track already exists.',
+            'InvalidAccessError');
+      }
+
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+      var oldStream = pc._streams[stream.id];
+      if (oldStream) {
+        // this is using odd Chrome behaviour, use with caution:
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
+        // Note: we rely on the high-level addTrack/dtmf shim to
+        // create the sender with a dtmf sender.
+        oldStream.addTrack(track);
+
+        // Trigger ONN async.
+        Promise.resolve().then(function() {
+          pc.dispatchEvent(new Event('negotiationneeded'));
+        });
+      } else {
+        var newStream = new window.MediaStream([track]);
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        pc.addStream(newStream);
+      }
+      return pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+    };
+
+    // replace the internal stream id with the external one and
+    // vice versa.
+    function replaceInternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(internalStream.id, 'g'),
+            externalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    function replaceExternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(externalStream.id, 'g'),
+            internalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    ['createOffer', 'createAnswer'].forEach(function(method) {
+      var nativeMethod = window.RTCPeerConnection.prototype[method];
+      window.RTCPeerConnection.prototype[method] = function() {
+        var pc = this;
+        var args = arguments;
+        var isLegacyCall = arguments.length &&
+            typeof arguments[0] === 'function';
+        if (isLegacyCall) {
+          return nativeMethod.apply(pc, [
+            function(description) {
+              var desc = replaceInternalStreamId(pc, description);
+              args[0].apply(null, [desc]);
+            },
+            function(err) {
+              if (args[1]) {
+                args[1].apply(null, err);
+              }
+            }, arguments[2]
+          ]);
+        }
+        return nativeMethod.apply(pc, arguments)
+        .then(function(description) {
+          return replaceInternalStreamId(pc, description);
+        });
+      };
+    });
+
+    var origSetLocalDescription =
+        window.RTCPeerConnection.prototype.setLocalDescription;
+    window.RTCPeerConnection.prototype.setLocalDescription = function() {
+      var pc = this;
+      if (!arguments.length || !arguments[0].type) {
+        return origSetLocalDescription.apply(pc, arguments);
+      }
+      arguments[0] = replaceExternalStreamId(pc, arguments[0]);
+      return origSetLocalDescription.apply(pc, arguments);
+    };
+
+    // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier
+
+    var origLocalDescription = Object.getOwnPropertyDescriptor(
+        window.RTCPeerConnection.prototype, 'localDescription');
+    Object.defineProperty(window.RTCPeerConnection.prototype,
+        'localDescription', {
+          get: function() {
+            var pc = this;
+            var description = origLocalDescription.get.apply(this);
+            if (description.type === '') {
+              return description;
+            }
+            return replaceInternalStreamId(pc, description);
+          }
+        });
+
+    window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      // We can not yet check for sender instanceof RTCRtpSender
+      // since we shim RTPSender. So we check if sender._pc is set.
+      if (!sender._pc) {
+        throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' +
+            'does not implement interface RTCRtpSender.', 'TypeError');
+      }
+      var isLocal = sender._pc === pc;
+      if (!isLocal) {
+        throw new DOMException('Sender was not created by this connection.',
+            'InvalidAccessError');
+      }
+
+      // Search for the native stream the senders track belongs to.
+      pc._streams = pc._streams || {};
+      var stream;
+      Object.keys(pc._streams).forEach(function(streamid) {
+        var hasTrack = pc._streams[streamid].getTracks().find(function(track) {
+          return sender.track === track;
+        });
+        if (hasTrack) {
+          stream = pc._streams[streamid];
+        }
+      });
+
+      if (stream) {
+        if (stream.getTracks().length === 1) {
+          // if this is the last track of the stream, remove the stream. This
+          // takes care of any shimmed _senders.
+          pc.removeStream(pc._reverseStreams[stream.id]);
+        } else {
+          // relying on the same odd chrome behaviour as above.
+          stream.removeTrack(sender.track);
+        }
+        pc.dispatchEvent(new Event('negotiationneeded'));
+      }
+    };
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        // Translate iceTransportPolicy to iceTransports,
+        // see https://code.google.com/p/webrtc/issues/detail?id=4869
+        // this was fixed in M56 along with unprefixing RTCPeerConnection.
+        logging('PeerConnection');
+        if (pcConfig && pcConfig.iceTransportPolicy) {
+          pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+        }
+
+        return new window.webkitRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.webkitRTCPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      if (window.webkitRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.webkitRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+    } else {
+      // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+      var OrigPeerConnection = window.RTCPeerConnection;
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (pcConfig && pcConfig.iceServers) {
+          var newIceServers = [];
+          for (var i = 0; i < pcConfig.iceServers.length; i++) {
+            var server = pcConfig.iceServers[i];
+            if (!server.hasOwnProperty('urls') &&
+                server.hasOwnProperty('url')) {
+              utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+              server = JSON.parse(JSON.stringify(server));
+              server.urls = server.url;
+              newIceServers.push(server);
+            } else {
+              newIceServers.push(pcConfig.iceServers[i]);
+            }
+          }
+          pcConfig.iceServers = newIceServers;
+        }
+        return new OrigPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+
+    var origGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(selector,
+        successCallback, errorCallback) {
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats.apply(this, arguments);
+      }
+
+      // When spec-style getStats is supported, return those when called with
+      // either no arguments or the selector argument is null.
+      if (origGetStats.length === 0 && (arguments.length === 0 ||
+          typeof arguments[0] !== 'function')) {
+        return origGetStats.apply(this, []);
+      }
+
+      var fixChromeStats_ = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: {
+              localcandidate: 'local-candidate',
+              remotecandidate: 'remote-candidate'
+            }[report.type] || report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      // shim getStats with maplike support
+      var makeMapStats = function(stats) {
+        return new Map(Object.keys(stats).map(function(key) {
+          return [key, stats[key]];
+        }));
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper_ = function(response) {
+          args[1](makeMapStats(fixChromeStats_(response)));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper_,
+          arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        origGetStats.apply(self, [
+          function(response) {
+            resolve(makeMapStats(fixChromeStats_(response)));
+          }, reject]);
+      }).then(successCallback, errorCallback);
+    };
+
+    // add promise support -- natively available in Chrome 51
+    if (browserDetails.version < 51) {
+      ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+          .forEach(function(method) {
+            var nativeMethod = window.RTCPeerConnection.prototype[method];
+            window.RTCPeerConnection.prototype[method] = function() {
+              var args = arguments;
+              var self = this;
+              var promise = new Promise(function(resolve, reject) {
+                nativeMethod.apply(self, [args[0], resolve, reject]);
+              });
+              if (args.length < 2) {
+                return promise;
+              }
+              return promise.then(function() {
+                args[1].apply(null, []);
+              },
+              function(err) {
+                if (args.length >= 3) {
+                  args[2].apply(null, [err]);
+                }
+              });
+            };
+          });
+    }
+
+    // promise support for createOffer and createAnswer. Available (without
+    // bugs) since M52: crbug/619289
+    if (browserDetails.version < 52) {
+      ['createOffer', 'createAnswer'].forEach(function(method) {
+        var nativeMethod = window.RTCPeerConnection.prototype[method];
+        window.RTCPeerConnection.prototype[method] = function() {
+          var self = this;
+          if (arguments.length < 1 || (arguments.length === 1 &&
+              typeof arguments[0] === 'object')) {
+            var opts = arguments.length === 1 ? arguments[0] : undefined;
+            return new Promise(function(resolve, reject) {
+              nativeMethod.apply(self, [resolve, reject, opts]);
+            });
+          }
+          return nativeMethod.apply(this, arguments);
+        };
+      });
+    }
+
+    // shim implicit creation of RTCSessionDescription/RTCIceCandidate
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+  }
+};
+
+
+// Expose public methods.
+module.exports = {
+  shimMediaStream: chromeShim.shimMediaStream,
+  shimOnTrack: chromeShim.shimOnTrack,
+  shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack,
+  shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf,
+  shimSourceObject: chromeShim.shimSourceObject,
+  shimPeerConnection: chromeShim.shimPeerConnection,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils.js":11,"./getusermedia":6}],6:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+
+  var constraintsToChrome_ = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname_ = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname_('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname_('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname_('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname_('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname_(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  var shimConstraints_ = function(constraints, func) {
+    if (browserDetails.version >= 61) {
+      return func(constraints);
+    }
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (constraints && typeof constraints.audio === 'object') {
+      var remap = function(obj, a, b) {
+        if (a in obj && !(b in obj)) {
+          obj[b] = obj[a];
+          delete obj[a];
+        }
+      };
+      constraints = JSON.parse(JSON.stringify(constraints));
+      remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
+      remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
+      constraints.audio = constraintsToChrome_(constraints.audio);
+    }
+    if (constraints && typeof constraints.video === 'object') {
+      // Shim facingMode for mobile & surface pro.
+      var face = constraints.video.facingMode;
+      face = face && ((typeof face === 'object') ? face : {ideal: face});
+      var getSupportedFacingModeLies = browserDetails.version < 66;
+
+      if ((face && (face.exact === 'user' || face.exact === 'environment' ||
+                    face.ideal === 'user' || face.ideal === 'environment')) &&
+          !(navigator.mediaDevices.getSupportedConstraints &&
+            navigator.mediaDevices.getSupportedConstraints().facingMode &&
+            !getSupportedFacingModeLies)) {
+        delete constraints.video.facingMode;
+        var matches;
+        if (face.exact === 'environment' || face.ideal === 'environment') {
+          matches = ['back', 'rear'];
+        } else if (face.exact === 'user' || face.ideal === 'user') {
+          matches = ['front'];
+        }
+        if (matches) {
+          // Look for matches in label, or use last cam for back (typical).
+          return navigator.mediaDevices.enumerateDevices()
+          .then(function(devices) {
+            devices = devices.filter(function(d) {
+              return d.kind === 'videoinput';
+            });
+            var dev = devices.find(function(d) {
+              return matches.some(function(match) {
+                return d.label.toLowerCase().indexOf(match) !== -1;
+              });
+            });
+            if (!dev && devices.length && matches.indexOf('back') !== -1) {
+              dev = devices[devices.length - 1]; // more likely the back cam
+            }
+            if (dev) {
+              constraints.video.deviceId = face.exact ? {exact: dev.deviceId} :
+                                                        {ideal: dev.deviceId};
+            }
+            constraints.video = constraintsToChrome_(constraints.video);
+            logging('chrome: ' + JSON.stringify(constraints));
+            return func(constraints);
+          });
+        }
+      }
+      constraints.video = constraintsToChrome_(constraints.video);
+    }
+    logging('chrome: ' + JSON.stringify(constraints));
+    return func(constraints);
+  };
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        PermissionDeniedError: 'NotAllowedError',
+        InvalidStateError: 'NotReadableError',
+        DevicesNotFoundError: 'NotFoundError',
+        ConstraintNotSatisfiedError: 'OverconstrainedError',
+        TrackStartError: 'NotReadableError',
+        MediaDeviceFailedDueToShutdown: 'NotReadableError',
+        MediaDeviceKillSwitchOn: 'NotReadableError'
+      }[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraintName,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    shimConstraints_(constraints, function(c) {
+      navigator.webkitGetUserMedia(c, onSuccess, function(e) {
+        if (onError) {
+          onError(shimError_(e));
+        }
+      });
+    });
+  };
+
+  navigator.getUserMedia = getUserMedia_;
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      navigator.getUserMedia(constraints, resolve, reject);
+    });
+  };
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {
+      getUserMedia: getUserMediaPromise_,
+      enumerateDevices: function() {
+        return new Promise(function(resolve) {
+          var kinds = {audio: 'audioinput', video: 'videoinput'};
+          return window.MediaStreamTrack.getSources(function(devices) {
+            resolve(devices.map(function(device) {
+              return {label: device.label,
+                kind: kinds[device.kind],
+                deviceId: device.id,
+                groupId: ''};
+            }));
+          });
+        });
+      },
+      getSupportedConstraints: function() {
+        return {
+          deviceId: true, echoCancellation: true, facingMode: true,
+          frameRate: true, height: true, width: true
+        };
+      }
+    };
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return getUserMediaPromise_(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(cs) {
+      return shimConstraints_(cs, function(c) {
+        return origGetUserMedia(c).then(function(stream) {
+          if (c.audio && !stream.getAudioTracks().length ||
+              c.video && !stream.getVideoTracks().length) {
+            stream.getTracks().forEach(function(track) {
+              track.stop();
+            });
+            throw new DOMException('', 'NotFoundError');
+          }
+          return stream;
+        }, function(e) {
+          return Promise.reject(shimError_(e));
+        });
+      });
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      logging('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      logging('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+};
+
+},{"../utils.js":11}],7:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+var utils = require('./utils');
+
+// Wraps the peerconnection event eventNameToWrap in a function
+// which returns the modified event object.
+function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
+  if (!window.RTCPeerConnection) {
+    return;
+  }
+  var proto = window.RTCPeerConnection.prototype;
+  var nativeAddEventListener = proto.addEventListener;
+  proto.addEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap) {
+      return nativeAddEventListener.apply(this, arguments);
+    }
+    var wrappedCallback = function(e) {
+      cb(wrapper(e));
+    };
+    this._eventMap = this._eventMap || {};
+    this._eventMap[cb] = wrappedCallback;
+    return nativeAddEventListener.apply(this, [nativeEventName,
+      wrappedCallback]);
+  };
+
+  var nativeRemoveEventListener = proto.removeEventListener;
+  proto.removeEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap || !this._eventMap
+        || !this._eventMap[cb]) {
+      return nativeRemoveEventListener.apply(this, arguments);
+    }
+    var unwrappedCb = this._eventMap[cb];
+    delete this._eventMap[cb];
+    return nativeRemoveEventListener.apply(this, [nativeEventName,
+      unwrappedCb]);
+  };
+
+  Object.defineProperty(proto, 'on' + eventNameToWrap, {
+    get: function() {
+      return this['_on' + eventNameToWrap];
+    },
+    set: function(cb) {
+      if (this['_on' + eventNameToWrap]) {
+        this.removeEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap]);
+        delete this['_on' + eventNameToWrap];
+      }
+      if (cb) {
+        this.addEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap] = cb);
+      }
+    }
+  });
+}
+
+module.exports = {
+  shimRTCIceCandidate: function(window) {
+    // foundation is arbitrarily chosen as an indicator for full support for
+    // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
+    if (window.RTCIceCandidate && 'foundation' in
+        window.RTCIceCandidate.prototype) {
+      return;
+    }
+
+    var NativeRTCIceCandidate = window.RTCIceCandidate;
+    window.RTCIceCandidate = function(args) {
+      // Remove the a= which shouldn't be part of the candidate string.
+      if (typeof args === 'object' && args.candidate &&
+          args.candidate.indexOf('a=') === 0) {
+        args = JSON.parse(JSON.stringify(args));
+        args.candidate = args.candidate.substr(2);
+      }
+
+      // Augment the native candidate with the parsed fields.
+      var nativeCandidate = new NativeRTCIceCandidate(args);
+      var parsedCandidate = SDPUtils.parseCandidate(args.candidate);
+      var augmentedCandidate = Object.assign(nativeCandidate,
+          parsedCandidate);
+
+      // Add a serializer that does not serialize the extra attributes.
+      augmentedCandidate.toJSON = function() {
+        return {
+          candidate: augmentedCandidate.candidate,
+          sdpMid: augmentedCandidate.sdpMid,
+          sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
+          usernameFragment: augmentedCandidate.usernameFragment,
+        };
+      };
+      return augmentedCandidate;
+    };
+
+    // Hook up the augmented candidate in onicecandidate and
+    // addEventListener('icecandidate', ...)
+    wrapPeerConnectionEvent(window, 'icecandidate', function(e) {
+      if (e.candidate) {
+        Object.defineProperty(e, 'candidate', {
+          value: new window.RTCIceCandidate(e.candidate),
+          writable: 'false'
+        });
+      }
+      return e;
+    });
+  },
+
+  // shimCreateObjectURL must be called before shimSourceObject to avoid loop.
+
+  shimCreateObjectURL: function(window) {
+    var URL = window && window.URL;
+
+    if (!(typeof window === 'object' && window.HTMLMediaElement &&
+          'srcObject' in window.HTMLMediaElement.prototype &&
+        URL.createObjectURL && URL.revokeObjectURL)) {
+      // Only shim CreateObjectURL using srcObject if srcObject exists.
+      return undefined;
+    }
+
+    var nativeCreateObjectURL = URL.createObjectURL.bind(URL);
+    var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL);
+    var streams = new Map(), newId = 0;
+
+    URL.createObjectURL = function(stream) {
+      if ('getTracks' in stream) {
+        var url = 'polyblob:' + (++newId);
+        streams.set(url, stream);
+        utils.deprecated('URL.createObjectURL(stream)',
+            'elem.srcObject = stream');
+        return url;
+      }
+      return nativeCreateObjectURL(stream);
+    };
+    URL.revokeObjectURL = function(url) {
+      nativeRevokeObjectURL(url);
+      streams.delete(url);
+    };
+
+    var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,
+                                              'src');
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'src', {
+      get: function() {
+        return dsc.get.apply(this);
+      },
+      set: function(url) {
+        this.srcObject = streams.get(url) || null;
+        return dsc.set.apply(this, [url]);
+      }
+    });
+
+    var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute;
+    window.HTMLMediaElement.prototype.setAttribute = function() {
+      if (arguments.length === 2 &&
+          ('' + arguments[0]).toLowerCase() === 'src') {
+        this.srcObject = streams.get(arguments[1]) || null;
+      }
+      return nativeSetAttribute.apply(this, arguments);
+    };
+  }
+};
+
+},{"./utils":11,"sdp":2}],8:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+
+var firefoxShim = {
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+            this.removeEventListener('addstream', this._ontrackpoly);
+          }
+          this.addEventListener('track', this._ontrack = f);
+          this.addEventListener('addstream', this._ontrackpoly = function(e) {
+            e.stream.getTracks().forEach(function(track) {
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = {track: track};
+              event.transceiver = {receiver: event.receiver};
+              event.streams = [e.stream];
+              this.dispatchEvent(event);
+            }.bind(this));
+          }.bind(this));
+        }
+      });
+    }
+    if (typeof window === 'object' && window.RTCTrackEvent &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        !('transceiver' in window.RTCTrackEvent.prototype)) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    // Firefox has supported mozSrcObject since FF22, unprefixed in 42.
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this.mozSrcObject;
+          },
+          set: function(stream) {
+            this.mozSrcObject = stream;
+          }
+        });
+      }
+    }
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (typeof window !== 'object' || !(window.RTCPeerConnection ||
+        window.mozRTCPeerConnection)) {
+      return; // probably media.peerconnection.enabled=false in about:config
+    }
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (browserDetails.version < 38) {
+          // .urls is not supported in FF < 38.
+          // create RTCIceServers with a single url.
+          if (pcConfig && pcConfig.iceServers) {
+            var newIceServers = [];
+            for (var i = 0; i < pcConfig.iceServers.length; i++) {
+              var server = pcConfig.iceServers[i];
+              if (server.hasOwnProperty('urls')) {
+                for (var j = 0; j < server.urls.length; j++) {
+                  var newServer = {
+                    url: server.urls[j]
+                  };
+                  if (server.urls[j].indexOf('turn') === 0) {
+                    newServer.username = server.username;
+                    newServer.credential = server.credential;
+                  }
+                  newIceServers.push(newServer);
+                }
+              } else {
+                newIceServers.push(pcConfig.iceServers[i]);
+              }
+            }
+            pcConfig.iceServers = newIceServers;
+          }
+        }
+        return new window.mozRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.mozRTCPeerConnection.prototype;
+
+      // wrap static methods. Currently just generateCertificate.
+      if (window.mozRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.mozRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+
+      window.RTCSessionDescription = window.mozRTCSessionDescription;
+      window.RTCIceCandidate = window.mozRTCIceCandidate;
+    }
+
+    // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+
+    // shim getStats with maplike support
+    var makeMapStats = function(stats) {
+      var map = new Map();
+      Object.keys(stats).forEach(function(key) {
+        map.set(key, stats[key]);
+        map[key] = stats[key];
+      });
+      return map;
+    };
+
+    var modernStatsTypes = {
+      inboundrtp: 'inbound-rtp',
+      outboundrtp: 'outbound-rtp',
+      candidatepair: 'candidate-pair',
+      localcandidate: 'local-candidate',
+      remotecandidate: 'remote-candidate'
+    };
+
+    var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(
+      selector,
+      onSucc,
+      onErr
+    ) {
+      return nativeGetStats.apply(this, [selector || null])
+        .then(function(stats) {
+          if (browserDetails.version < 48) {
+            stats = makeMapStats(stats);
+          }
+          if (browserDetails.version < 53 && !onSucc) {
+            // Shim only promise getStats with spec-hyphens in type names
+            // Leave callback version alone; misc old uses of forEach before Map
+            try {
+              stats.forEach(function(stat) {
+                stat.type = modernStatsTypes[stat.type] || stat.type;
+              });
+            } catch (e) {
+              if (e.name !== 'TypeError') {
+                throw e;
+              }
+              // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
+              stats.forEach(function(stat, i) {
+                stats.set(i, Object.assign({}, stat, {
+                  type: modernStatsTypes[stat.type] || stat.type
+                }));
+              });
+            }
+          }
+          return stats;
+        })
+        .then(onSucc, onErr);
+    };
+  },
+
+  shimRemoveStream: function(window) {
+    if (!window.RTCPeerConnection ||
+        'removeStream' in window.RTCPeerConnection.prototype) {
+      return;
+    }
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      utils.deprecated('removeStream', 'removeTrack');
+      this.getSenders().forEach(function(sender) {
+        if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) {
+          pc.removeTrack(sender);
+        }
+      });
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimOnTrack: firefoxShim.shimOnTrack,
+  shimSourceObject: firefoxShim.shimSourceObject,
+  shimPeerConnection: firefoxShim.shimPeerConnection,
+  shimRemoveStream: firefoxShim.shimRemoveStream,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils":11,"./getusermedia":9}],9:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+  var MediaStreamTrack = window && window.MediaStreamTrack;
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        InternalError: 'NotReadableError',
+        NotSupportedError: 'TypeError',
+        PermissionDeniedError: 'NotAllowedError',
+        SecurityError: 'NotAllowedError'
+      }[e.name] || e.name,
+      message: {
+        'The operation is insecure.': 'The request is not allowed by the ' +
+        'user agent or the platform in the current context.'
+      }[e.message] || e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  // getUserMedia constraints shim.
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    var constraintsToFF37_ = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r. min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (browserDetails.version < 38) {
+      logging('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37_(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37_(constraints.video);
+      }
+      logging('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
+      onError(shimError_(e));
+    });
+  };
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      getUserMedia_(constraints, resolve, reject);
+    });
+  };
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+        return new Promise(function(resolve) {
+          var infos = [
+            {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+            {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+          ];
+          resolve(infos);
+        });
+      };
+
+  if (browserDetails.version < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+  if (browserDetails.version < 49) {
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      return origGetUserMedia(c).then(function(stream) {
+        // Work around https://bugzil.la/802326
+        if (c.audio && !stream.getAudioTracks().length ||
+            c.video && !stream.getVideoTracks().length) {
+          stream.getTracks().forEach(function(track) {
+            track.stop();
+          });
+          throw new DOMException('The object can not be found here.',
+                                 'NotFoundError');
+        }
+        return stream;
+      }, function(e) {
+        return Promise.reject(shimError_(e));
+      });
+    };
+  }
+  if (!(browserDetails.version > 55 &&
+      'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
+    var remap = function(obj, a, b) {
+      if (a in obj && !(b in obj)) {
+        obj[b] = obj[a];
+        delete obj[a];
+      }
+    };
+
+    var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      if (typeof c === 'object' && typeof c.audio === 'object') {
+        c = JSON.parse(JSON.stringify(c));
+        remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
+        remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
+      }
+      return nativeGetUserMedia(c);
+    };
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
+      var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
+      MediaStreamTrack.prototype.getSettings = function() {
+        var obj = nativeGetSettings.apply(this, arguments);
+        remap(obj, 'mozAutoGainControl', 'autoGainControl');
+        remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
+        return obj;
+      };
+    }
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
+      var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
+      MediaStreamTrack.prototype.applyConstraints = function(c) {
+        if (this.kind === 'audio' && typeof c === 'object') {
+          c = JSON.parse(JSON.stringify(c));
+          remap(c, 'autoGainControl', 'mozAutoGainControl');
+          remap(c, 'noiseSuppression', 'mozNoiseSuppression');
+        }
+        return nativeApplyConstraints.apply(this, [c]);
+      };
+    }
+  }
+  navigator.getUserMedia = function(constraints, onSuccess, onError) {
+    if (browserDetails.version < 44) {
+      return getUserMedia_(constraints, onSuccess, onError);
+    }
+    // Replace Firefox 44+'s deprecation warning with unprefixed version.
+    utils.deprecated('navigator.getUserMedia',
+        'navigator.mediaDevices.getUserMedia');
+    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
+  };
+};
+
+},{"../utils":11}],10:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+'use strict';
+var utils = require('../utils');
+
+var safariShim = {
+  // TODO: DrAlex, should be here, double check against LayoutTests
+
+  // TODO: once the back-end for the mac port is done, add.
+  // TODO: check for webkitGTK+
+  // shimPeerConnection: function() { },
+
+  shimLocalStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getLocalStreams = function() {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        return this._localStreams;
+      };
+    }
+    if (!('getStreamById' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getStreamById = function(id) {
+        var result = null;
+        if (this._localStreams) {
+          this._localStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        if (this._remoteStreams) {
+          this._remoteStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        return result;
+      };
+    }
+    if (!('addStream' in window.RTCPeerConnection.prototype)) {
+      var _addTrack = window.RTCPeerConnection.prototype.addTrack;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        if (this._localStreams.indexOf(stream) === -1) {
+          this._localStreams.push(stream);
+        }
+        var self = this;
+        stream.getTracks().forEach(function(track) {
+          _addTrack.call(self, track, stream);
+        });
+      };
+
+      window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+        if (stream) {
+          if (!this._localStreams) {
+            this._localStreams = [stream];
+          } else if (this._localStreams.indexOf(stream) === -1) {
+            this._localStreams.push(stream);
+          }
+        }
+        return _addTrack.call(this, track, stream);
+      };
+    }
+    if (!('removeStream' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        var index = this._localStreams.indexOf(stream);
+        if (index === -1) {
+          return;
+        }
+        this._localStreams.splice(index, 1);
+        var self = this;
+        var tracks = stream.getTracks();
+        this.getSenders().forEach(function(sender) {
+          if (tracks.indexOf(sender.track) !== -1) {
+            self.removeTrack(sender);
+          }
+        });
+      };
+    }
+  },
+  shimRemoteStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getRemoteStreams = function() {
+        return this._remoteStreams ? this._remoteStreams : [];
+      };
+    }
+    if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
+        get: function() {
+          return this._onaddstream;
+        },
+        set: function(f) {
+          if (this._onaddstream) {
+            this.removeEventListener('addstream', this._onaddstream);
+            this.removeEventListener('track', this._onaddstreampoly);
+          }
+          this.addEventListener('addstream', this._onaddstream = f);
+          this.addEventListener('track', this._onaddstreampoly = function(e) {
+            var stream = e.streams[0];
+            if (!this._remoteStreams) {
+              this._remoteStreams = [];
+            }
+            if (this._remoteStreams.indexOf(stream) >= 0) {
+              return;
+            }
+            this._remoteStreams.push(stream);
+            var event = new Event('addstream');
+            event.stream = e.streams[0];
+            this.dispatchEvent(event);
+          }.bind(this));
+        }
+      });
+    }
+  },
+  shimCallbacksAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    var prototype = window.RTCPeerConnection.prototype;
+    var createOffer = prototype.createOffer;
+    var createAnswer = prototype.createAnswer;
+    var setLocalDescription = prototype.setLocalDescription;
+    var setRemoteDescription = prototype.setRemoteDescription;
+    var addIceCandidate = prototype.addIceCandidate;
+
+    prototype.createOffer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createOffer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    prototype.createAnswer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createAnswer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    var withCallback = function(description, successCallback, failureCallback) {
+      var promise = setLocalDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setLocalDescription = withCallback;
+
+    withCallback = function(description, successCallback, failureCallback) {
+      var promise = setRemoteDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setRemoteDescription = withCallback;
+
+    withCallback = function(candidate, successCallback, failureCallback) {
+      var promise = addIceCandidate.apply(this, [candidate]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.addIceCandidate = withCallback;
+  },
+  shimGetUserMedia: function(window) {
+    var navigator = window && window.navigator;
+
+    if (!navigator.getUserMedia) {
+      if (navigator.webkitGetUserMedia) {
+        navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+      } else if (navigator.mediaDevices &&
+          navigator.mediaDevices.getUserMedia) {
+        navigator.getUserMedia = function(constraints, cb, errcb) {
+          navigator.mediaDevices.getUserMedia(constraints)
+          .then(cb, errcb);
+        }.bind(navigator);
+      }
+    }
+  },
+  shimRTCIceServerUrls: function(window) {
+    // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+    var OrigPeerConnection = window.RTCPeerConnection;
+    window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (!server.hasOwnProperty('urls') &&
+              server.hasOwnProperty('url')) {
+            utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+            server = JSON.parse(JSON.stringify(server));
+            server.urls = server.url;
+            delete server.url;
+            newIceServers.push(server);
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+      return new OrigPeerConnection(pcConfig, pcConstraints);
+    };
+    window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+    // wrap static methods. Currently just generateCertificate.
+    if ('generateCertificate' in window.RTCPeerConnection) {
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+  },
+  shimTrackEventTransceiver: function(window) {
+    // Add event.transceiver member over deprecated event.receiver
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is
+        // defined for some reason even when window.RTCTransceiver is not.
+        !window.RTCTransceiver) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimCreateOfferLegacy: function(window) {
+    var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
+    window.RTCPeerConnection.prototype.createOffer = function(offerOptions) {
+      var pc = this;
+      if (offerOptions) {
+        var audioTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'audio';
+        });
+        if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
+          if (audioTransceiver.direction === 'sendrecv') {
+            audioTransceiver.setDirection('sendonly');
+          } else if (audioTransceiver.direction === 'recvonly') {
+            audioTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveAudio === true &&
+            !audioTransceiver) {
+          pc.addTransceiver('audio');
+        }
+
+        var videoTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'video';
+        });
+        if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
+          if (videoTransceiver.direction === 'sendrecv') {
+            videoTransceiver.setDirection('sendonly');
+          } else if (videoTransceiver.direction === 'recvonly') {
+            videoTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveVideo === true &&
+            !videoTransceiver) {
+          pc.addTransceiver('video');
+        }
+      }
+      return origCreateOffer.apply(pc, arguments);
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimCallbacksAPI: safariShim.shimCallbacksAPI,
+  shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI,
+  shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI,
+  shimGetUserMedia: safariShim.shimGetUserMedia,
+  shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls,
+  shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver,
+  shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy
+  // TODO
+  // shimPeerConnection: safariShim.shimPeerConnection
+};
+
+},{"../utils":11}],11:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var logDisabled_ = true;
+var deprecationWarnings_ = true;
+
+// Utility methods.
+var utils = {
+  disableLog: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    logDisabled_ = bool;
+    return (bool) ? 'adapter.js logging disabled' :
+        'adapter.js logging enabled';
+  },
+
+  /**
+   * Disable or enable deprecation warnings
+   * @param {!boolean} bool set to true to disable warnings.
+   */
+  disableWarnings: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    deprecationWarnings_ = !bool;
+    return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
+  },
+
+  log: function() {
+    if (typeof window === 'object') {
+      if (logDisabled_) {
+        return;
+      }
+      if (typeof console !== 'undefined' && typeof console.log === 'function') {
+        console.log.apply(console, arguments);
+      }
+    }
+  },
+
+  /**
+   * Shows a deprecation warning suggesting the modern and spec-compatible API.
+   */
+  deprecated: function(oldMethod, newMethod) {
+    if (!deprecationWarnings_) {
+      return;
+    }
+    console.warn(oldMethod + ' is deprecated, please use ' + newMethod +
+        ' instead.');
+  },
+
+  /**
+   * Extract browser version out of the provided user agent string.
+   *
+   * @param {!string} uastring userAgent string.
+   * @param {!string} expr Regular expression used as match criteria.
+   * @param {!number} pos position in the version string to be returned.
+   * @return {!number} browser version.
+   */
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos], 10);
+  },
+
+  /**
+   * Browser detector.
+   *
+   * @return {object} result containing browser and version
+   *     properties.
+   */
+  detectBrowser: function(window) {
+    var navigator = window && window.navigator;
+
+    // Returned result object.
+    var result = {};
+    result.browser = null;
+    result.version = null;
+
+    // Fail early if it's not a browser
+    if (typeof window === 'undefined' || !window.navigator) {
+      result.browser = 'Not a browser.';
+      return result;
+    }
+
+    // Firefox.
+    if (navigator.mozGetUserMedia) {
+      result.browser = 'firefox';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Firefox\/(\d+)\./, 1);
+    } else if (navigator.webkitGetUserMedia) {
+      // Chrome, Chromium, Webview, Opera, all use the chrome shim for now
+      if (window.webkitRTCPeerConnection) {
+        result.browser = 'chrome';
+        result.version = this.extractVersion(navigator.userAgent,
+          /Chrom(e|ium)\/(\d+)\./, 2);
+      } else { // Safari (in an unpublished version) or unknown webkit-based.
+        if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
+          result.browser = 'safari';
+          result.version = this.extractVersion(navigator.userAgent,
+            /AppleWebKit\/(\d+)\./, 1);
+        } else { // unknown webkit-based browser.
+          result.browser = 'Unsupported webkit-based browser ' +
+              'with GUM support but no WebRTC support.';
+          return result;
+        }
+      }
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
+      result.browser = 'edge';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Edge\/(\d+).(\d+)$/, 2);
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
+        // Safari, with webkitGetUserMedia removed.
+      result.browser = 'safari';
+      result.version = this.extractVersion(navigator.userAgent,
+          /AppleWebKit\/(\d+)\./, 1);
+    } else { // Default fallthrough: not supported.
+      result.browser = 'Not a supported browser.';
+      return result;
+    }
+
+    return result;
+  },
+
+};
+
+// Export.
+module.exports = {
+  log: utils.log,
+  deprecated: utils.deprecated,
+  disableLog: utils.disableLog,
+  disableWarnings: utils.disableWarnings,
+  extractVersion: utils.extractVersion,
+  shimCreateObjectURL: utils.shimCreateObjectURL,
+  detectBrowser: utils.detectBrowser.bind(utils)
+};
+
+},{}]},{},[3])(3)
+});

+ 2794 - 0
support/client/lib/vwf/view/webrtc/dist/adapter_no_edge_no_global.js

@@ -0,0 +1,2794 @@
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+
+},{}],2:[function(require,module,exports){
+ /* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+  return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+  return blob.trim().split('\n').map(function(line) {
+    return line.trim();
+  });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+  var parts = blob.split('\nm=');
+  return parts.map(function(part, index) {
+    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+  });
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+  return SDPUtils.splitLines(blob).filter(function(line) {
+    return line.indexOf(prefix) === 0;
+  });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+  var parts;
+  // Parse both variants.
+  if (line.indexOf('a=candidate:') === 0) {
+    parts = line.substring(12).split(' ');
+  } else {
+    parts = line.substring(10).split(' ');
+  }
+
+  var candidate = {
+    foundation: parts[0],
+    component: parseInt(parts[1], 10),
+    protocol: parts[2].toLowerCase(),
+    priority: parseInt(parts[3], 10),
+    ip: parts[4],
+    port: parseInt(parts[5], 10),
+    // skip parts[6] == 'typ'
+    type: parts[7]
+  };
+
+  for (var i = 8; i < parts.length; i += 2) {
+    switch (parts[i]) {
+      case 'raddr':
+        candidate.relatedAddress = parts[i + 1];
+        break;
+      case 'rport':
+        candidate.relatedPort = parseInt(parts[i + 1], 10);
+        break;
+      case 'tcptype':
+        candidate.tcpType = parts[i + 1];
+        break;
+      case 'ufrag':
+        candidate.ufrag = parts[i + 1]; // for backward compability.
+        candidate.usernameFragment = parts[i + 1];
+        break;
+      default: // extension handling, in particular ufrag
+        candidate[parts[i]] = parts[i + 1];
+        break;
+    }
+  }
+  return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+  var sdp = [];
+  sdp.push(candidate.foundation);
+  sdp.push(candidate.component);
+  sdp.push(candidate.protocol.toUpperCase());
+  sdp.push(candidate.priority);
+  sdp.push(candidate.ip);
+  sdp.push(candidate.port);
+
+  var type = candidate.type;
+  sdp.push('typ');
+  sdp.push(type);
+  if (type !== 'host' && candidate.relatedAddress &&
+      candidate.relatedPort) {
+    sdp.push('raddr');
+    sdp.push(candidate.relatedAddress); // was: relAddr
+    sdp.push('rport');
+    sdp.push(candidate.relatedPort); // was: relPort
+  }
+  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+    sdp.push('tcptype');
+    sdp.push(candidate.tcpType);
+  }
+  if (candidate.ufrag) {
+    sdp.push('ufrag');
+    sdp.push(candidate.ufrag);
+  }
+  return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+  return line.substr(14).split(' ');
+}
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+  var parts = line.substr(9).split(' ');
+  var parsed = {
+    payloadType: parseInt(parts.shift(), 10) // was: id
+  };
+
+  parts = parts[0].split('/');
+
+  parsed.name = parts[0];
+  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+  // was: channels
+  parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+  return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+      (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+  var parts = line.substr(9).split(' ');
+  return {
+    id: parseInt(parts[0], 10),
+    direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+    uri: parts[1]
+  };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+      (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+          ? '/' + headerExtension.direction
+          : '') +
+      ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+  var parsed = {};
+  var kv;
+  var parts = line.substr(line.indexOf(' ') + 1).split(';');
+  for (var j = 0; j < parts.length; j++) {
+    kv = parts[j].trim().split('=');
+    parsed[kv[0].trim()] = kv[1];
+  }
+  return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+  var line = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.parameters && Object.keys(codec.parameters).length) {
+    var params = [];
+    Object.keys(codec.parameters).forEach(function(param) {
+      params.push(param + '=' + codec.parameters[param]);
+    });
+    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+  }
+  return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+  return {
+    type: parts.shift(),
+    parameter: parts.join(' ')
+  };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+  var lines = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+    // FIXME: special handling for trr-int?
+    codec.rtcpFeedback.forEach(function(fb) {
+      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+      (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+          '\r\n';
+    });
+  }
+  return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+  var sp = line.indexOf(' ');
+  var parts = {
+    ssrc: parseInt(line.substr(7, sp - 7), 10)
+  };
+  var colon = line.indexOf(':', sp);
+  if (colon > -1) {
+    parts.attribute = line.substr(sp + 1, colon - sp - 1);
+    parts.value = line.substr(colon + 1);
+  } else {
+    parts.attribute = line.substr(sp + 1);
+  }
+  return parts;
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+  var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+  if (mid) {
+    return mid.substr(6);
+  }
+}
+
+SDPUtils.parseFingerprint = function(line) {
+  var parts = line.substr(14).split(' ');
+  return {
+    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+    value: parts[1]
+  };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+      'a=fingerprint:');
+  // Note: a=setup line is ignored since we use the 'auto' role.
+  // Note2: 'algorithm' is not case sensitive except in Edge.
+  return {
+    role: 'auto',
+    fingerprints: lines.map(SDPUtils.parseFingerprint)
+  };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+  var sdp = 'a=setup:' + setupType + '\r\n';
+  params.fingerprints.forEach(function(fp) {
+    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+  });
+  return sdp;
+};
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var iceParameters = {
+    usernameFragment: lines.filter(function(line) {
+      return line.indexOf('a=ice-ufrag:') === 0;
+    })[0].substr(12),
+    password: lines.filter(function(line) {
+      return line.indexOf('a=ice-pwd:') === 0;
+    })[0].substr(10)
+  };
+  return iceParameters;
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+      'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+  var description = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: [],
+    rtcp: []
+  };
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+    var pt = mline[i];
+    var rtpmapline = SDPUtils.matchPrefix(
+        mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+    if (rtpmapline) {
+      var codec = SDPUtils.parseRtpMap(rtpmapline);
+      var fmtps = SDPUtils.matchPrefix(
+          mediaSection, 'a=fmtp:' + pt + ' ');
+      // Only the first a=fmtp:<pt> is considered.
+      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+      codec.rtcpFeedback = SDPUtils.matchPrefix(
+          mediaSection, 'a=rtcp-fb:' + pt + ' ')
+        .map(SDPUtils.parseRtcpFb);
+      description.codecs.push(codec);
+      // parse FEC mechanisms from rtpmap lines.
+      switch (codec.name.toUpperCase()) {
+        case 'RED':
+        case 'ULPFEC':
+          description.fecMechanisms.push(codec.name.toUpperCase());
+          break;
+        default: // only RED and ULPFEC are recognized as FEC mechanisms.
+          break;
+      }
+    }
+  }
+  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+    description.headerExtensions.push(SDPUtils.parseExtmap(line));
+  });
+  // FIXME: parse rtcp.
+  return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+  var sdp = '';
+
+  // Build the mline.
+  sdp += 'm=' + kind + ' ';
+  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+  sdp += ' UDP/TLS/RTP/SAVPF ';
+  sdp += caps.codecs.map(function(codec) {
+    if (codec.preferredPayloadType !== undefined) {
+      return codec.preferredPayloadType;
+    }
+    return codec.payloadType;
+  }).join(' ') + '\r\n';
+
+  sdp += 'c=IN IP4 0.0.0.0\r\n';
+  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+  caps.codecs.forEach(function(codec) {
+    sdp += SDPUtils.writeRtpMap(codec);
+    sdp += SDPUtils.writeFmtp(codec);
+    sdp += SDPUtils.writeRtcpFb(codec);
+  });
+  var maxptime = 0;
+  caps.codecs.forEach(function(codec) {
+    if (codec.maxptime > maxptime) {
+      maxptime = codec.maxptime;
+    }
+  });
+  if (maxptime > 0) {
+    sdp += 'a=maxptime:' + maxptime + '\r\n';
+  }
+  sdp += 'a=rtcp-mux\r\n';
+
+  caps.headerExtensions.forEach(function(extension) {
+    sdp += SDPUtils.writeExtmap(extension);
+  });
+  // FIXME: write fecMechanisms.
+  return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+  var encodingParameters = [];
+  var description = SDPUtils.parseRtpParameters(mediaSection);
+  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+  // filter a=ssrc:... cname:, ignore PlanB-msid
+  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'cname';
+  });
+  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+  var secondarySsrc;
+
+  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+  .map(function(line) {
+    var parts = line.split(' ');
+    parts.shift();
+    return parts.map(function(part) {
+      return parseInt(part, 10);
+    });
+  });
+  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+    secondarySsrc = flows[0][1];
+  }
+
+  description.codecs.forEach(function(codec) {
+    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+      var encParam = {
+        ssrc: primarySsrc,
+        codecPayloadType: parseInt(codec.parameters.apt, 10),
+        rtx: {
+          ssrc: secondarySsrc
+        }
+      };
+      encodingParameters.push(encParam);
+      if (hasRed) {
+        encParam = JSON.parse(JSON.stringify(encParam));
+        encParam.fec = {
+          ssrc: secondarySsrc,
+          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+        };
+        encodingParameters.push(encParam);
+      }
+    }
+  });
+  if (encodingParameters.length === 0 && primarySsrc) {
+    encodingParameters.push({
+      ssrc: primarySsrc
+    });
+  }
+
+  // we support both b=AS and b=TIAS but interpret AS as TIAS.
+  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+  if (bandwidth.length) {
+    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(7), 10);
+    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+      // use formula from JSEP to convert b=AS to TIAS value.
+      bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+          - (50 * 40 * 8);
+    } else {
+      bandwidth = undefined;
+    }
+    encodingParameters.forEach(function(params) {
+      params.maxBitrate = bandwidth;
+    });
+  }
+  return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+  var rtcpParameters = {};
+
+  var cname;
+  // Gets the first SSRC. Note that with RTX there might be multiple
+  // SSRCs.
+  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+      .map(function(line) {
+        return SDPUtils.parseSsrcMedia(line);
+      })
+      .filter(function(obj) {
+        return obj.attribute === 'cname';
+      })[0];
+  if (remoteSsrc) {
+    rtcpParameters.cname = remoteSsrc.value;
+    rtcpParameters.ssrc = remoteSsrc.ssrc;
+  }
+
+  // Edge uses the compound attribute instead of reducedSize
+  // compound is !reducedSize
+  var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+  rtcpParameters.reducedSize = rsize.length > 0;
+  rtcpParameters.compound = rsize.length === 0;
+
+  // parses the rtcp-mux attrіbute.
+  // Note that Edge does not support unmuxed RTCP.
+  var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+  rtcpParameters.mux = mux.length > 0;
+
+  return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+  var parts;
+  var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+  if (spec.length === 1) {
+    parts = spec[0].substr(7).split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+  var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'msid';
+  });
+  if (planB.length > 0) {
+    parts = planB[0].value.split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+  return Math.random().toString().substr(2, 21);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) {
+  var sessionId;
+  var version = sessVer !== undefined ? sessVer : 2;
+  if (sessId) {
+    sessionId = sessId;
+  } else {
+    sessionId = SDPUtils.generateSessionId();
+  }
+  // FIXME: sess-id should be an NTP timestamp.
+  return 'v=0\r\n' +
+      'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' +
+      's=-\r\n' +
+      't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  var lines = SDPUtils.splitLines(mediaSection);
+  for (var i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substr(2);
+      default:
+        // FIXME: What should happen here?
+    }
+  }
+  if (sessionpart) {
+    return SDPUtils.getDirection(sessionpart);
+  }
+  return 'sendrecv';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+  return mediaSection.split(' ', 2)[1] === '0';
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return {
+    kind: mline[0].substr(2),
+    port: parseInt(mline[1], 10),
+    protocol: mline[2],
+    fmt: mline.slice(3).join(' ')
+  };
+};
+
+// Expose public methods.
+if (typeof module === 'object') {
+  module.exports = SDPUtils;
+}
+
+},{}],3:[function(require,module,exports){
+(function (global){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var adapterFactory = require('./adapter_factory.js');
+module.exports = adapterFactory({window: global.window});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./adapter_factory.js":4}],4:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var utils = require('./utils');
+// Shimming starts here.
+module.exports = function(dependencies, opts) {
+  var window = dependencies && dependencies.window;
+
+  var options = {
+    shimChrome: true,
+    shimFirefox: true,
+    shimEdge: true,
+    shimSafari: true,
+  };
+
+  for (var key in opts) {
+    if (hasOwnProperty.call(opts, key)) {
+      options[key] = opts[key];
+    }
+  }
+
+  // Utils.
+  var logging = utils.log;
+  var browserDetails = utils.detectBrowser(window);
+
+  // Export to the adapter global object visible in the browser.
+  var adapter = {
+    browserDetails: browserDetails,
+    extractVersion: utils.extractVersion,
+    disableLog: utils.disableLog,
+    disableWarnings: utils.disableWarnings
+  };
+
+  // Uncomment the line below if you want logging to occur, including logging
+  // for the switch statement below. Can also be turned on in the browser via
+  // adapter.disableLog(false), but then logging from the switch statement below
+  // will not appear.
+  // require('./utils').disableLog(false);
+
+  // Browser shims.
+  var chromeShim = require('./chrome/chrome_shim') || null;
+  var edgeShim = require('./edge/edge_shim') || null;
+  var firefoxShim = require('./firefox/firefox_shim') || null;
+  var safariShim = require('./safari/safari_shim') || null;
+  var commonShim = require('./common_shim') || null;
+
+  // Shim browser if found.
+  switch (browserDetails.browser) {
+    case 'chrome':
+      if (!chromeShim || !chromeShim.shimPeerConnection ||
+          !options.shimChrome) {
+        logging('Chrome shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming chrome.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = chromeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      chromeShim.shimGetUserMedia(window);
+      chromeShim.shimMediaStream(window);
+      chromeShim.shimSourceObject(window);
+      chromeShim.shimPeerConnection(window);
+      chromeShim.shimOnTrack(window);
+      chromeShim.shimAddTrackRemoveTrack(window);
+      chromeShim.shimGetSendersWithDtmf(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'firefox':
+      if (!firefoxShim || !firefoxShim.shimPeerConnection ||
+          !options.shimFirefox) {
+        logging('Firefox shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming firefox.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = firefoxShim;
+      commonShim.shimCreateObjectURL(window);
+
+      firefoxShim.shimGetUserMedia(window);
+      firefoxShim.shimSourceObject(window);
+      firefoxShim.shimPeerConnection(window);
+      firefoxShim.shimOnTrack(window);
+      firefoxShim.shimRemoveStream(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'edge':
+      if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
+        logging('MS edge shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming edge.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = edgeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      edgeShim.shimGetUserMedia(window);
+      edgeShim.shimPeerConnection(window);
+      edgeShim.shimReplaceTrack(window);
+
+      // the edge shim implements the full RTCIceCandidate object.
+      break;
+    case 'safari':
+      if (!safariShim || !options.shimSafari) {
+        logging('Safari shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming safari.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = safariShim;
+      commonShim.shimCreateObjectURL(window);
+
+      safariShim.shimRTCIceServerUrls(window);
+      safariShim.shimCallbacksAPI(window);
+      safariShim.shimLocalStreamsAPI(window);
+      safariShim.shimRemoteStreamsAPI(window);
+      safariShim.shimTrackEventTransceiver(window);
+      safariShim.shimGetUserMedia(window);
+      safariShim.shimCreateOfferLegacy(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    default:
+      logging('Unsupported browser!');
+      break;
+  }
+
+  return adapter;
+};
+
+},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":1,"./firefox/firefox_shim":8,"./safari/safari_shim":10,"./utils":11}],5:[function(require,module,exports){
+
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+var chromeShim = {
+  shimMediaStream: function(window) {
+    window.MediaStream = window.MediaStream || window.webkitMediaStream;
+  },
+
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+          }
+          this.addEventListener('track', this._ontrack = f);
+        }
+      });
+      var origSetRemoteDescription =
+          window.RTCPeerConnection.prototype.setRemoteDescription;
+      window.RTCPeerConnection.prototype.setRemoteDescription = function() {
+        var pc = this;
+        if (!pc._ontrackpoly) {
+          pc._ontrackpoly = function(e) {
+            // onaddstream does not fire when a track is added to an existing
+            // stream. But stream.onaddtrack is implemented so we use that.
+            e.stream.addEventListener('addtrack', function(te) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === te.track.id;
+                });
+              } else {
+                receiver = {track: te.track};
+              }
+
+              var event = new Event('track');
+              event.track = te.track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+            e.stream.getTracks().forEach(function(track) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === track.id;
+                });
+              } else {
+                receiver = {track: track};
+              }
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+          };
+          pc.addEventListener('addstream', pc._ontrackpoly);
+        }
+        return origSetRemoteDescription.apply(pc, arguments);
+      };
+    }
+  },
+
+  shimGetSendersWithDtmf: function(window) {
+    // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        !('getSenders' in window.RTCPeerConnection.prototype) &&
+        'createDTMFSender' in window.RTCPeerConnection.prototype) {
+      var shimSenderWithDtmf = function(pc, track) {
+        return {
+          track: track,
+          get dtmf() {
+            if (this._dtmf === undefined) {
+              if (track.kind === 'audio') {
+                this._dtmf = pc.createDTMFSender(track);
+              } else {
+                this._dtmf = null;
+              }
+            }
+            return this._dtmf;
+          },
+          _pc: pc
+        };
+      };
+
+      // augment addTrack when getSenders is not available.
+      if (!window.RTCPeerConnection.prototype.getSenders) {
+        window.RTCPeerConnection.prototype.getSenders = function() {
+          this._senders = this._senders || [];
+          return this._senders.slice(); // return a copy of the internal state.
+        };
+        var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
+        window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+          var pc = this;
+          var sender = origAddTrack.apply(pc, arguments);
+          if (!sender) {
+            sender = shimSenderWithDtmf(pc, track);
+            pc._senders.push(sender);
+          }
+          return sender;
+        };
+
+        var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
+        window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+          var pc = this;
+          origRemoveTrack.apply(pc, arguments);
+          var idx = pc._senders.indexOf(sender);
+          if (idx !== -1) {
+            pc._senders.splice(idx, 1);
+          }
+        };
+      }
+      var origAddStream = window.RTCPeerConnection.prototype.addStream;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origAddStream.apply(pc, [stream]);
+        stream.getTracks().forEach(function(track) {
+          pc._senders.push(shimSenderWithDtmf(pc, track));
+        });
+      };
+
+      var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origRemoveStream.apply(pc, [stream]);
+
+        stream.getTracks().forEach(function(track) {
+          var sender = pc._senders.find(function(s) {
+            return s.track === track;
+          });
+          if (sender) {
+            pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender
+          }
+        });
+      };
+    } else if (typeof window === 'object' && window.RTCPeerConnection &&
+               'getSenders' in window.RTCPeerConnection.prototype &&
+               'createDTMFSender' in window.RTCPeerConnection.prototype &&
+               window.RTCRtpSender &&
+               !('dtmf' in window.RTCRtpSender.prototype)) {
+      var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
+      window.RTCPeerConnection.prototype.getSenders = function() {
+        var pc = this;
+        var senders = origGetSenders.apply(pc, []);
+        senders.forEach(function(sender) {
+          sender._pc = pc;
+        });
+        return senders;
+      };
+
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = this._pc.createDTMFSender(this.track);
+            } else {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    var URL = window && window.URL;
+
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this._srcObject;
+          },
+          set: function(stream) {
+            var self = this;
+            // Use _srcObject as a private property for this shim
+            this._srcObject = stream;
+            if (this.src) {
+              URL.revokeObjectURL(this.src);
+            }
+
+            if (!stream) {
+              this.src = '';
+              return undefined;
+            }
+            this.src = URL.createObjectURL(stream);
+            // We need to recreate the blob url when a track is added or
+            // removed. Doing it manually since we want to avoid a recursion.
+            stream.addEventListener('addtrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+            stream.addEventListener('removetrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+          }
+        });
+      }
+    }
+  },
+
+  shimAddTrackRemoveTrack: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+    // shim addTrack and removeTrack.
+    if (window.RTCPeerConnection.prototype.addTrack &&
+        browserDetails.version >= 64) {
+      return;
+    }
+
+    // also shim pc.getLocalStreams when addTrack is shimmed
+    // to return the original streams.
+    var origGetLocalStreams = window.RTCPeerConnection.prototype
+        .getLocalStreams;
+    window.RTCPeerConnection.prototype.getLocalStreams = function() {
+      var self = this;
+      var nativeStreams = origGetLocalStreams.apply(this);
+      self._reverseStreams = self._reverseStreams || {};
+      return nativeStreams.map(function(stream) {
+        return self._reverseStreams[stream.id];
+      });
+    };
+
+    var origAddStream = window.RTCPeerConnection.prototype.addStream;
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      stream.getTracks().forEach(function(track) {
+        var alreadyExists = pc.getSenders().find(function(s) {
+          return s.track === track;
+        });
+        if (alreadyExists) {
+          throw new DOMException('Track already exists.',
+              'InvalidAccessError');
+        }
+      });
+      // Add identity mapping for consistency with addTrack.
+      // Unless this is being used with a stream from addTrack.
+      if (!pc._reverseStreams[stream.id]) {
+        var newStream = new window.MediaStream(stream.getTracks());
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        stream = newStream;
+      }
+      origAddStream.apply(pc, [stream]);
+    };
+
+    var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+      delete pc._reverseStreams[(pc._streams[stream.id] ?
+          pc._streams[stream.id].id : stream.id)];
+      delete pc._streams[stream.id];
+    };
+
+    window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      var streams = [].slice.call(arguments, 1);
+      if (streams.length !== 1 ||
+          !streams[0].getTracks().find(function(t) {
+            return t === track;
+          })) {
+        // this is not fully correct but all we can manage without
+        // [[associated MediaStreams]] internal slot.
+        throw new DOMException(
+          'The adapter.js addTrack polyfill only supports a single ' +
+          ' stream which is associated with the specified track.',
+          'NotSupportedError');
+      }
+
+      var alreadyExists = pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+      if (alreadyExists) {
+        throw new DOMException('Track already exists.',
+            'InvalidAccessError');
+      }
+
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+      var oldStream = pc._streams[stream.id];
+      if (oldStream) {
+        // this is using odd Chrome behaviour, use with caution:
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
+        // Note: we rely on the high-level addTrack/dtmf shim to
+        // create the sender with a dtmf sender.
+        oldStream.addTrack(track);
+
+        // Trigger ONN async.
+        Promise.resolve().then(function() {
+          pc.dispatchEvent(new Event('negotiationneeded'));
+        });
+      } else {
+        var newStream = new window.MediaStream([track]);
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        pc.addStream(newStream);
+      }
+      return pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+    };
+
+    // replace the internal stream id with the external one and
+    // vice versa.
+    function replaceInternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(internalStream.id, 'g'),
+            externalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    function replaceExternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(externalStream.id, 'g'),
+            internalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    ['createOffer', 'createAnswer'].forEach(function(method) {
+      var nativeMethod = window.RTCPeerConnection.prototype[method];
+      window.RTCPeerConnection.prototype[method] = function() {
+        var pc = this;
+        var args = arguments;
+        var isLegacyCall = arguments.length &&
+            typeof arguments[0] === 'function';
+        if (isLegacyCall) {
+          return nativeMethod.apply(pc, [
+            function(description) {
+              var desc = replaceInternalStreamId(pc, description);
+              args[0].apply(null, [desc]);
+            },
+            function(err) {
+              if (args[1]) {
+                args[1].apply(null, err);
+              }
+            }, arguments[2]
+          ]);
+        }
+        return nativeMethod.apply(pc, arguments)
+        .then(function(description) {
+          return replaceInternalStreamId(pc, description);
+        });
+      };
+    });
+
+    var origSetLocalDescription =
+        window.RTCPeerConnection.prototype.setLocalDescription;
+    window.RTCPeerConnection.prototype.setLocalDescription = function() {
+      var pc = this;
+      if (!arguments.length || !arguments[0].type) {
+        return origSetLocalDescription.apply(pc, arguments);
+      }
+      arguments[0] = replaceExternalStreamId(pc, arguments[0]);
+      return origSetLocalDescription.apply(pc, arguments);
+    };
+
+    // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier
+
+    var origLocalDescription = Object.getOwnPropertyDescriptor(
+        window.RTCPeerConnection.prototype, 'localDescription');
+    Object.defineProperty(window.RTCPeerConnection.prototype,
+        'localDescription', {
+          get: function() {
+            var pc = this;
+            var description = origLocalDescription.get.apply(this);
+            if (description.type === '') {
+              return description;
+            }
+            return replaceInternalStreamId(pc, description);
+          }
+        });
+
+    window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      // We can not yet check for sender instanceof RTCRtpSender
+      // since we shim RTPSender. So we check if sender._pc is set.
+      if (!sender._pc) {
+        throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' +
+            'does not implement interface RTCRtpSender.', 'TypeError');
+      }
+      var isLocal = sender._pc === pc;
+      if (!isLocal) {
+        throw new DOMException('Sender was not created by this connection.',
+            'InvalidAccessError');
+      }
+
+      // Search for the native stream the senders track belongs to.
+      pc._streams = pc._streams || {};
+      var stream;
+      Object.keys(pc._streams).forEach(function(streamid) {
+        var hasTrack = pc._streams[streamid].getTracks().find(function(track) {
+          return sender.track === track;
+        });
+        if (hasTrack) {
+          stream = pc._streams[streamid];
+        }
+      });
+
+      if (stream) {
+        if (stream.getTracks().length === 1) {
+          // if this is the last track of the stream, remove the stream. This
+          // takes care of any shimmed _senders.
+          pc.removeStream(pc._reverseStreams[stream.id]);
+        } else {
+          // relying on the same odd chrome behaviour as above.
+          stream.removeTrack(sender.track);
+        }
+        pc.dispatchEvent(new Event('negotiationneeded'));
+      }
+    };
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        // Translate iceTransportPolicy to iceTransports,
+        // see https://code.google.com/p/webrtc/issues/detail?id=4869
+        // this was fixed in M56 along with unprefixing RTCPeerConnection.
+        logging('PeerConnection');
+        if (pcConfig && pcConfig.iceTransportPolicy) {
+          pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+        }
+
+        return new window.webkitRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.webkitRTCPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      if (window.webkitRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.webkitRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+    } else {
+      // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+      var OrigPeerConnection = window.RTCPeerConnection;
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (pcConfig && pcConfig.iceServers) {
+          var newIceServers = [];
+          for (var i = 0; i < pcConfig.iceServers.length; i++) {
+            var server = pcConfig.iceServers[i];
+            if (!server.hasOwnProperty('urls') &&
+                server.hasOwnProperty('url')) {
+              utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+              server = JSON.parse(JSON.stringify(server));
+              server.urls = server.url;
+              newIceServers.push(server);
+            } else {
+              newIceServers.push(pcConfig.iceServers[i]);
+            }
+          }
+          pcConfig.iceServers = newIceServers;
+        }
+        return new OrigPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+
+    var origGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(selector,
+        successCallback, errorCallback) {
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats.apply(this, arguments);
+      }
+
+      // When spec-style getStats is supported, return those when called with
+      // either no arguments or the selector argument is null.
+      if (origGetStats.length === 0 && (arguments.length === 0 ||
+          typeof arguments[0] !== 'function')) {
+        return origGetStats.apply(this, []);
+      }
+
+      var fixChromeStats_ = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: {
+              localcandidate: 'local-candidate',
+              remotecandidate: 'remote-candidate'
+            }[report.type] || report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      // shim getStats with maplike support
+      var makeMapStats = function(stats) {
+        return new Map(Object.keys(stats).map(function(key) {
+          return [key, stats[key]];
+        }));
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper_ = function(response) {
+          args[1](makeMapStats(fixChromeStats_(response)));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper_,
+          arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        origGetStats.apply(self, [
+          function(response) {
+            resolve(makeMapStats(fixChromeStats_(response)));
+          }, reject]);
+      }).then(successCallback, errorCallback);
+    };
+
+    // add promise support -- natively available in Chrome 51
+    if (browserDetails.version < 51) {
+      ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+          .forEach(function(method) {
+            var nativeMethod = window.RTCPeerConnection.prototype[method];
+            window.RTCPeerConnection.prototype[method] = function() {
+              var args = arguments;
+              var self = this;
+              var promise = new Promise(function(resolve, reject) {
+                nativeMethod.apply(self, [args[0], resolve, reject]);
+              });
+              if (args.length < 2) {
+                return promise;
+              }
+              return promise.then(function() {
+                args[1].apply(null, []);
+              },
+              function(err) {
+                if (args.length >= 3) {
+                  args[2].apply(null, [err]);
+                }
+              });
+            };
+          });
+    }
+
+    // promise support for createOffer and createAnswer. Available (without
+    // bugs) since M52: crbug/619289
+    if (browserDetails.version < 52) {
+      ['createOffer', 'createAnswer'].forEach(function(method) {
+        var nativeMethod = window.RTCPeerConnection.prototype[method];
+        window.RTCPeerConnection.prototype[method] = function() {
+          var self = this;
+          if (arguments.length < 1 || (arguments.length === 1 &&
+              typeof arguments[0] === 'object')) {
+            var opts = arguments.length === 1 ? arguments[0] : undefined;
+            return new Promise(function(resolve, reject) {
+              nativeMethod.apply(self, [resolve, reject, opts]);
+            });
+          }
+          return nativeMethod.apply(this, arguments);
+        };
+      });
+    }
+
+    // shim implicit creation of RTCSessionDescription/RTCIceCandidate
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+  }
+};
+
+
+// Expose public methods.
+module.exports = {
+  shimMediaStream: chromeShim.shimMediaStream,
+  shimOnTrack: chromeShim.shimOnTrack,
+  shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack,
+  shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf,
+  shimSourceObject: chromeShim.shimSourceObject,
+  shimPeerConnection: chromeShim.shimPeerConnection,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils.js":11,"./getusermedia":6}],6:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+
+  var constraintsToChrome_ = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname_ = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname_('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname_('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname_('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname_('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname_(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  var shimConstraints_ = function(constraints, func) {
+    if (browserDetails.version >= 61) {
+      return func(constraints);
+    }
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (constraints && typeof constraints.audio === 'object') {
+      var remap = function(obj, a, b) {
+        if (a in obj && !(b in obj)) {
+          obj[b] = obj[a];
+          delete obj[a];
+        }
+      };
+      constraints = JSON.parse(JSON.stringify(constraints));
+      remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
+      remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
+      constraints.audio = constraintsToChrome_(constraints.audio);
+    }
+    if (constraints && typeof constraints.video === 'object') {
+      // Shim facingMode for mobile & surface pro.
+      var face = constraints.video.facingMode;
+      face = face && ((typeof face === 'object') ? face : {ideal: face});
+      var getSupportedFacingModeLies = browserDetails.version < 66;
+
+      if ((face && (face.exact === 'user' || face.exact === 'environment' ||
+                    face.ideal === 'user' || face.ideal === 'environment')) &&
+          !(navigator.mediaDevices.getSupportedConstraints &&
+            navigator.mediaDevices.getSupportedConstraints().facingMode &&
+            !getSupportedFacingModeLies)) {
+        delete constraints.video.facingMode;
+        var matches;
+        if (face.exact === 'environment' || face.ideal === 'environment') {
+          matches = ['back', 'rear'];
+        } else if (face.exact === 'user' || face.ideal === 'user') {
+          matches = ['front'];
+        }
+        if (matches) {
+          // Look for matches in label, or use last cam for back (typical).
+          return navigator.mediaDevices.enumerateDevices()
+          .then(function(devices) {
+            devices = devices.filter(function(d) {
+              return d.kind === 'videoinput';
+            });
+            var dev = devices.find(function(d) {
+              return matches.some(function(match) {
+                return d.label.toLowerCase().indexOf(match) !== -1;
+              });
+            });
+            if (!dev && devices.length && matches.indexOf('back') !== -1) {
+              dev = devices[devices.length - 1]; // more likely the back cam
+            }
+            if (dev) {
+              constraints.video.deviceId = face.exact ? {exact: dev.deviceId} :
+                                                        {ideal: dev.deviceId};
+            }
+            constraints.video = constraintsToChrome_(constraints.video);
+            logging('chrome: ' + JSON.stringify(constraints));
+            return func(constraints);
+          });
+        }
+      }
+      constraints.video = constraintsToChrome_(constraints.video);
+    }
+    logging('chrome: ' + JSON.stringify(constraints));
+    return func(constraints);
+  };
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        PermissionDeniedError: 'NotAllowedError',
+        InvalidStateError: 'NotReadableError',
+        DevicesNotFoundError: 'NotFoundError',
+        ConstraintNotSatisfiedError: 'OverconstrainedError',
+        TrackStartError: 'NotReadableError',
+        MediaDeviceFailedDueToShutdown: 'NotReadableError',
+        MediaDeviceKillSwitchOn: 'NotReadableError'
+      }[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraintName,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    shimConstraints_(constraints, function(c) {
+      navigator.webkitGetUserMedia(c, onSuccess, function(e) {
+        if (onError) {
+          onError(shimError_(e));
+        }
+      });
+    });
+  };
+
+  navigator.getUserMedia = getUserMedia_;
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      navigator.getUserMedia(constraints, resolve, reject);
+    });
+  };
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {
+      getUserMedia: getUserMediaPromise_,
+      enumerateDevices: function() {
+        return new Promise(function(resolve) {
+          var kinds = {audio: 'audioinput', video: 'videoinput'};
+          return window.MediaStreamTrack.getSources(function(devices) {
+            resolve(devices.map(function(device) {
+              return {label: device.label,
+                kind: kinds[device.kind],
+                deviceId: device.id,
+                groupId: ''};
+            }));
+          });
+        });
+      },
+      getSupportedConstraints: function() {
+        return {
+          deviceId: true, echoCancellation: true, facingMode: true,
+          frameRate: true, height: true, width: true
+        };
+      }
+    };
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return getUserMediaPromise_(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(cs) {
+      return shimConstraints_(cs, function(c) {
+        return origGetUserMedia(c).then(function(stream) {
+          if (c.audio && !stream.getAudioTracks().length ||
+              c.video && !stream.getVideoTracks().length) {
+            stream.getTracks().forEach(function(track) {
+              track.stop();
+            });
+            throw new DOMException('', 'NotFoundError');
+          }
+          return stream;
+        }, function(e) {
+          return Promise.reject(shimError_(e));
+        });
+      });
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      logging('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      logging('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+};
+
+},{"../utils.js":11}],7:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+var utils = require('./utils');
+
+// Wraps the peerconnection event eventNameToWrap in a function
+// which returns the modified event object.
+function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
+  if (!window.RTCPeerConnection) {
+    return;
+  }
+  var proto = window.RTCPeerConnection.prototype;
+  var nativeAddEventListener = proto.addEventListener;
+  proto.addEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap) {
+      return nativeAddEventListener.apply(this, arguments);
+    }
+    var wrappedCallback = function(e) {
+      cb(wrapper(e));
+    };
+    this._eventMap = this._eventMap || {};
+    this._eventMap[cb] = wrappedCallback;
+    return nativeAddEventListener.apply(this, [nativeEventName,
+      wrappedCallback]);
+  };
+
+  var nativeRemoveEventListener = proto.removeEventListener;
+  proto.removeEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap || !this._eventMap
+        || !this._eventMap[cb]) {
+      return nativeRemoveEventListener.apply(this, arguments);
+    }
+    var unwrappedCb = this._eventMap[cb];
+    delete this._eventMap[cb];
+    return nativeRemoveEventListener.apply(this, [nativeEventName,
+      unwrappedCb]);
+  };
+
+  Object.defineProperty(proto, 'on' + eventNameToWrap, {
+    get: function() {
+      return this['_on' + eventNameToWrap];
+    },
+    set: function(cb) {
+      if (this['_on' + eventNameToWrap]) {
+        this.removeEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap]);
+        delete this['_on' + eventNameToWrap];
+      }
+      if (cb) {
+        this.addEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap] = cb);
+      }
+    }
+  });
+}
+
+module.exports = {
+  shimRTCIceCandidate: function(window) {
+    // foundation is arbitrarily chosen as an indicator for full support for
+    // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
+    if (window.RTCIceCandidate && 'foundation' in
+        window.RTCIceCandidate.prototype) {
+      return;
+    }
+
+    var NativeRTCIceCandidate = window.RTCIceCandidate;
+    window.RTCIceCandidate = function(args) {
+      // Remove the a= which shouldn't be part of the candidate string.
+      if (typeof args === 'object' && args.candidate &&
+          args.candidate.indexOf('a=') === 0) {
+        args = JSON.parse(JSON.stringify(args));
+        args.candidate = args.candidate.substr(2);
+      }
+
+      // Augment the native candidate with the parsed fields.
+      var nativeCandidate = new NativeRTCIceCandidate(args);
+      var parsedCandidate = SDPUtils.parseCandidate(args.candidate);
+      var augmentedCandidate = Object.assign(nativeCandidate,
+          parsedCandidate);
+
+      // Add a serializer that does not serialize the extra attributes.
+      augmentedCandidate.toJSON = function() {
+        return {
+          candidate: augmentedCandidate.candidate,
+          sdpMid: augmentedCandidate.sdpMid,
+          sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
+          usernameFragment: augmentedCandidate.usernameFragment,
+        };
+      };
+      return augmentedCandidate;
+    };
+
+    // Hook up the augmented candidate in onicecandidate and
+    // addEventListener('icecandidate', ...)
+    wrapPeerConnectionEvent(window, 'icecandidate', function(e) {
+      if (e.candidate) {
+        Object.defineProperty(e, 'candidate', {
+          value: new window.RTCIceCandidate(e.candidate),
+          writable: 'false'
+        });
+      }
+      return e;
+    });
+  },
+
+  // shimCreateObjectURL must be called before shimSourceObject to avoid loop.
+
+  shimCreateObjectURL: function(window) {
+    var URL = window && window.URL;
+
+    if (!(typeof window === 'object' && window.HTMLMediaElement &&
+          'srcObject' in window.HTMLMediaElement.prototype &&
+        URL.createObjectURL && URL.revokeObjectURL)) {
+      // Only shim CreateObjectURL using srcObject if srcObject exists.
+      return undefined;
+    }
+
+    var nativeCreateObjectURL = URL.createObjectURL.bind(URL);
+    var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL);
+    var streams = new Map(), newId = 0;
+
+    URL.createObjectURL = function(stream) {
+      if ('getTracks' in stream) {
+        var url = 'polyblob:' + (++newId);
+        streams.set(url, stream);
+        utils.deprecated('URL.createObjectURL(stream)',
+            'elem.srcObject = stream');
+        return url;
+      }
+      return nativeCreateObjectURL(stream);
+    };
+    URL.revokeObjectURL = function(url) {
+      nativeRevokeObjectURL(url);
+      streams.delete(url);
+    };
+
+    var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,
+                                              'src');
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'src', {
+      get: function() {
+        return dsc.get.apply(this);
+      },
+      set: function(url) {
+        this.srcObject = streams.get(url) || null;
+        return dsc.set.apply(this, [url]);
+      }
+    });
+
+    var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute;
+    window.HTMLMediaElement.prototype.setAttribute = function() {
+      if (arguments.length === 2 &&
+          ('' + arguments[0]).toLowerCase() === 'src') {
+        this.srcObject = streams.get(arguments[1]) || null;
+      }
+      return nativeSetAttribute.apply(this, arguments);
+    };
+  }
+};
+
+},{"./utils":11,"sdp":2}],8:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+
+var firefoxShim = {
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+            this.removeEventListener('addstream', this._ontrackpoly);
+          }
+          this.addEventListener('track', this._ontrack = f);
+          this.addEventListener('addstream', this._ontrackpoly = function(e) {
+            e.stream.getTracks().forEach(function(track) {
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = {track: track};
+              event.transceiver = {receiver: event.receiver};
+              event.streams = [e.stream];
+              this.dispatchEvent(event);
+            }.bind(this));
+          }.bind(this));
+        }
+      });
+    }
+    if (typeof window === 'object' && window.RTCTrackEvent &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        !('transceiver' in window.RTCTrackEvent.prototype)) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    // Firefox has supported mozSrcObject since FF22, unprefixed in 42.
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this.mozSrcObject;
+          },
+          set: function(stream) {
+            this.mozSrcObject = stream;
+          }
+        });
+      }
+    }
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (typeof window !== 'object' || !(window.RTCPeerConnection ||
+        window.mozRTCPeerConnection)) {
+      return; // probably media.peerconnection.enabled=false in about:config
+    }
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (browserDetails.version < 38) {
+          // .urls is not supported in FF < 38.
+          // create RTCIceServers with a single url.
+          if (pcConfig && pcConfig.iceServers) {
+            var newIceServers = [];
+            for (var i = 0; i < pcConfig.iceServers.length; i++) {
+              var server = pcConfig.iceServers[i];
+              if (server.hasOwnProperty('urls')) {
+                for (var j = 0; j < server.urls.length; j++) {
+                  var newServer = {
+                    url: server.urls[j]
+                  };
+                  if (server.urls[j].indexOf('turn') === 0) {
+                    newServer.username = server.username;
+                    newServer.credential = server.credential;
+                  }
+                  newIceServers.push(newServer);
+                }
+              } else {
+                newIceServers.push(pcConfig.iceServers[i]);
+              }
+            }
+            pcConfig.iceServers = newIceServers;
+          }
+        }
+        return new window.mozRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.mozRTCPeerConnection.prototype;
+
+      // wrap static methods. Currently just generateCertificate.
+      if (window.mozRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.mozRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+
+      window.RTCSessionDescription = window.mozRTCSessionDescription;
+      window.RTCIceCandidate = window.mozRTCIceCandidate;
+    }
+
+    // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+
+    // shim getStats with maplike support
+    var makeMapStats = function(stats) {
+      var map = new Map();
+      Object.keys(stats).forEach(function(key) {
+        map.set(key, stats[key]);
+        map[key] = stats[key];
+      });
+      return map;
+    };
+
+    var modernStatsTypes = {
+      inboundrtp: 'inbound-rtp',
+      outboundrtp: 'outbound-rtp',
+      candidatepair: 'candidate-pair',
+      localcandidate: 'local-candidate',
+      remotecandidate: 'remote-candidate'
+    };
+
+    var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(
+      selector,
+      onSucc,
+      onErr
+    ) {
+      return nativeGetStats.apply(this, [selector || null])
+        .then(function(stats) {
+          if (browserDetails.version < 48) {
+            stats = makeMapStats(stats);
+          }
+          if (browserDetails.version < 53 && !onSucc) {
+            // Shim only promise getStats with spec-hyphens in type names
+            // Leave callback version alone; misc old uses of forEach before Map
+            try {
+              stats.forEach(function(stat) {
+                stat.type = modernStatsTypes[stat.type] || stat.type;
+              });
+            } catch (e) {
+              if (e.name !== 'TypeError') {
+                throw e;
+              }
+              // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
+              stats.forEach(function(stat, i) {
+                stats.set(i, Object.assign({}, stat, {
+                  type: modernStatsTypes[stat.type] || stat.type
+                }));
+              });
+            }
+          }
+          return stats;
+        })
+        .then(onSucc, onErr);
+    };
+  },
+
+  shimRemoveStream: function(window) {
+    if (!window.RTCPeerConnection ||
+        'removeStream' in window.RTCPeerConnection.prototype) {
+      return;
+    }
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      utils.deprecated('removeStream', 'removeTrack');
+      this.getSenders().forEach(function(sender) {
+        if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) {
+          pc.removeTrack(sender);
+        }
+      });
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimOnTrack: firefoxShim.shimOnTrack,
+  shimSourceObject: firefoxShim.shimSourceObject,
+  shimPeerConnection: firefoxShim.shimPeerConnection,
+  shimRemoveStream: firefoxShim.shimRemoveStream,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils":11,"./getusermedia":9}],9:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+  var MediaStreamTrack = window && window.MediaStreamTrack;
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        InternalError: 'NotReadableError',
+        NotSupportedError: 'TypeError',
+        PermissionDeniedError: 'NotAllowedError',
+        SecurityError: 'NotAllowedError'
+      }[e.name] || e.name,
+      message: {
+        'The operation is insecure.': 'The request is not allowed by the ' +
+        'user agent or the platform in the current context.'
+      }[e.message] || e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  // getUserMedia constraints shim.
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    var constraintsToFF37_ = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r. min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (browserDetails.version < 38) {
+      logging('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37_(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37_(constraints.video);
+      }
+      logging('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
+      onError(shimError_(e));
+    });
+  };
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      getUserMedia_(constraints, resolve, reject);
+    });
+  };
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+        return new Promise(function(resolve) {
+          var infos = [
+            {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+            {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+          ];
+          resolve(infos);
+        });
+      };
+
+  if (browserDetails.version < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+  if (browserDetails.version < 49) {
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      return origGetUserMedia(c).then(function(stream) {
+        // Work around https://bugzil.la/802326
+        if (c.audio && !stream.getAudioTracks().length ||
+            c.video && !stream.getVideoTracks().length) {
+          stream.getTracks().forEach(function(track) {
+            track.stop();
+          });
+          throw new DOMException('The object can not be found here.',
+                                 'NotFoundError');
+        }
+        return stream;
+      }, function(e) {
+        return Promise.reject(shimError_(e));
+      });
+    };
+  }
+  if (!(browserDetails.version > 55 &&
+      'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
+    var remap = function(obj, a, b) {
+      if (a in obj && !(b in obj)) {
+        obj[b] = obj[a];
+        delete obj[a];
+      }
+    };
+
+    var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      if (typeof c === 'object' && typeof c.audio === 'object') {
+        c = JSON.parse(JSON.stringify(c));
+        remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
+        remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
+      }
+      return nativeGetUserMedia(c);
+    };
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
+      var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
+      MediaStreamTrack.prototype.getSettings = function() {
+        var obj = nativeGetSettings.apply(this, arguments);
+        remap(obj, 'mozAutoGainControl', 'autoGainControl');
+        remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
+        return obj;
+      };
+    }
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
+      var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
+      MediaStreamTrack.prototype.applyConstraints = function(c) {
+        if (this.kind === 'audio' && typeof c === 'object') {
+          c = JSON.parse(JSON.stringify(c));
+          remap(c, 'autoGainControl', 'mozAutoGainControl');
+          remap(c, 'noiseSuppression', 'mozNoiseSuppression');
+        }
+        return nativeApplyConstraints.apply(this, [c]);
+      };
+    }
+  }
+  navigator.getUserMedia = function(constraints, onSuccess, onError) {
+    if (browserDetails.version < 44) {
+      return getUserMedia_(constraints, onSuccess, onError);
+    }
+    // Replace Firefox 44+'s deprecation warning with unprefixed version.
+    utils.deprecated('navigator.getUserMedia',
+        'navigator.mediaDevices.getUserMedia');
+    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
+  };
+};
+
+},{"../utils":11}],10:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+'use strict';
+var utils = require('../utils');
+
+var safariShim = {
+  // TODO: DrAlex, should be here, double check against LayoutTests
+
+  // TODO: once the back-end for the mac port is done, add.
+  // TODO: check for webkitGTK+
+  // shimPeerConnection: function() { },
+
+  shimLocalStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getLocalStreams = function() {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        return this._localStreams;
+      };
+    }
+    if (!('getStreamById' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getStreamById = function(id) {
+        var result = null;
+        if (this._localStreams) {
+          this._localStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        if (this._remoteStreams) {
+          this._remoteStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        return result;
+      };
+    }
+    if (!('addStream' in window.RTCPeerConnection.prototype)) {
+      var _addTrack = window.RTCPeerConnection.prototype.addTrack;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        if (this._localStreams.indexOf(stream) === -1) {
+          this._localStreams.push(stream);
+        }
+        var self = this;
+        stream.getTracks().forEach(function(track) {
+          _addTrack.call(self, track, stream);
+        });
+      };
+
+      window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+        if (stream) {
+          if (!this._localStreams) {
+            this._localStreams = [stream];
+          } else if (this._localStreams.indexOf(stream) === -1) {
+            this._localStreams.push(stream);
+          }
+        }
+        return _addTrack.call(this, track, stream);
+      };
+    }
+    if (!('removeStream' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        var index = this._localStreams.indexOf(stream);
+        if (index === -1) {
+          return;
+        }
+        this._localStreams.splice(index, 1);
+        var self = this;
+        var tracks = stream.getTracks();
+        this.getSenders().forEach(function(sender) {
+          if (tracks.indexOf(sender.track) !== -1) {
+            self.removeTrack(sender);
+          }
+        });
+      };
+    }
+  },
+  shimRemoteStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getRemoteStreams = function() {
+        return this._remoteStreams ? this._remoteStreams : [];
+      };
+    }
+    if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
+        get: function() {
+          return this._onaddstream;
+        },
+        set: function(f) {
+          if (this._onaddstream) {
+            this.removeEventListener('addstream', this._onaddstream);
+            this.removeEventListener('track', this._onaddstreampoly);
+          }
+          this.addEventListener('addstream', this._onaddstream = f);
+          this.addEventListener('track', this._onaddstreampoly = function(e) {
+            var stream = e.streams[0];
+            if (!this._remoteStreams) {
+              this._remoteStreams = [];
+            }
+            if (this._remoteStreams.indexOf(stream) >= 0) {
+              return;
+            }
+            this._remoteStreams.push(stream);
+            var event = new Event('addstream');
+            event.stream = e.streams[0];
+            this.dispatchEvent(event);
+          }.bind(this));
+        }
+      });
+    }
+  },
+  shimCallbacksAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    var prototype = window.RTCPeerConnection.prototype;
+    var createOffer = prototype.createOffer;
+    var createAnswer = prototype.createAnswer;
+    var setLocalDescription = prototype.setLocalDescription;
+    var setRemoteDescription = prototype.setRemoteDescription;
+    var addIceCandidate = prototype.addIceCandidate;
+
+    prototype.createOffer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createOffer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    prototype.createAnswer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createAnswer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    var withCallback = function(description, successCallback, failureCallback) {
+      var promise = setLocalDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setLocalDescription = withCallback;
+
+    withCallback = function(description, successCallback, failureCallback) {
+      var promise = setRemoteDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setRemoteDescription = withCallback;
+
+    withCallback = function(candidate, successCallback, failureCallback) {
+      var promise = addIceCandidate.apply(this, [candidate]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.addIceCandidate = withCallback;
+  },
+  shimGetUserMedia: function(window) {
+    var navigator = window && window.navigator;
+
+    if (!navigator.getUserMedia) {
+      if (navigator.webkitGetUserMedia) {
+        navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+      } else if (navigator.mediaDevices &&
+          navigator.mediaDevices.getUserMedia) {
+        navigator.getUserMedia = function(constraints, cb, errcb) {
+          navigator.mediaDevices.getUserMedia(constraints)
+          .then(cb, errcb);
+        }.bind(navigator);
+      }
+    }
+  },
+  shimRTCIceServerUrls: function(window) {
+    // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+    var OrigPeerConnection = window.RTCPeerConnection;
+    window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (!server.hasOwnProperty('urls') &&
+              server.hasOwnProperty('url')) {
+            utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+            server = JSON.parse(JSON.stringify(server));
+            server.urls = server.url;
+            delete server.url;
+            newIceServers.push(server);
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+      return new OrigPeerConnection(pcConfig, pcConstraints);
+    };
+    window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+    // wrap static methods. Currently just generateCertificate.
+    if ('generateCertificate' in window.RTCPeerConnection) {
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+  },
+  shimTrackEventTransceiver: function(window) {
+    // Add event.transceiver member over deprecated event.receiver
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is
+        // defined for some reason even when window.RTCTransceiver is not.
+        !window.RTCTransceiver) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimCreateOfferLegacy: function(window) {
+    var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
+    window.RTCPeerConnection.prototype.createOffer = function(offerOptions) {
+      var pc = this;
+      if (offerOptions) {
+        var audioTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'audio';
+        });
+        if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
+          if (audioTransceiver.direction === 'sendrecv') {
+            audioTransceiver.setDirection('sendonly');
+          } else if (audioTransceiver.direction === 'recvonly') {
+            audioTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveAudio === true &&
+            !audioTransceiver) {
+          pc.addTransceiver('audio');
+        }
+
+        var videoTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'video';
+        });
+        if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
+          if (videoTransceiver.direction === 'sendrecv') {
+            videoTransceiver.setDirection('sendonly');
+          } else if (videoTransceiver.direction === 'recvonly') {
+            videoTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveVideo === true &&
+            !videoTransceiver) {
+          pc.addTransceiver('video');
+        }
+      }
+      return origCreateOffer.apply(pc, arguments);
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimCallbacksAPI: safariShim.shimCallbacksAPI,
+  shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI,
+  shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI,
+  shimGetUserMedia: safariShim.shimGetUserMedia,
+  shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls,
+  shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver,
+  shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy
+  // TODO
+  // shimPeerConnection: safariShim.shimPeerConnection
+};
+
+},{"../utils":11}],11:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var logDisabled_ = true;
+var deprecationWarnings_ = true;
+
+// Utility methods.
+var utils = {
+  disableLog: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    logDisabled_ = bool;
+    return (bool) ? 'adapter.js logging disabled' :
+        'adapter.js logging enabled';
+  },
+
+  /**
+   * Disable or enable deprecation warnings
+   * @param {!boolean} bool set to true to disable warnings.
+   */
+  disableWarnings: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    deprecationWarnings_ = !bool;
+    return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
+  },
+
+  log: function() {
+    if (typeof window === 'object') {
+      if (logDisabled_) {
+        return;
+      }
+      if (typeof console !== 'undefined' && typeof console.log === 'function') {
+        console.log.apply(console, arguments);
+      }
+    }
+  },
+
+  /**
+   * Shows a deprecation warning suggesting the modern and spec-compatible API.
+   */
+  deprecated: function(oldMethod, newMethod) {
+    if (!deprecationWarnings_) {
+      return;
+    }
+    console.warn(oldMethod + ' is deprecated, please use ' + newMethod +
+        ' instead.');
+  },
+
+  /**
+   * Extract browser version out of the provided user agent string.
+   *
+   * @param {!string} uastring userAgent string.
+   * @param {!string} expr Regular expression used as match criteria.
+   * @param {!number} pos position in the version string to be returned.
+   * @return {!number} browser version.
+   */
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos], 10);
+  },
+
+  /**
+   * Browser detector.
+   *
+   * @return {object} result containing browser and version
+   *     properties.
+   */
+  detectBrowser: function(window) {
+    var navigator = window && window.navigator;
+
+    // Returned result object.
+    var result = {};
+    result.browser = null;
+    result.version = null;
+
+    // Fail early if it's not a browser
+    if (typeof window === 'undefined' || !window.navigator) {
+      result.browser = 'Not a browser.';
+      return result;
+    }
+
+    // Firefox.
+    if (navigator.mozGetUserMedia) {
+      result.browser = 'firefox';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Firefox\/(\d+)\./, 1);
+    } else if (navigator.webkitGetUserMedia) {
+      // Chrome, Chromium, Webview, Opera, all use the chrome shim for now
+      if (window.webkitRTCPeerConnection) {
+        result.browser = 'chrome';
+        result.version = this.extractVersion(navigator.userAgent,
+          /Chrom(e|ium)\/(\d+)\./, 2);
+      } else { // Safari (in an unpublished version) or unknown webkit-based.
+        if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
+          result.browser = 'safari';
+          result.version = this.extractVersion(navigator.userAgent,
+            /AppleWebKit\/(\d+)\./, 1);
+        } else { // unknown webkit-based browser.
+          result.browser = 'Unsupported webkit-based browser ' +
+              'with GUM support but no WebRTC support.';
+          return result;
+        }
+      }
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
+      result.browser = 'edge';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Edge\/(\d+).(\d+)$/, 2);
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
+        // Safari, with webkitGetUserMedia removed.
+      result.browser = 'safari';
+      result.version = this.extractVersion(navigator.userAgent,
+          /AppleWebKit\/(\d+)\./, 1);
+    } else { // Default fallthrough: not supported.
+      result.browser = 'Not a supported browser.';
+      return result;
+    }
+
+    return result;
+  },
+
+};
+
+// Export.
+module.exports = {
+  log: utils.log,
+  deprecated: utils.deprecated,
+  disableLog: utils.disableLog,
+  disableWarnings: utils.disableWarnings,
+  extractVersion: utils.extractVersion,
+  shimCreateObjectURL: utils.shimCreateObjectURL,
+  detectBrowser: utils.detectBrowser.bind(utils)
+};
+
+},{}]},{},[3]);

+ 4470 - 0
support/client/lib/vwf/view/webrtc/dist/adapter_no_global.js

@@ -0,0 +1,4470 @@
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+
+function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : dtlsRole || 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+}
+
+// Edge does not like
+// 1) stun: filtered after 14393 unless ?transport=udp is present
+// 2) turn: that does not have all of turn:host:port?transport=udp
+// 3) turn: with ipv6 addresses
+// 4) turn: occurring muliple times
+function filterIceServers(iceServers, edgeVersion) {
+  var hasTurn = false;
+  iceServers = JSON.parse(JSON.stringify(iceServers));
+  return iceServers.filter(function(server) {
+    if (server && (server.urls || server.url)) {
+      var urls = server.urls || server.url;
+      if (server.url && !server.urls) {
+        console.warn('RTCIceServer.url is deprecated! Use urls instead.');
+      }
+      var isString = typeof urls === 'string';
+      if (isString) {
+        urls = [urls];
+      }
+      urls = urls.filter(function(url) {
+        var validTurn = url.indexOf('turn:') === 0 &&
+            url.indexOf('transport=udp') !== -1 &&
+            url.indexOf('turn:[') === -1 &&
+            !hasTurn;
+
+        if (validTurn) {
+          hasTurn = true;
+          return true;
+        }
+        return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
+            url.indexOf('?transport=udp') === -1;
+      });
+
+      delete server.url;
+      server.urls = isString ? urls[0] : urls;
+      return !!urls.length;
+    }
+    return false;
+  });
+}
+
+// Determines the intersection of local and remote capabilities.
+function getCommonCapabilities(localCapabilities, remoteCapabilities) {
+  var commonCapabilities = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: []
+  };
+
+  var findCodecByPayloadType = function(pt, codecs) {
+    pt = parseInt(pt, 10);
+    for (var i = 0; i < codecs.length; i++) {
+      if (codecs[i].payloadType === pt ||
+          codecs[i].preferredPayloadType === pt) {
+        return codecs[i];
+      }
+    }
+  };
+
+  var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
+    var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
+    var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
+    return lCodec && rCodec &&
+        lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
+  };
+
+  localCapabilities.codecs.forEach(function(lCodec) {
+    for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
+      var rCodec = remoteCapabilities.codecs[i];
+      if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
+          lCodec.clockRate === rCodec.clockRate) {
+        if (lCodec.name.toLowerCase() === 'rtx' &&
+            lCodec.parameters && rCodec.parameters.apt) {
+          // for RTX we need to find the local rtx that has a apt
+          // which points to the same local codec as the remote one.
+          if (!rtxCapabilityMatches(lCodec, rCodec,
+              localCapabilities.codecs, remoteCapabilities.codecs)) {
+            continue;
+          }
+        }
+        rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
+        // number of channels is the highest common number of channels
+        rCodec.numChannels = Math.min(lCodec.numChannels,
+            rCodec.numChannels);
+        // push rCodec so we reply with offerer payload type
+        commonCapabilities.codecs.push(rCodec);
+
+        // determine common feedback mechanisms
+        rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
+          for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
+            if (lCodec.rtcpFeedback[j].type === fb.type &&
+                lCodec.rtcpFeedback[j].parameter === fb.parameter) {
+              return true;
+            }
+          }
+          return false;
+        });
+        // FIXME: also need to determine .parameters
+        //  see https://github.com/openpeer/ortc/issues/569
+        break;
+      }
+    }
+  });
+
+  localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
+    for (var i = 0; i < remoteCapabilities.headerExtensions.length;
+         i++) {
+      var rHeaderExtension = remoteCapabilities.headerExtensions[i];
+      if (lHeaderExtension.uri === rHeaderExtension.uri) {
+        commonCapabilities.headerExtensions.push(rHeaderExtension);
+        break;
+      }
+    }
+  });
+
+  // FIXME: fecMechanisms
+  return commonCapabilities;
+}
+
+// is action=setLocalDescription with type allowed in signalingState
+function isActionAllowedInSignalingState(action, type, signalingState) {
+  return {
+    offer: {
+      setLocalDescription: ['stable', 'have-local-offer'],
+      setRemoteDescription: ['stable', 'have-remote-offer']
+    },
+    answer: {
+      setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
+      setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
+    }
+  }[type][action].indexOf(signalingState) !== -1;
+}
+
+function maybeAddCandidate(iceTransport, candidate) {
+  // Edge's internal representation adds some fields therefore
+  // not all fieldѕ are taken into account.
+  var alreadyAdded = iceTransport.getRemoteCandidates()
+      .find(function(remoteCandidate) {
+        return candidate.foundation === remoteCandidate.foundation &&
+            candidate.ip === remoteCandidate.ip &&
+            candidate.port === remoteCandidate.port &&
+            candidate.priority === remoteCandidate.priority &&
+            candidate.protocol === remoteCandidate.protocol &&
+            candidate.type === remoteCandidate.type;
+      });
+  if (!alreadyAdded) {
+    iceTransport.addRemoteCandidate(candidate);
+  }
+  return !alreadyAdded;
+}
+
+module.exports = function(window, edgeVersion) {
+  var RTCPeerConnection = function(config) {
+    var self = this;
+
+    var _eventTarget = document.createDocumentFragment();
+    ['addEventListener', 'removeEventListener', 'dispatchEvent']
+        .forEach(function(method) {
+          self[method] = _eventTarget[method].bind(_eventTarget);
+        });
+
+    this.onicecandidate = null;
+    this.onaddstream = null;
+    this.ontrack = null;
+    this.onremovestream = null;
+    this.onsignalingstatechange = null;
+    this.oniceconnectionstatechange = null;
+    this.onicegatheringstatechange = null;
+    this.onnegotiationneeded = null;
+    this.ondatachannel = null;
+    this.canTrickleIceCandidates = null;
+
+    this.needNegotiation = false;
+
+    this.localStreams = [];
+    this.remoteStreams = [];
+
+    this.localDescription = null;
+    this.remoteDescription = null;
+
+    this.signalingState = 'stable';
+    this.iceConnectionState = 'new';
+    this.iceGatheringState = 'new';
+
+    config = JSON.parse(JSON.stringify(config || {}));
+
+    this.usingBundle = config.bundlePolicy === 'max-bundle';
+    if (config.rtcpMuxPolicy === 'negotiate') {
+      var e = new Error('rtcpMuxPolicy \'negotiate\' is not supported');
+      e.name = 'NotSupportedError';
+      throw(e);
+    } else if (!config.rtcpMuxPolicy) {
+      config.rtcpMuxPolicy = 'require';
+    }
+
+    switch (config.iceTransportPolicy) {
+      case 'all':
+      case 'relay':
+        break;
+      default:
+        config.iceTransportPolicy = 'all';
+        break;
+    }
+
+    switch (config.bundlePolicy) {
+      case 'balanced':
+      case 'max-compat':
+      case 'max-bundle':
+        break;
+      default:
+        config.bundlePolicy = 'balanced';
+        break;
+    }
+
+    config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);
+
+    this._iceGatherers = [];
+    if (config.iceCandidatePoolSize) {
+      for (var i = config.iceCandidatePoolSize; i > 0; i--) {
+        this._iceGatherers = new window.RTCIceGatherer({
+          iceServers: config.iceServers,
+          gatherPolicy: config.iceTransportPolicy
+        });
+      }
+    } else {
+      config.iceCandidatePoolSize = 0;
+    }
+
+    this._config = config;
+
+    // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
+    // everything that is needed to describe a SDP m-line.
+    this.transceivers = [];
+
+    this._sdpSessionId = SDPUtils.generateSessionId();
+    this._sdpSessionVersion = 0;
+
+    this._dtlsRole = undefined; // role for a=setup to use in answers.
+  };
+
+  RTCPeerConnection.prototype._emitGatheringStateChange = function() {
+    var event = new Event('icegatheringstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onicegatheringstatechange === 'function') {
+      this.onicegatheringstatechange(event);
+    }
+  };
+
+  RTCPeerConnection.prototype.getConfiguration = function() {
+    return this._config;
+  };
+
+  RTCPeerConnection.prototype.getLocalStreams = function() {
+    return this.localStreams;
+  };
+
+  RTCPeerConnection.prototype.getRemoteStreams = function() {
+    return this.remoteStreams;
+  };
+
+  // internal helper to create a transceiver object.
+  // (whih is not yet the same as the WebRTC 1.0 transceiver)
+  RTCPeerConnection.prototype._createTransceiver = function(kind) {
+    var hasBundleTransport = this.transceivers.length > 0;
+    var transceiver = {
+      track: null,
+      iceGatherer: null,
+      iceTransport: null,
+      dtlsTransport: null,
+      localCapabilities: null,
+      remoteCapabilities: null,
+      rtpSender: null,
+      rtpReceiver: null,
+      kind: kind,
+      mid: null,
+      sendEncodingParameters: null,
+      recvEncodingParameters: null,
+      stream: null,
+      wantReceive: true
+    };
+    if (this.usingBundle && hasBundleTransport) {
+      transceiver.iceTransport = this.transceivers[0].iceTransport;
+      transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
+    } else {
+      var transports = this._createIceAndDtlsTransports();
+      transceiver.iceTransport = transports.iceTransport;
+      transceiver.dtlsTransport = transports.dtlsTransport;
+    }
+    this.transceivers.push(transceiver);
+    return transceiver;
+  };
+
+  RTCPeerConnection.prototype.addTrack = function(track, stream) {
+    var transceiver;
+    for (var i = 0; i < this.transceivers.length; i++) {
+      if (!this.transceivers[i].track &&
+          this.transceivers[i].kind === track.kind) {
+        transceiver = this.transceivers[i];
+      }
+    }
+    if (!transceiver) {
+      transceiver = this._createTransceiver(track.kind);
+    }
+
+    this._maybeFireNegotiationNeeded();
+
+    if (this.localStreams.indexOf(stream) === -1) {
+      this.localStreams.push(stream);
+    }
+
+    transceiver.track = track;
+    transceiver.stream = stream;
+    transceiver.rtpSender = new window.RTCRtpSender(track,
+        transceiver.dtlsTransport);
+    return transceiver.rtpSender;
+  };
+
+  RTCPeerConnection.prototype.addStream = function(stream) {
+    var self = this;
+    if (edgeVersion >= 15025) {
+      stream.getTracks().forEach(function(track) {
+        self.addTrack(track, stream);
+      });
+    } else {
+      // Clone is necessary for local demos mostly, attaching directly
+      // to two different senders does not work (build 10547).
+      // Fixed in 15025 (or earlier)
+      var clonedStream = stream.clone();
+      stream.getTracks().forEach(function(track, idx) {
+        var clonedTrack = clonedStream.getTracks()[idx];
+        track.addEventListener('enabled', function(event) {
+          clonedTrack.enabled = event.enabled;
+        });
+      });
+      clonedStream.getTracks().forEach(function(track) {
+        self.addTrack(track, clonedStream);
+      });
+    }
+  };
+
+  RTCPeerConnection.prototype.removeStream = function(stream) {
+    var idx = this.localStreams.indexOf(stream);
+    if (idx > -1) {
+      this.localStreams.splice(idx, 1);
+      this._maybeFireNegotiationNeeded();
+    }
+  };
+
+  RTCPeerConnection.prototype.getSenders = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpSender;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpSender;
+    });
+  };
+
+  RTCPeerConnection.prototype.getReceivers = function() {
+    return this.transceivers.filter(function(transceiver) {
+      return !!transceiver.rtpReceiver;
+    })
+    .map(function(transceiver) {
+      return transceiver.rtpReceiver;
+    });
+  };
+
+
+  RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex,
+      usingBundle) {
+    var self = this;
+    if (usingBundle && sdpMLineIndex > 0) {
+      return this.transceivers[0].iceGatherer;
+    } else if (this._iceGatherers.length) {
+      return this._iceGatherers.shift();
+    }
+    var iceGatherer = new window.RTCIceGatherer({
+      iceServers: this._config.iceServers,
+      gatherPolicy: this._config.iceTransportPolicy
+    });
+    Object.defineProperty(iceGatherer, 'state',
+        {value: 'new', writable: true}
+    );
+
+    this.transceivers[sdpMLineIndex].candidates = [];
+    this.transceivers[sdpMLineIndex].bufferCandidates = function(event) {
+      var end = !event.candidate || Object.keys(event.candidate).length === 0;
+      // polyfill since RTCIceGatherer.state is not implemented in
+      // Edge 10547 yet.
+      iceGatherer.state = end ? 'completed' : 'gathering';
+      if (self.transceivers[sdpMLineIndex].candidates !== null) {
+        self.transceivers[sdpMLineIndex].candidates.push(event.candidate);
+      }
+    };
+    iceGatherer.addEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    return iceGatherer;
+  };
+
+  // start gathering from an RTCIceGatherer.
+  RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) {
+    var self = this;
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer.onlocalcandidate) {
+      return;
+    }
+    var candidates = this.transceivers[sdpMLineIndex].candidates;
+    this.transceivers[sdpMLineIndex].candidates = null;
+    iceGatherer.removeEventListener('localcandidate',
+      this.transceivers[sdpMLineIndex].bufferCandidates);
+    iceGatherer.onlocalcandidate = function(evt) {
+      if (self.usingBundle && sdpMLineIndex > 0) {
+        // if we know that we use bundle we can drop candidates with
+        // ѕdpMLineIndex > 0. If we don't do this then our state gets
+        // confused since we dispose the extra ice gatherer.
+        return;
+      }
+      var event = new Event('icecandidate');
+      event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
+
+      var cand = evt.candidate;
+      // Edge emits an empty object for RTCIceCandidateComplete‥
+      var end = !cand || Object.keys(cand).length === 0;
+      if (end) {
+        // polyfill since RTCIceGatherer.state is not implemented in
+        // Edge 10547 yet.
+        if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') {
+          iceGatherer.state = 'completed';
+        }
+      } else {
+        if (iceGatherer.state === 'new') {
+          iceGatherer.state = 'gathering';
+        }
+        // RTCIceCandidate doesn't have a component, needs to be added
+        cand.component = 1;
+        event.candidate.candidate = SDPUtils.writeCandidate(cand);
+      }
+
+      // update local description.
+      var sections = SDPUtils.splitSections(self.localDescription.sdp);
+      if (!end) {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=' + event.candidate.candidate + '\r\n';
+      } else {
+        sections[event.candidate.sdpMLineIndex + 1] +=
+            'a=end-of-candidates\r\n';
+      }
+      self.localDescription.sdp = sections.join('');
+      var complete = self.transceivers.every(function(transceiver) {
+        return transceiver.iceGatherer &&
+            transceiver.iceGatherer.state === 'completed';
+      });
+
+      if (self.iceGatheringState !== 'gathering') {
+        self.iceGatheringState = 'gathering';
+        self._emitGatheringStateChange();
+      }
+
+      // Emit candidate. Also emit null candidate when all gatherers are
+      // complete.
+      if (!end) {
+        self.dispatchEvent(event);
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(event);
+        }
+      }
+      if (complete) {
+        self.dispatchEvent(new Event('icecandidate'));
+        if (typeof self.onicecandidate === 'function') {
+          self.onicecandidate(new Event('icecandidate'));
+        }
+        self.iceGatheringState = 'complete';
+        self._emitGatheringStateChange();
+      }
+    };
+
+    // emit already gathered candidates.
+    window.setTimeout(function() {
+      candidates.forEach(function(candidate) {
+        var e = new Event('RTCIceGatherEvent');
+        e.candidate = candidate;
+        iceGatherer.onlocalcandidate(e);
+      });
+    }, 0);
+  };
+
+  // Create ICE transport and DTLS transport.
+  RTCPeerConnection.prototype._createIceAndDtlsTransports = function() {
+    var self = this;
+    var iceTransport = new window.RTCIceTransport(null);
+    iceTransport.onicestatechange = function() {
+      self._updateConnectionState();
+    };
+
+    var dtlsTransport = new window.RTCDtlsTransport(iceTransport);
+    dtlsTransport.ondtlsstatechange = function() {
+      self._updateConnectionState();
+    };
+    dtlsTransport.onerror = function() {
+      // onerror does not set state to failed by itself.
+      Object.defineProperty(dtlsTransport, 'state',
+          {value: 'failed', writable: true});
+      self._updateConnectionState();
+    };
+
+    return {
+      iceTransport: iceTransport,
+      dtlsTransport: dtlsTransport
+    };
+  };
+
+  // Destroy ICE gatherer, ICE transport and DTLS transport.
+  // Without triggering the callbacks.
+  RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function(
+      sdpMLineIndex) {
+    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
+    if (iceGatherer) {
+      delete iceGatherer.onlocalcandidate;
+      delete this.transceivers[sdpMLineIndex].iceGatherer;
+    }
+    var iceTransport = this.transceivers[sdpMLineIndex].iceTransport;
+    if (iceTransport) {
+      delete iceTransport.onicestatechange;
+      delete this.transceivers[sdpMLineIndex].iceTransport;
+    }
+    var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport;
+    if (dtlsTransport) {
+      delete dtlsTransport.ondtlsstatechange;
+      delete dtlsTransport.onerror;
+      delete this.transceivers[sdpMLineIndex].dtlsTransport;
+    }
+  };
+
+  // Start the RTP Sender and Receiver for a transceiver.
+  RTCPeerConnection.prototype._transceive = function(transceiver,
+      send, recv) {
+    var params = getCommonCapabilities(transceiver.localCapabilities,
+        transceiver.remoteCapabilities);
+    if (send && transceiver.rtpSender) {
+      params.encodings = transceiver.sendEncodingParameters;
+      params.rtcp = {
+        cname: SDPUtils.localCName,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.recvEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpSender.send(params);
+    }
+    if (recv && transceiver.rtpReceiver && params.codecs.length > 0) {
+      // remove RTX field in Edge 14942
+      if (transceiver.kind === 'video'
+          && transceiver.recvEncodingParameters
+          && edgeVersion < 15019) {
+        transceiver.recvEncodingParameters.forEach(function(p) {
+          delete p.rtx;
+        });
+      }
+      params.encodings = transceiver.recvEncodingParameters;
+      params.rtcp = {
+        cname: transceiver.rtcpParameters.cname,
+        compound: transceiver.rtcpParameters.compound
+      };
+      if (transceiver.sendEncodingParameters.length) {
+        params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
+      }
+      transceiver.rtpReceiver.receive(params);
+    }
+  };
+
+  RTCPeerConnection.prototype.setLocalDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setLocalDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set local ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var sections;
+    var sessionpart;
+    if (description.type === 'offer') {
+      // VERY limited support for SDP munging. Limited to:
+      // * changing the order of codecs
+      sections = SDPUtils.splitSections(description.sdp);
+      sessionpart = sections.shift();
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var caps = SDPUtils.parseRtpParameters(mediaSection);
+        self.transceivers[sdpMLineIndex].localCapabilities = caps;
+      });
+
+      this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+        self._gather(transceiver.mid, sdpMLineIndex);
+      });
+    } else if (description.type === 'answer') {
+      sections = SDPUtils.splitSections(self.remoteDescription.sdp);
+      sessionpart = sections.shift();
+      var isIceLite = SDPUtils.matchPrefix(sessionpart,
+          'a=ice-lite').length > 0;
+      sections.forEach(function(mediaSection, sdpMLineIndex) {
+        var transceiver = self.transceivers[sdpMLineIndex];
+        var iceGatherer = transceiver.iceGatherer;
+        var iceTransport = transceiver.iceTransport;
+        var dtlsTransport = transceiver.dtlsTransport;
+        var localCapabilities = transceiver.localCapabilities;
+        var remoteCapabilities = transceiver.remoteCapabilities;
+
+        // treat bundle-only as not-rejected.
+        var rejected = SDPUtils.isRejected(mediaSection) &&
+            !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+
+        if (!rejected && !transceiver.isDatachannel) {
+          var remoteIceParameters = SDPUtils.getIceParameters(
+              mediaSection, sessionpart);
+          var remoteDtlsParameters = SDPUtils.getDtlsParameters(
+              mediaSection, sessionpart);
+          if (isIceLite) {
+            remoteDtlsParameters.role = 'server';
+          }
+
+          if (!self.usingBundle || sdpMLineIndex === 0) {
+            self._gather(transceiver.mid, sdpMLineIndex);
+            if (iceTransport.state === 'new') {
+              iceTransport.start(iceGatherer, remoteIceParameters,
+                  isIceLite ? 'controlling' : 'controlled');
+            }
+            if (dtlsTransport.state === 'new') {
+              dtlsTransport.start(remoteDtlsParameters);
+            }
+          }
+
+          // Calculate intersection of capabilities.
+          var params = getCommonCapabilities(localCapabilities,
+              remoteCapabilities);
+
+          // Start the RTCRtpSender. The RTCRtpReceiver for this
+          // transceiver has already been started in setRemoteDescription.
+          self._transceive(transceiver,
+              params.codecs.length > 0,
+              false);
+        }
+      });
+    }
+
+    this.localDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-local-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+
+    // If a success callback was provided, emit ICE candidates after it
+    // has been executed. Otherwise, emit callback after the Promise is
+    // resolved.
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    return new Promise(function(resolve) {
+      if (cb) {
+        cb.apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.setRemoteDescription = function(description) {
+    var self = this;
+    var args = arguments;
+
+    if (!isActionAllowedInSignalingState('setRemoteDescription',
+        description.type, this.signalingState)) {
+      return new Promise(function(resolve, reject) {
+        var e = new Error('Can not set remote ' + description.type +
+            ' in state ' + self.signalingState);
+        e.name = 'InvalidStateError';
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [e]);
+        }
+        reject(e);
+      });
+    }
+
+    var streams = {};
+    this.remoteStreams.forEach(function(stream) {
+      streams[stream.id] = stream;
+    });
+    var receiverList = [];
+    var sections = SDPUtils.splitSections(description.sdp);
+    var sessionpart = sections.shift();
+    var isIceLite = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-lite').length > 0;
+    var usingBundle = SDPUtils.matchPrefix(sessionpart,
+        'a=group:BUNDLE ').length > 0;
+    this.usingBundle = usingBundle;
+    var iceOptions = SDPUtils.matchPrefix(sessionpart,
+        'a=ice-options:')[0];
+    if (iceOptions) {
+      this.canTrickleIceCandidates = iceOptions.substr(14).split(' ')
+          .indexOf('trickle') >= 0;
+    } else {
+      this.canTrickleIceCandidates = false;
+    }
+
+    sections.forEach(function(mediaSection, sdpMLineIndex) {
+      var lines = SDPUtils.splitLines(mediaSection);
+      var kind = SDPUtils.getKind(mediaSection);
+      // treat bundle-only as not-rejected.
+      var rejected = SDPUtils.isRejected(mediaSection) &&
+          !SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 1;
+      var protocol = lines[0].substr(2).split(' ')[2];
+
+      var direction = SDPUtils.getDirection(mediaSection, sessionpart);
+      var remoteMsid = SDPUtils.parseMsid(mediaSection);
+
+      var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier();
+
+      // Reject datachannels which are not implemented yet.
+      if (kind === 'application' && protocol === 'DTLS/SCTP') {
+        self.transceivers[sdpMLineIndex] = {
+          mid: mid,
+          isDatachannel: true
+        };
+        return;
+      }
+
+      var transceiver;
+      var iceGatherer;
+      var iceTransport;
+      var dtlsTransport;
+      var rtpReceiver;
+      var sendEncodingParameters;
+      var recvEncodingParameters;
+      var localCapabilities;
+
+      var track;
+      // FIXME: ensure the mediaSection has rtcp-mux set.
+      var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
+      var remoteIceParameters;
+      var remoteDtlsParameters;
+      if (!rejected) {
+        remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
+            sessionpart);
+        remoteDtlsParameters.role = 'client';
+      }
+      recvEncodingParameters =
+          SDPUtils.parseRtpEncodingParameters(mediaSection);
+
+      var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection);
+
+      var isComplete = SDPUtils.matchPrefix(mediaSection,
+          'a=end-of-candidates', sessionpart).length > 0;
+      var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
+          .map(function(cand) {
+            return SDPUtils.parseCandidate(cand);
+          })
+          .filter(function(cand) {
+            return cand.component === 1;
+          });
+
+      // Check if we can use BUNDLE and dispose transports.
+      if ((description.type === 'offer' || description.type === 'answer') &&
+          !rejected && usingBundle && sdpMLineIndex > 0 &&
+          self.transceivers[sdpMLineIndex]) {
+        self._disposeIceAndDtlsTransports(sdpMLineIndex);
+        self.transceivers[sdpMLineIndex].iceGatherer =
+            self.transceivers[0].iceGatherer;
+        self.transceivers[sdpMLineIndex].iceTransport =
+            self.transceivers[0].iceTransport;
+        self.transceivers[sdpMLineIndex].dtlsTransport =
+            self.transceivers[0].dtlsTransport;
+        if (self.transceivers[sdpMLineIndex].rtpSender) {
+          self.transceivers[sdpMLineIndex].rtpSender.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+        if (self.transceivers[sdpMLineIndex].rtpReceiver) {
+          self.transceivers[sdpMLineIndex].rtpReceiver.setTransport(
+              self.transceivers[0].dtlsTransport);
+        }
+      }
+      if (description.type === 'offer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex] ||
+            self._createTransceiver(kind);
+        transceiver.mid = mid;
+
+        if (!transceiver.iceGatherer) {
+          transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+              usingBundle);
+        }
+
+        if (cands.length && transceiver.iceTransport.state === 'new') {
+          if (isComplete && (!usingBundle || sdpMLineIndex === 0)) {
+            transceiver.iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        localCapabilities = window.RTCRtpReceiver.getCapabilities(kind);
+
+        // filter RTX until additional stuff needed for RTX is implemented
+        // in adapter.js
+        if (edgeVersion < 15019) {
+          localCapabilities.codecs = localCapabilities.codecs.filter(
+              function(codec) {
+                return codec.name !== 'rtx';
+              });
+        }
+
+        sendEncodingParameters = transceiver.sendEncodingParameters || [{
+          ssrc: (2 * sdpMLineIndex + 2) * 1001
+        }];
+
+        var isNewTrack = false;
+        if (direction === 'sendrecv' || direction === 'sendonly') {
+          isNewTrack = !transceiver.rtpReceiver;
+          rtpReceiver = transceiver.rtpReceiver ||
+              new window.RTCRtpReceiver(transceiver.dtlsTransport, kind);
+
+          if (isNewTrack) {
+            var stream;
+            track = rtpReceiver.track;
+            // FIXME: does not work with Plan B.
+            if (remoteMsid) {
+              if (!streams[remoteMsid.stream]) {
+                streams[remoteMsid.stream] = new window.MediaStream();
+                Object.defineProperty(streams[remoteMsid.stream], 'id', {
+                  get: function() {
+                    return remoteMsid.stream;
+                  }
+                });
+              }
+              Object.defineProperty(track, 'id', {
+                get: function() {
+                  return remoteMsid.track;
+                }
+              });
+              stream = streams[remoteMsid.stream];
+            } else {
+              if (!streams.default) {
+                streams.default = new window.MediaStream();
+              }
+              stream = streams.default;
+            }
+            stream.addTrack(track);
+            receiverList.push([track, rtpReceiver, stream]);
+          }
+        }
+
+        transceiver.localCapabilities = localCapabilities;
+        transceiver.remoteCapabilities = remoteCapabilities;
+        transceiver.rtpReceiver = rtpReceiver;
+        transceiver.rtcpParameters = rtcpParameters;
+        transceiver.sendEncodingParameters = sendEncodingParameters;
+        transceiver.recvEncodingParameters = recvEncodingParameters;
+
+        // Start the RTCRtpReceiver now. The RTPSender is started in
+        // setLocalDescription.
+        self._transceive(self.transceivers[sdpMLineIndex],
+            false,
+            isNewTrack);
+      } else if (description.type === 'answer' && !rejected) {
+        transceiver = self.transceivers[sdpMLineIndex];
+        iceGatherer = transceiver.iceGatherer;
+        iceTransport = transceiver.iceTransport;
+        dtlsTransport = transceiver.dtlsTransport;
+        rtpReceiver = transceiver.rtpReceiver;
+        sendEncodingParameters = transceiver.sendEncodingParameters;
+        localCapabilities = transceiver.localCapabilities;
+
+        self.transceivers[sdpMLineIndex].recvEncodingParameters =
+            recvEncodingParameters;
+        self.transceivers[sdpMLineIndex].remoteCapabilities =
+            remoteCapabilities;
+        self.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters;
+
+        if (cands.length && iceTransport.state === 'new') {
+          if ((isIceLite || isComplete) &&
+              (!usingBundle || sdpMLineIndex === 0)) {
+            iceTransport.setRemoteCandidates(cands);
+          } else {
+            cands.forEach(function(candidate) {
+              maybeAddCandidate(transceiver.iceTransport, candidate);
+            });
+          }
+        }
+
+        if (!usingBundle || sdpMLineIndex === 0) {
+          if (iceTransport.state === 'new') {
+            iceTransport.start(iceGatherer, remoteIceParameters,
+                'controlling');
+          }
+          if (dtlsTransport.state === 'new') {
+            dtlsTransport.start(remoteDtlsParameters);
+          }
+        }
+
+        self._transceive(transceiver,
+            direction === 'sendrecv' || direction === 'recvonly',
+            direction === 'sendrecv' || direction === 'sendonly');
+
+        if (rtpReceiver &&
+            (direction === 'sendrecv' || direction === 'sendonly')) {
+          track = rtpReceiver.track;
+          if (remoteMsid) {
+            if (!streams[remoteMsid.stream]) {
+              streams[remoteMsid.stream] = new window.MediaStream();
+            }
+            streams[remoteMsid.stream].addTrack(track);
+            receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]);
+          } else {
+            if (!streams.default) {
+              streams.default = new window.MediaStream();
+            }
+            streams.default.addTrack(track);
+            receiverList.push([track, rtpReceiver, streams.default]);
+          }
+        } else {
+          // FIXME: actually the receiver should be created later.
+          delete transceiver.rtpReceiver;
+        }
+      }
+    });
+
+    if (this._dtlsRole === undefined) {
+      this._dtlsRole = description.type === 'offer' ? 'active' : 'passive';
+    }
+
+    this.remoteDescription = {
+      type: description.type,
+      sdp: description.sdp
+    };
+    switch (description.type) {
+      case 'offer':
+        this._updateSignalingState('have-remote-offer');
+        break;
+      case 'answer':
+        this._updateSignalingState('stable');
+        break;
+      default:
+        throw new TypeError('unsupported type "' + description.type +
+            '"');
+    }
+    Object.keys(streams).forEach(function(sid) {
+      var stream = streams[sid];
+      if (stream.getTracks().length) {
+        if (self.remoteStreams.indexOf(stream) === -1) {
+          self.remoteStreams.push(stream);
+          var event = new Event('addstream');
+          event.stream = stream;
+          window.setTimeout(function() {
+            self.dispatchEvent(event);
+            if (typeof self.onaddstream === 'function') {
+              self.onaddstream(event);
+            }
+          });
+        }
+
+        receiverList.forEach(function(item) {
+          var track = item[0];
+          var receiver = item[1];
+          if (stream.id !== item[2].id) {
+            return;
+          }
+          var trackEvent = new Event('track');
+          trackEvent.track = track;
+          trackEvent.receiver = receiver;
+          trackEvent.transceiver = {receiver: receiver};
+          trackEvent.streams = [stream];
+          window.setTimeout(function() {
+            self.dispatchEvent(trackEvent);
+            if (typeof self.ontrack === 'function') {
+              self.ontrack(trackEvent);
+            }
+          });
+        });
+      }
+    });
+
+    // check whether addIceCandidate({}) was called within four seconds after
+    // setRemoteDescription.
+    window.setTimeout(function() {
+      if (!(self && self.transceivers)) {
+        return;
+      }
+      self.transceivers.forEach(function(transceiver) {
+        if (transceiver.iceTransport &&
+            transceiver.iceTransport.state === 'new' &&
+            transceiver.iceTransport.getRemoteCandidates().length > 0) {
+          console.warn('Timeout for addRemoteCandidate. Consider sending ' +
+              'an end-of-candidates notification');
+          transceiver.iceTransport.addRemoteCandidate({});
+        }
+      });
+    }, 4000);
+
+    return new Promise(function(resolve) {
+      if (args.length > 1 && typeof args[1] === 'function') {
+        args[1].apply(null);
+      }
+      resolve();
+    });
+  };
+
+  RTCPeerConnection.prototype.close = function() {
+    this.transceivers.forEach(function(transceiver) {
+      /* not yet
+      if (transceiver.iceGatherer) {
+        transceiver.iceGatherer.close();
+      }
+      */
+      if (transceiver.iceTransport) {
+        transceiver.iceTransport.stop();
+      }
+      if (transceiver.dtlsTransport) {
+        transceiver.dtlsTransport.stop();
+      }
+      if (transceiver.rtpSender) {
+        transceiver.rtpSender.stop();
+      }
+      if (transceiver.rtpReceiver) {
+        transceiver.rtpReceiver.stop();
+      }
+    });
+    // FIXME: clean up tracks, local streams, remote streams, etc
+    this._updateSignalingState('closed');
+  };
+
+  // Update the signaling state.
+  RTCPeerConnection.prototype._updateSignalingState = function(newState) {
+    this.signalingState = newState;
+    var event = new Event('signalingstatechange');
+    this.dispatchEvent(event);
+    if (typeof this.onsignalingstatechange === 'function') {
+      this.onsignalingstatechange(event);
+    }
+  };
+
+  // Determine whether to fire the negotiationneeded event.
+  RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() {
+    var self = this;
+    if (this.signalingState !== 'stable' || this.needNegotiation === true) {
+      return;
+    }
+    this.needNegotiation = true;
+    window.setTimeout(function() {
+      if (self.needNegotiation === false) {
+        return;
+      }
+      self.needNegotiation = false;
+      var event = new Event('negotiationneeded');
+      self.dispatchEvent(event);
+      if (typeof self.onnegotiationneeded === 'function') {
+        self.onnegotiationneeded(event);
+      }
+    }, 0);
+  };
+
+  // Update the connection state.
+  RTCPeerConnection.prototype._updateConnectionState = function() {
+    var newState;
+    var states = {
+      'new': 0,
+      closed: 0,
+      connecting: 0,
+      checking: 0,
+      connected: 0,
+      completed: 0,
+      disconnected: 0,
+      failed: 0
+    };
+    this.transceivers.forEach(function(transceiver) {
+      states[transceiver.iceTransport.state]++;
+      states[transceiver.dtlsTransport.state]++;
+    });
+    // ICETransport.completed and connected are the same for this purpose.
+    states.connected += states.completed;
+
+    newState = 'new';
+    if (states.failed > 0) {
+      newState = 'failed';
+    } else if (states.connecting > 0 || states.checking > 0) {
+      newState = 'connecting';
+    } else if (states.disconnected > 0) {
+      newState = 'disconnected';
+    } else if (states.new > 0) {
+      newState = 'new';
+    } else if (states.connected > 0 || states.completed > 0) {
+      newState = 'connected';
+    }
+
+    if (newState !== this.iceConnectionState) {
+      this.iceConnectionState = newState;
+      var event = new Event('iceconnectionstatechange');
+      this.dispatchEvent(event);
+      if (typeof this.oniceconnectionstatechange === 'function') {
+        this.oniceconnectionstatechange(event);
+      }
+    }
+  };
+
+  RTCPeerConnection.prototype.createOffer = function() {
+    var self = this;
+    var args = arguments;
+
+    var offerOptions;
+    if (arguments.length === 1 && typeof arguments[0] !== 'function') {
+      offerOptions = arguments[0];
+    } else if (arguments.length === 3) {
+      offerOptions = arguments[2];
+    }
+
+    var numAudioTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'audio';
+    }).length;
+    var numVideoTracks = this.transceivers.filter(function(t) {
+      return t.kind === 'video';
+    }).length;
+
+    // Determine number of audio and video tracks we need to send/recv.
+    if (offerOptions) {
+      // Reject Chrome legacy constraints.
+      if (offerOptions.mandatory || offerOptions.optional) {
+        throw new TypeError(
+            'Legacy mandatory/optional constraints not supported.');
+      }
+      if (offerOptions.offerToReceiveAudio !== undefined) {
+        if (offerOptions.offerToReceiveAudio === true) {
+          numAudioTracks = 1;
+        } else if (offerOptions.offerToReceiveAudio === false) {
+          numAudioTracks = 0;
+        } else {
+          numAudioTracks = offerOptions.offerToReceiveAudio;
+        }
+      }
+      if (offerOptions.offerToReceiveVideo !== undefined) {
+        if (offerOptions.offerToReceiveVideo === true) {
+          numVideoTracks = 1;
+        } else if (offerOptions.offerToReceiveVideo === false) {
+          numVideoTracks = 0;
+        } else {
+          numVideoTracks = offerOptions.offerToReceiveVideo;
+        }
+      }
+    }
+
+    this.transceivers.forEach(function(transceiver) {
+      if (transceiver.kind === 'audio') {
+        numAudioTracks--;
+        if (numAudioTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      } else if (transceiver.kind === 'video') {
+        numVideoTracks--;
+        if (numVideoTracks < 0) {
+          transceiver.wantReceive = false;
+        }
+      }
+    });
+
+    // Create M-lines for recvonly streams.
+    while (numAudioTracks > 0 || numVideoTracks > 0) {
+      if (numAudioTracks > 0) {
+        this._createTransceiver('audio');
+        numAudioTracks--;
+      }
+      if (numVideoTracks > 0) {
+        this._createTransceiver('video');
+        numVideoTracks--;
+      }
+    }
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      // For each track, create an ice gatherer, ice transport,
+      // dtls transport, potentially rtpsender and rtpreceiver.
+      var track = transceiver.track;
+      var kind = transceiver.kind;
+      var mid = SDPUtils.generateIdentifier();
+      transceiver.mid = mid;
+
+      if (!transceiver.iceGatherer) {
+        transceiver.iceGatherer = self._createIceGatherer(sdpMLineIndex,
+            self.usingBundle);
+      }
+
+      var localCapabilities = window.RTCRtpSender.getCapabilities(kind);
+      // filter RTX until additional stuff needed for RTX is implemented
+      // in adapter.js
+      if (edgeVersion < 15019) {
+        localCapabilities.codecs = localCapabilities.codecs.filter(
+            function(codec) {
+              return codec.name !== 'rtx';
+            });
+      }
+      localCapabilities.codecs.forEach(function(codec) {
+        // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
+        // by adding level-asymmetry-allowed=1
+        if (codec.name === 'H264' &&
+            codec.parameters['level-asymmetry-allowed'] === undefined) {
+          codec.parameters['level-asymmetry-allowed'] = '1';
+        }
+      });
+
+      // generate an ssrc now, to be used later in rtpSender.send
+      var sendEncodingParameters = transceiver.sendEncodingParameters || [{
+        ssrc: (2 * sdpMLineIndex + 1) * 1001
+      }];
+      if (track) {
+        // add RTX
+        if (edgeVersion >= 15019 && kind === 'video' &&
+            !sendEncodingParameters[0].rtx) {
+          sendEncodingParameters[0].rtx = {
+            ssrc: sendEncodingParameters[0].ssrc + 1
+          };
+        }
+      }
+
+      if (transceiver.wantReceive) {
+        transceiver.rtpReceiver = new window.RTCRtpReceiver(
+            transceiver.dtlsTransport, kind);
+      }
+
+      transceiver.localCapabilities = localCapabilities;
+      transceiver.sendEncodingParameters = sendEncodingParameters;
+    });
+
+    // always offer BUNDLE and dispose on return if not supported.
+    if (this._config.bundlePolicy !== 'max-compat') {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    sdp += 'a=ice-options:trickle\r\n';
+
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      sdp += writeMediaSection(transceiver, transceiver.localCapabilities,
+          'offer', transceiver.stream, self._dtlsRole);
+      sdp += 'a=rtcp-rsize\r\n';
+
+      if (transceiver.iceGatherer && self.iceGatheringState !== 'new' &&
+          (sdpMLineIndex === 0 || !self.usingBundle)) {
+        transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) {
+          cand.component = 1;
+          sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n';
+        });
+
+        if (transceiver.iceGatherer.state === 'completed') {
+          sdp += 'a=end-of-candidates\r\n';
+        }
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'offer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.createAnswer = function() {
+    var self = this;
+    var args = arguments;
+
+    var sdp = SDPUtils.writeSessionBoilerplate(this._sdpSessionId,
+        this._sdpSessionVersion++);
+    if (this.usingBundle) {
+      sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
+        return t.mid;
+      }).join(' ') + '\r\n';
+    }
+    var mediaSectionsInOffer = SDPUtils.splitSections(
+        this.remoteDescription.sdp).length - 1;
+    this.transceivers.forEach(function(transceiver, sdpMLineIndex) {
+      if (sdpMLineIndex + 1 > mediaSectionsInOffer) {
+        return;
+      }
+      if (transceiver.isDatachannel) {
+        sdp += 'm=application 0 DTLS/SCTP 5000\r\n' +
+            'c=IN IP4 0.0.0.0\r\n' +
+            'a=mid:' + transceiver.mid + '\r\n';
+        return;
+      }
+
+      // FIXME: look at direction.
+      if (transceiver.stream) {
+        var localTrack;
+        if (transceiver.kind === 'audio') {
+          localTrack = transceiver.stream.getAudioTracks()[0];
+        } else if (transceiver.kind === 'video') {
+          localTrack = transceiver.stream.getVideoTracks()[0];
+        }
+        if (localTrack) {
+          // add RTX
+          if (edgeVersion >= 15019 && transceiver.kind === 'video' &&
+              !transceiver.sendEncodingParameters[0].rtx) {
+            transceiver.sendEncodingParameters[0].rtx = {
+              ssrc: transceiver.sendEncodingParameters[0].ssrc + 1
+            };
+          }
+        }
+      }
+
+      // Calculate intersection of capabilities.
+      var commonCapabilities = getCommonCapabilities(
+          transceiver.localCapabilities,
+          transceiver.remoteCapabilities);
+
+      var hasRtx = commonCapabilities.codecs.filter(function(c) {
+        return c.name.toLowerCase() === 'rtx';
+      }).length;
+      if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
+        delete transceiver.sendEncodingParameters[0].rtx;
+      }
+
+      sdp += writeMediaSection(transceiver, commonCapabilities,
+          'answer', transceiver.stream, self._dtlsRole);
+      if (transceiver.rtcpParameters &&
+          transceiver.rtcpParameters.reducedSize) {
+        sdp += 'a=rtcp-rsize\r\n';
+      }
+    });
+
+    var desc = new window.RTCSessionDescription({
+      type: 'answer',
+      sdp: sdp
+    });
+    return new Promise(function(resolve) {
+      if (args.length > 0 && typeof args[0] === 'function') {
+        args[0].apply(null, [desc]);
+        resolve();
+        return;
+      }
+      resolve(desc);
+    });
+  };
+
+  RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
+    var err;
+    var sections;
+    if (!candidate || candidate.candidate === '') {
+      for (var j = 0; j < this.transceivers.length; j++) {
+        if (this.transceivers[j].isDatachannel) {
+          continue;
+        }
+        this.transceivers[j].iceTransport.addRemoteCandidate({});
+        sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+        sections[j + 1] += 'a=end-of-candidates\r\n';
+        this.remoteDescription.sdp = sections.join('');
+        if (this.usingBundle) {
+          break;
+        }
+      }
+    } else if (!(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) {
+      throw new TypeError('sdpMLineIndex or sdpMid required');
+    } else if (!this.remoteDescription) {
+      err = new Error('Can not add ICE candidate without ' +
+          'a remote description');
+      err.name = 'InvalidStateError';
+    } else {
+      var sdpMLineIndex = candidate.sdpMLineIndex;
+      if (candidate.sdpMid) {
+        for (var i = 0; i < this.transceivers.length; i++) {
+          if (this.transceivers[i].mid === candidate.sdpMid) {
+            sdpMLineIndex = i;
+            break;
+          }
+        }
+      }
+      var transceiver = this.transceivers[sdpMLineIndex];
+      if (transceiver) {
+        if (transceiver.isDatachannel) {
+          return Promise.resolve();
+        }
+        var cand = Object.keys(candidate.candidate).length > 0 ?
+            SDPUtils.parseCandidate(candidate.candidate) : {};
+        // Ignore Chrome's invalid candidates since Edge does not like them.
+        if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
+          return Promise.resolve();
+        }
+        // Ignore RTCP candidates, we assume RTCP-MUX.
+        if (cand.component && cand.component !== 1) {
+          return Promise.resolve();
+        }
+        // when using bundle, avoid adding candidates to the wrong
+        // ice transport. And avoid adding candidates added in the SDP.
+        if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 &&
+            transceiver.iceTransport !== this.transceivers[0].iceTransport)) {
+          if (!maybeAddCandidate(transceiver.iceTransport, cand)) {
+            err = new Error('Can not add ICE candidate');
+            err.name = 'OperationError';
+          }
+        }
+
+        if (!err) {
+          // update the remoteDescription.
+          var candidateString = candidate.candidate.trim();
+          if (candidateString.indexOf('a=') === 0) {
+            candidateString = candidateString.substr(2);
+          }
+          sections = SDPUtils.splitSections(this.remoteDescription.sdp);
+          sections[sdpMLineIndex + 1] += 'a=' +
+              (cand.type ? candidateString : 'end-of-candidates')
+              + '\r\n';
+          this.remoteDescription.sdp = sections.join('');
+        }
+      } else {
+        err = new Error('Can not add ICE candidate');
+        err.name = 'OperationError';
+      }
+    }
+    var args = arguments;
+    return new Promise(function(resolve, reject) {
+      if (err) {
+        if (args.length > 2 && typeof args[2] === 'function') {
+          args[2].apply(null, [err]);
+        }
+        reject(err);
+      } else {
+        if (args.length > 1 && typeof args[1] === 'function') {
+          args[1].apply(null);
+        }
+        resolve();
+      }
+    });
+  };
+
+  RTCPeerConnection.prototype.getStats = function() {
+    var promises = [];
+    this.transceivers.forEach(function(transceiver) {
+      ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
+          'dtlsTransport'].forEach(function(method) {
+            if (transceiver[method]) {
+              promises.push(transceiver[method].getStats());
+            }
+          });
+    });
+    var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
+        arguments[1];
+    var fixStatsType = function(stat) {
+      return {
+        inboundrtp: 'inbound-rtp',
+        outboundrtp: 'outbound-rtp',
+        candidatepair: 'candidate-pair',
+        localcandidate: 'local-candidate',
+        remotecandidate: 'remote-candidate'
+      }[stat.type] || stat.type;
+    };
+    return new Promise(function(resolve) {
+      // shim getStats with maplike support
+      var results = new Map();
+      Promise.all(promises).then(function(res) {
+        res.forEach(function(result) {
+          Object.keys(result).forEach(function(id) {
+            result[id].type = fixStatsType(result[id]);
+            results.set(id, result[id]);
+          });
+        });
+        if (cb) {
+          cb.apply(null, results);
+        }
+        resolve(results);
+      });
+    });
+  };
+  return RTCPeerConnection;
+};
+
+},{"sdp":2}],2:[function(require,module,exports){
+ /* eslint-env node */
+'use strict';
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+  return Math.random().toString(36).substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+  return blob.trim().split('\n').map(function(line) {
+    return line.trim();
+  });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+  var parts = blob.split('\nm=');
+  return parts.map(function(part, index) {
+    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
+  });
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+  return SDPUtils.splitLines(blob).filter(function(line) {
+    return line.indexOf(prefix) === 0;
+  });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+  var parts;
+  // Parse both variants.
+  if (line.indexOf('a=candidate:') === 0) {
+    parts = line.substring(12).split(' ');
+  } else {
+    parts = line.substring(10).split(' ');
+  }
+
+  var candidate = {
+    foundation: parts[0],
+    component: parseInt(parts[1], 10),
+    protocol: parts[2].toLowerCase(),
+    priority: parseInt(parts[3], 10),
+    ip: parts[4],
+    port: parseInt(parts[5], 10),
+    // skip parts[6] == 'typ'
+    type: parts[7]
+  };
+
+  for (var i = 8; i < parts.length; i += 2) {
+    switch (parts[i]) {
+      case 'raddr':
+        candidate.relatedAddress = parts[i + 1];
+        break;
+      case 'rport':
+        candidate.relatedPort = parseInt(parts[i + 1], 10);
+        break;
+      case 'tcptype':
+        candidate.tcpType = parts[i + 1];
+        break;
+      case 'ufrag':
+        candidate.ufrag = parts[i + 1]; // for backward compability.
+        candidate.usernameFragment = parts[i + 1];
+        break;
+      default: // extension handling, in particular ufrag
+        candidate[parts[i]] = parts[i + 1];
+        break;
+    }
+  }
+  return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+  var sdp = [];
+  sdp.push(candidate.foundation);
+  sdp.push(candidate.component);
+  sdp.push(candidate.protocol.toUpperCase());
+  sdp.push(candidate.priority);
+  sdp.push(candidate.ip);
+  sdp.push(candidate.port);
+
+  var type = candidate.type;
+  sdp.push('typ');
+  sdp.push(type);
+  if (type !== 'host' && candidate.relatedAddress &&
+      candidate.relatedPort) {
+    sdp.push('raddr');
+    sdp.push(candidate.relatedAddress); // was: relAddr
+    sdp.push('rport');
+    sdp.push(candidate.relatedPort); // was: relPort
+  }
+  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
+    sdp.push('tcptype');
+    sdp.push(candidate.tcpType);
+  }
+  if (candidate.ufrag) {
+    sdp.push('ufrag');
+    sdp.push(candidate.ufrag);
+  }
+  return 'candidate:' + sdp.join(' ');
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+  return line.substr(14).split(' ');
+}
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+  var parts = line.substr(9).split(' ');
+  var parsed = {
+    payloadType: parseInt(parts.shift(), 10) // was: id
+  };
+
+  parts = parts[0].split('/');
+
+  parsed.name = parts[0];
+  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+  // was: channels
+  parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+  return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
+      (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+  var parts = line.substr(9).split(' ');
+  return {
+    id: parseInt(parts[0], 10),
+    direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
+    uri: parts[1]
+  };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
+      (headerExtension.direction && headerExtension.direction !== 'sendrecv'
+          ? '/' + headerExtension.direction
+          : '') +
+      ' ' + headerExtension.uri + '\r\n';
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+  var parsed = {};
+  var kv;
+  var parts = line.substr(line.indexOf(' ') + 1).split(';');
+  for (var j = 0; j < parts.length; j++) {
+    kv = parts[j].trim().split('=');
+    parsed[kv[0].trim()] = kv[1];
+  }
+  return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+  var line = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.parameters && Object.keys(codec.parameters).length) {
+    var params = [];
+    Object.keys(codec.parameters).forEach(function(param) {
+      params.push(param + '=' + codec.parameters[param]);
+    });
+    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
+  }
+  return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
+  return {
+    type: parts.shift(),
+    parameter: parts.join(' ')
+  };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+  var lines = '';
+  var pt = codec.payloadType;
+  if (codec.preferredPayloadType !== undefined) {
+    pt = codec.preferredPayloadType;
+  }
+  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+    // FIXME: special handling for trr-int?
+    codec.rtcpFeedback.forEach(function(fb) {
+      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
+      (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
+          '\r\n';
+    });
+  }
+  return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+  var sp = line.indexOf(' ');
+  var parts = {
+    ssrc: parseInt(line.substr(7, sp - 7), 10)
+  };
+  var colon = line.indexOf(':', sp);
+  if (colon > -1) {
+    parts.attribute = line.substr(sp + 1, colon - sp - 1);
+    parts.value = line.substr(colon + 1);
+  } else {
+    parts.attribute = line.substr(sp + 1);
+  }
+  return parts;
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+  var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
+  if (mid) {
+    return mid.substr(6);
+  }
+}
+
+SDPUtils.parseFingerprint = function(line) {
+  var parts = line.substr(14).split(' ');
+  return {
+    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+    value: parts[1]
+  };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
+      'a=fingerprint:');
+  // Note: a=setup line is ignored since we use the 'auto' role.
+  // Note2: 'algorithm' is not case sensitive except in Edge.
+  return {
+    role: 'auto',
+    fingerprints: lines.map(SDPUtils.parseFingerprint)
+  };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+  var sdp = 'a=setup:' + setupType + '\r\n';
+  params.fingerprints.forEach(function(fp) {
+    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
+  });
+  return sdp;
+};
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+//   get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  // Search in session part, too.
+  lines = lines.concat(SDPUtils.splitLines(sessionpart));
+  var iceParameters = {
+    usernameFragment: lines.filter(function(line) {
+      return line.indexOf('a=ice-ufrag:') === 0;
+    })[0].substr(12),
+    password: lines.filter(function(line) {
+      return line.indexOf('a=ice-pwd:') === 0;
+    })[0].substr(10)
+  };
+  return iceParameters;
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
+      'a=ice-pwd:' + params.password + '\r\n';
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+  var description = {
+    codecs: [],
+    headerExtensions: [],
+    fecMechanisms: [],
+    rtcp: []
+  };
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
+    var pt = mline[i];
+    var rtpmapline = SDPUtils.matchPrefix(
+        mediaSection, 'a=rtpmap:' + pt + ' ')[0];
+    if (rtpmapline) {
+      var codec = SDPUtils.parseRtpMap(rtpmapline);
+      var fmtps = SDPUtils.matchPrefix(
+          mediaSection, 'a=fmtp:' + pt + ' ');
+      // Only the first a=fmtp:<pt> is considered.
+      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+      codec.rtcpFeedback = SDPUtils.matchPrefix(
+          mediaSection, 'a=rtcp-fb:' + pt + ' ')
+        .map(SDPUtils.parseRtcpFb);
+      description.codecs.push(codec);
+      // parse FEC mechanisms from rtpmap lines.
+      switch (codec.name.toUpperCase()) {
+        case 'RED':
+        case 'ULPFEC':
+          description.fecMechanisms.push(codec.name.toUpperCase());
+          break;
+        default: // only RED and ULPFEC are recognized as FEC mechanisms.
+          break;
+      }
+    }
+  }
+  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
+    description.headerExtensions.push(SDPUtils.parseExtmap(line));
+  });
+  // FIXME: parse rtcp.
+  return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+  var sdp = '';
+
+  // Build the mline.
+  sdp += 'm=' + kind + ' ';
+  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
+  sdp += ' UDP/TLS/RTP/SAVPF ';
+  sdp += caps.codecs.map(function(codec) {
+    if (codec.preferredPayloadType !== undefined) {
+      return codec.preferredPayloadType;
+    }
+    return codec.payloadType;
+  }).join(' ') + '\r\n';
+
+  sdp += 'c=IN IP4 0.0.0.0\r\n';
+  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
+
+  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+  caps.codecs.forEach(function(codec) {
+    sdp += SDPUtils.writeRtpMap(codec);
+    sdp += SDPUtils.writeFmtp(codec);
+    sdp += SDPUtils.writeRtcpFb(codec);
+  });
+  var maxptime = 0;
+  caps.codecs.forEach(function(codec) {
+    if (codec.maxptime > maxptime) {
+      maxptime = codec.maxptime;
+    }
+  });
+  if (maxptime > 0) {
+    sdp += 'a=maxptime:' + maxptime + '\r\n';
+  }
+  sdp += 'a=rtcp-mux\r\n';
+
+  caps.headerExtensions.forEach(function(extension) {
+    sdp += SDPUtils.writeExtmap(extension);
+  });
+  // FIXME: write fecMechanisms.
+  return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+  var encodingParameters = [];
+  var description = SDPUtils.parseRtpParameters(mediaSection);
+  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
+  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
+
+  // filter a=ssrc:... cname:, ignore PlanB-msid
+  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'cname';
+  });
+  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+  var secondarySsrc;
+
+  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
+  .map(function(line) {
+    var parts = line.split(' ');
+    parts.shift();
+    return parts.map(function(part) {
+      return parseInt(part, 10);
+    });
+  });
+  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+    secondarySsrc = flows[0][1];
+  }
+
+  description.codecs.forEach(function(codec) {
+    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
+      var encParam = {
+        ssrc: primarySsrc,
+        codecPayloadType: parseInt(codec.parameters.apt, 10),
+        rtx: {
+          ssrc: secondarySsrc
+        }
+      };
+      encodingParameters.push(encParam);
+      if (hasRed) {
+        encParam = JSON.parse(JSON.stringify(encParam));
+        encParam.fec = {
+          ssrc: secondarySsrc,
+          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
+        };
+        encodingParameters.push(encParam);
+      }
+    }
+  });
+  if (encodingParameters.length === 0 && primarySsrc) {
+    encodingParameters.push({
+      ssrc: primarySsrc
+    });
+  }
+
+  // we support both b=AS and b=TIAS but interpret AS as TIAS.
+  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
+  if (bandwidth.length) {
+    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
+      bandwidth = parseInt(bandwidth[0].substr(7), 10);
+    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
+      // use formula from JSEP to convert b=AS to TIAS value.
+      bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
+          - (50 * 40 * 8);
+    } else {
+      bandwidth = undefined;
+    }
+    encodingParameters.forEach(function(params) {
+      params.maxBitrate = bandwidth;
+    });
+  }
+  return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+  var rtcpParameters = {};
+
+  var cname;
+  // Gets the first SSRC. Note that with RTX there might be multiple
+  // SSRCs.
+  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+      .map(function(line) {
+        return SDPUtils.parseSsrcMedia(line);
+      })
+      .filter(function(obj) {
+        return obj.attribute === 'cname';
+      })[0];
+  if (remoteSsrc) {
+    rtcpParameters.cname = remoteSsrc.value;
+    rtcpParameters.ssrc = remoteSsrc.ssrc;
+  }
+
+  // Edge uses the compound attribute instead of reducedSize
+  // compound is !reducedSize
+  var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
+  rtcpParameters.reducedSize = rsize.length > 0;
+  rtcpParameters.compound = rsize.length === 0;
+
+  // parses the rtcp-mux attrіbute.
+  // Note that Edge does not support unmuxed RTCP.
+  var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
+  rtcpParameters.mux = mux.length > 0;
+
+  return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+  var parts;
+  var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
+  if (spec.length === 1) {
+    parts = spec[0].substr(7).split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+  var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
+  .map(function(line) {
+    return SDPUtils.parseSsrcMedia(line);
+  })
+  .filter(function(parts) {
+    return parts.attribute === 'msid';
+  });
+  if (planB.length > 0) {
+    parts = planB[0].value.split(' ');
+    return {stream: parts[0], track: parts[1]};
+  }
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+  return Math.random().toString().substr(2, 21);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer) {
+  var sessionId;
+  var version = sessVer !== undefined ? sessVer : 2;
+  if (sessId) {
+    sessionId = sessId;
+  } else {
+    sessionId = SDPUtils.generateSessionId();
+  }
+  // FIXME: sess-id should be an NTP timestamp.
+  return 'v=0\r\n' +
+      'o=thisisadapterortc ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' +
+      's=-\r\n' +
+      't=0 0\r\n';
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+  // Map ICE parameters (ufrag, pwd) to SDP.
+  sdp += SDPUtils.writeIceParameters(
+      transceiver.iceGatherer.getLocalParameters());
+
+  // Map DTLS parameters to SDP.
+  sdp += SDPUtils.writeDtlsParameters(
+      transceiver.dtlsTransport.getLocalParameters(),
+      type === 'offer' ? 'actpass' : 'active');
+
+  sdp += 'a=mid:' + transceiver.mid + '\r\n';
+
+  if (transceiver.direction) {
+    sdp += 'a=' + transceiver.direction + '\r\n';
+  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+    sdp += 'a=sendrecv\r\n';
+  } else if (transceiver.rtpSender) {
+    sdp += 'a=sendonly\r\n';
+  } else if (transceiver.rtpReceiver) {
+    sdp += 'a=recvonly\r\n';
+  } else {
+    sdp += 'a=inactive\r\n';
+  }
+
+  if (transceiver.rtpSender) {
+    // spec.
+    var msid = 'msid:' + stream.id + ' ' +
+        transceiver.rtpSender.track.id + '\r\n';
+    sdp += 'a=' + msid;
+
+    // for Chrome.
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+        ' ' + msid;
+    if (transceiver.sendEncodingParameters[0].rtx) {
+      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+          ' ' + msid;
+      sdp += 'a=ssrc-group:FID ' +
+          transceiver.sendEncodingParameters[0].ssrc + ' ' +
+          transceiver.sendEncodingParameters[0].rtx.ssrc +
+          '\r\n';
+    }
+  }
+  // FIXME: this should be written by writeRtpDescription.
+  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
+      ' cname:' + SDPUtils.localCName + '\r\n';
+  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
+        ' cname:' + SDPUtils.localCName + '\r\n';
+  }
+  return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+  var lines = SDPUtils.splitLines(mediaSection);
+  for (var i = 0; i < lines.length; i++) {
+    switch (lines[i]) {
+      case 'a=sendrecv':
+      case 'a=sendonly':
+      case 'a=recvonly':
+      case 'a=inactive':
+        return lines[i].substr(2);
+      default:
+        // FIXME: What should happen here?
+    }
+  }
+  if (sessionpart) {
+    return SDPUtils.getDirection(sessionpart);
+  }
+  return 'sendrecv';
+};
+
+SDPUtils.getKind = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+  return mediaSection.split(' ', 2)[1] === '0';
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+  var lines = SDPUtils.splitLines(mediaSection);
+  var mline = lines[0].split(' ');
+  return {
+    kind: mline[0].substr(2),
+    port: parseInt(mline[1], 10),
+    protocol: mline[2],
+    fmt: mline.slice(3).join(' ')
+  };
+};
+
+// Expose public methods.
+if (typeof module === 'object') {
+  module.exports = SDPUtils;
+}
+
+},{}],3:[function(require,module,exports){
+(function (global){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var adapterFactory = require('./adapter_factory.js');
+module.exports = adapterFactory({window: global.window});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./adapter_factory.js":4}],4:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+
+'use strict';
+
+var utils = require('./utils');
+// Shimming starts here.
+module.exports = function(dependencies, opts) {
+  var window = dependencies && dependencies.window;
+
+  var options = {
+    shimChrome: true,
+    shimFirefox: true,
+    shimEdge: true,
+    shimSafari: true,
+  };
+
+  for (var key in opts) {
+    if (hasOwnProperty.call(opts, key)) {
+      options[key] = opts[key];
+    }
+  }
+
+  // Utils.
+  var logging = utils.log;
+  var browserDetails = utils.detectBrowser(window);
+
+  // Export to the adapter global object visible in the browser.
+  var adapter = {
+    browserDetails: browserDetails,
+    extractVersion: utils.extractVersion,
+    disableLog: utils.disableLog,
+    disableWarnings: utils.disableWarnings
+  };
+
+  // Uncomment the line below if you want logging to occur, including logging
+  // for the switch statement below. Can also be turned on in the browser via
+  // adapter.disableLog(false), but then logging from the switch statement below
+  // will not appear.
+  // require('./utils').disableLog(false);
+
+  // Browser shims.
+  var chromeShim = require('./chrome/chrome_shim') || null;
+  var edgeShim = require('./edge/edge_shim') || null;
+  var firefoxShim = require('./firefox/firefox_shim') || null;
+  var safariShim = require('./safari/safari_shim') || null;
+  var commonShim = require('./common_shim') || null;
+
+  // Shim browser if found.
+  switch (browserDetails.browser) {
+    case 'chrome':
+      if (!chromeShim || !chromeShim.shimPeerConnection ||
+          !options.shimChrome) {
+        logging('Chrome shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming chrome.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = chromeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      chromeShim.shimGetUserMedia(window);
+      chromeShim.shimMediaStream(window);
+      chromeShim.shimSourceObject(window);
+      chromeShim.shimPeerConnection(window);
+      chromeShim.shimOnTrack(window);
+      chromeShim.shimAddTrackRemoveTrack(window);
+      chromeShim.shimGetSendersWithDtmf(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'firefox':
+      if (!firefoxShim || !firefoxShim.shimPeerConnection ||
+          !options.shimFirefox) {
+        logging('Firefox shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming firefox.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = firefoxShim;
+      commonShim.shimCreateObjectURL(window);
+
+      firefoxShim.shimGetUserMedia(window);
+      firefoxShim.shimSourceObject(window);
+      firefoxShim.shimPeerConnection(window);
+      firefoxShim.shimOnTrack(window);
+      firefoxShim.shimRemoveStream(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    case 'edge':
+      if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
+        logging('MS edge shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming edge.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = edgeShim;
+      commonShim.shimCreateObjectURL(window);
+
+      edgeShim.shimGetUserMedia(window);
+      edgeShim.shimPeerConnection(window);
+      edgeShim.shimReplaceTrack(window);
+
+      // the edge shim implements the full RTCIceCandidate object.
+      break;
+    case 'safari':
+      if (!safariShim || !options.shimSafari) {
+        logging('Safari shim is not included in this adapter release.');
+        return adapter;
+      }
+      logging('adapter.js shimming safari.');
+      // Export to the adapter global object visible in the browser.
+      adapter.browserShim = safariShim;
+      commonShim.shimCreateObjectURL(window);
+
+      safariShim.shimRTCIceServerUrls(window);
+      safariShim.shimCallbacksAPI(window);
+      safariShim.shimLocalStreamsAPI(window);
+      safariShim.shimRemoteStreamsAPI(window);
+      safariShim.shimTrackEventTransceiver(window);
+      safariShim.shimGetUserMedia(window);
+      safariShim.shimCreateOfferLegacy(window);
+
+      commonShim.shimRTCIceCandidate(window);
+      break;
+    default:
+      logging('Unsupported browser!');
+      break;
+  }
+
+  return adapter;
+};
+
+},{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":10,"./safari/safari_shim":12,"./utils":13}],5:[function(require,module,exports){
+
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+var chromeShim = {
+  shimMediaStream: function(window) {
+    window.MediaStream = window.MediaStream || window.webkitMediaStream;
+  },
+
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+          }
+          this.addEventListener('track', this._ontrack = f);
+        }
+      });
+      var origSetRemoteDescription =
+          window.RTCPeerConnection.prototype.setRemoteDescription;
+      window.RTCPeerConnection.prototype.setRemoteDescription = function() {
+        var pc = this;
+        if (!pc._ontrackpoly) {
+          pc._ontrackpoly = function(e) {
+            // onaddstream does not fire when a track is added to an existing
+            // stream. But stream.onaddtrack is implemented so we use that.
+            e.stream.addEventListener('addtrack', function(te) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === te.track.id;
+                });
+              } else {
+                receiver = {track: te.track};
+              }
+
+              var event = new Event('track');
+              event.track = te.track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+            e.stream.getTracks().forEach(function(track) {
+              var receiver;
+              if (window.RTCPeerConnection.prototype.getReceivers) {
+                receiver = pc.getReceivers().find(function(r) {
+                  return r.track && r.track.id === track.id;
+                });
+              } else {
+                receiver = {track: track};
+              }
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = receiver;
+              event.transceiver = {receiver: receiver};
+              event.streams = [e.stream];
+              pc.dispatchEvent(event);
+            });
+          };
+          pc.addEventListener('addstream', pc._ontrackpoly);
+        }
+        return origSetRemoteDescription.apply(pc, arguments);
+      };
+    }
+  },
+
+  shimGetSendersWithDtmf: function(window) {
+    // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        !('getSenders' in window.RTCPeerConnection.prototype) &&
+        'createDTMFSender' in window.RTCPeerConnection.prototype) {
+      var shimSenderWithDtmf = function(pc, track) {
+        return {
+          track: track,
+          get dtmf() {
+            if (this._dtmf === undefined) {
+              if (track.kind === 'audio') {
+                this._dtmf = pc.createDTMFSender(track);
+              } else {
+                this._dtmf = null;
+              }
+            }
+            return this._dtmf;
+          },
+          _pc: pc
+        };
+      };
+
+      // augment addTrack when getSenders is not available.
+      if (!window.RTCPeerConnection.prototype.getSenders) {
+        window.RTCPeerConnection.prototype.getSenders = function() {
+          this._senders = this._senders || [];
+          return this._senders.slice(); // return a copy of the internal state.
+        };
+        var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
+        window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+          var pc = this;
+          var sender = origAddTrack.apply(pc, arguments);
+          if (!sender) {
+            sender = shimSenderWithDtmf(pc, track);
+            pc._senders.push(sender);
+          }
+          return sender;
+        };
+
+        var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
+        window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+          var pc = this;
+          origRemoveTrack.apply(pc, arguments);
+          var idx = pc._senders.indexOf(sender);
+          if (idx !== -1) {
+            pc._senders.splice(idx, 1);
+          }
+        };
+      }
+      var origAddStream = window.RTCPeerConnection.prototype.addStream;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origAddStream.apply(pc, [stream]);
+        stream.getTracks().forEach(function(track) {
+          pc._senders.push(shimSenderWithDtmf(pc, track));
+        });
+      };
+
+      var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        var pc = this;
+        pc._senders = pc._senders || [];
+        origRemoveStream.apply(pc, [stream]);
+
+        stream.getTracks().forEach(function(track) {
+          var sender = pc._senders.find(function(s) {
+            return s.track === track;
+          });
+          if (sender) {
+            pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender
+          }
+        });
+      };
+    } else if (typeof window === 'object' && window.RTCPeerConnection &&
+               'getSenders' in window.RTCPeerConnection.prototype &&
+               'createDTMFSender' in window.RTCPeerConnection.prototype &&
+               window.RTCRtpSender &&
+               !('dtmf' in window.RTCRtpSender.prototype)) {
+      var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
+      window.RTCPeerConnection.prototype.getSenders = function() {
+        var pc = this;
+        var senders = origGetSenders.apply(pc, []);
+        senders.forEach(function(sender) {
+          sender._pc = pc;
+        });
+        return senders;
+      };
+
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = this._pc.createDTMFSender(this.track);
+            } else {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    var URL = window && window.URL;
+
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this._srcObject;
+          },
+          set: function(stream) {
+            var self = this;
+            // Use _srcObject as a private property for this shim
+            this._srcObject = stream;
+            if (this.src) {
+              URL.revokeObjectURL(this.src);
+            }
+
+            if (!stream) {
+              this.src = '';
+              return undefined;
+            }
+            this.src = URL.createObjectURL(stream);
+            // We need to recreate the blob url when a track is added or
+            // removed. Doing it manually since we want to avoid a recursion.
+            stream.addEventListener('addtrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+            stream.addEventListener('removetrack', function() {
+              if (self.src) {
+                URL.revokeObjectURL(self.src);
+              }
+              self.src = URL.createObjectURL(stream);
+            });
+          }
+        });
+      }
+    }
+  },
+
+  shimAddTrackRemoveTrack: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+    // shim addTrack and removeTrack.
+    if (window.RTCPeerConnection.prototype.addTrack &&
+        browserDetails.version >= 64) {
+      return;
+    }
+
+    // also shim pc.getLocalStreams when addTrack is shimmed
+    // to return the original streams.
+    var origGetLocalStreams = window.RTCPeerConnection.prototype
+        .getLocalStreams;
+    window.RTCPeerConnection.prototype.getLocalStreams = function() {
+      var self = this;
+      var nativeStreams = origGetLocalStreams.apply(this);
+      self._reverseStreams = self._reverseStreams || {};
+      return nativeStreams.map(function(stream) {
+        return self._reverseStreams[stream.id];
+      });
+    };
+
+    var origAddStream = window.RTCPeerConnection.prototype.addStream;
+    window.RTCPeerConnection.prototype.addStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      stream.getTracks().forEach(function(track) {
+        var alreadyExists = pc.getSenders().find(function(s) {
+          return s.track === track;
+        });
+        if (alreadyExists) {
+          throw new DOMException('Track already exists.',
+              'InvalidAccessError');
+        }
+      });
+      // Add identity mapping for consistency with addTrack.
+      // Unless this is being used with a stream from addTrack.
+      if (!pc._reverseStreams[stream.id]) {
+        var newStream = new window.MediaStream(stream.getTracks());
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        stream = newStream;
+      }
+      origAddStream.apply(pc, [stream]);
+    };
+
+    var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+
+      origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]);
+      delete pc._reverseStreams[(pc._streams[stream.id] ?
+          pc._streams[stream.id].id : stream.id)];
+      delete pc._streams[stream.id];
+    };
+
+    window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      var streams = [].slice.call(arguments, 1);
+      if (streams.length !== 1 ||
+          !streams[0].getTracks().find(function(t) {
+            return t === track;
+          })) {
+        // this is not fully correct but all we can manage without
+        // [[associated MediaStreams]] internal slot.
+        throw new DOMException(
+          'The adapter.js addTrack polyfill only supports a single ' +
+          ' stream which is associated with the specified track.',
+          'NotSupportedError');
+      }
+
+      var alreadyExists = pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+      if (alreadyExists) {
+        throw new DOMException('Track already exists.',
+            'InvalidAccessError');
+      }
+
+      pc._streams = pc._streams || {};
+      pc._reverseStreams = pc._reverseStreams || {};
+      var oldStream = pc._streams[stream.id];
+      if (oldStream) {
+        // this is using odd Chrome behaviour, use with caution:
+        // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
+        // Note: we rely on the high-level addTrack/dtmf shim to
+        // create the sender with a dtmf sender.
+        oldStream.addTrack(track);
+
+        // Trigger ONN async.
+        Promise.resolve().then(function() {
+          pc.dispatchEvent(new Event('negotiationneeded'));
+        });
+      } else {
+        var newStream = new window.MediaStream([track]);
+        pc._streams[stream.id] = newStream;
+        pc._reverseStreams[newStream.id] = stream;
+        pc.addStream(newStream);
+      }
+      return pc.getSenders().find(function(s) {
+        return s.track === track;
+      });
+    };
+
+    // replace the internal stream id with the external one and
+    // vice versa.
+    function replaceInternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(internalStream.id, 'g'),
+            externalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    function replaceExternalStreamId(pc, description) {
+      var sdp = description.sdp;
+      Object.keys(pc._reverseStreams || []).forEach(function(internalId) {
+        var externalStream = pc._reverseStreams[internalId];
+        var internalStream = pc._streams[externalStream.id];
+        sdp = sdp.replace(new RegExp(externalStream.id, 'g'),
+            internalStream.id);
+      });
+      return new RTCSessionDescription({
+        type: description.type,
+        sdp: sdp
+      });
+    }
+    ['createOffer', 'createAnswer'].forEach(function(method) {
+      var nativeMethod = window.RTCPeerConnection.prototype[method];
+      window.RTCPeerConnection.prototype[method] = function() {
+        var pc = this;
+        var args = arguments;
+        var isLegacyCall = arguments.length &&
+            typeof arguments[0] === 'function';
+        if (isLegacyCall) {
+          return nativeMethod.apply(pc, [
+            function(description) {
+              var desc = replaceInternalStreamId(pc, description);
+              args[0].apply(null, [desc]);
+            },
+            function(err) {
+              if (args[1]) {
+                args[1].apply(null, err);
+              }
+            }, arguments[2]
+          ]);
+        }
+        return nativeMethod.apply(pc, arguments)
+        .then(function(description) {
+          return replaceInternalStreamId(pc, description);
+        });
+      };
+    });
+
+    var origSetLocalDescription =
+        window.RTCPeerConnection.prototype.setLocalDescription;
+    window.RTCPeerConnection.prototype.setLocalDescription = function() {
+      var pc = this;
+      if (!arguments.length || !arguments[0].type) {
+        return origSetLocalDescription.apply(pc, arguments);
+      }
+      arguments[0] = replaceExternalStreamId(pc, arguments[0]);
+      return origSetLocalDescription.apply(pc, arguments);
+    };
+
+    // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier
+
+    var origLocalDescription = Object.getOwnPropertyDescriptor(
+        window.RTCPeerConnection.prototype, 'localDescription');
+    Object.defineProperty(window.RTCPeerConnection.prototype,
+        'localDescription', {
+          get: function() {
+            var pc = this;
+            var description = origLocalDescription.get.apply(this);
+            if (description.type === '') {
+              return description;
+            }
+            return replaceInternalStreamId(pc, description);
+          }
+        });
+
+    window.RTCPeerConnection.prototype.removeTrack = function(sender) {
+      var pc = this;
+      if (pc.signalingState === 'closed') {
+        throw new DOMException(
+          'The RTCPeerConnection\'s signalingState is \'closed\'.',
+          'InvalidStateError');
+      }
+      // We can not yet check for sender instanceof RTCRtpSender
+      // since we shim RTPSender. So we check if sender._pc is set.
+      if (!sender._pc) {
+        throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' +
+            'does not implement interface RTCRtpSender.', 'TypeError');
+      }
+      var isLocal = sender._pc === pc;
+      if (!isLocal) {
+        throw new DOMException('Sender was not created by this connection.',
+            'InvalidAccessError');
+      }
+
+      // Search for the native stream the senders track belongs to.
+      pc._streams = pc._streams || {};
+      var stream;
+      Object.keys(pc._streams).forEach(function(streamid) {
+        var hasTrack = pc._streams[streamid].getTracks().find(function(track) {
+          return sender.track === track;
+        });
+        if (hasTrack) {
+          stream = pc._streams[streamid];
+        }
+      });
+
+      if (stream) {
+        if (stream.getTracks().length === 1) {
+          // if this is the last track of the stream, remove the stream. This
+          // takes care of any shimmed _senders.
+          pc.removeStream(pc._reverseStreams[stream.id]);
+        } else {
+          // relying on the same odd chrome behaviour as above.
+          stream.removeTrack(sender.track);
+        }
+        pc.dispatchEvent(new Event('negotiationneeded'));
+      }
+    };
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        // Translate iceTransportPolicy to iceTransports,
+        // see https://code.google.com/p/webrtc/issues/detail?id=4869
+        // this was fixed in M56 along with unprefixing RTCPeerConnection.
+        logging('PeerConnection');
+        if (pcConfig && pcConfig.iceTransportPolicy) {
+          pcConfig.iceTransports = pcConfig.iceTransportPolicy;
+        }
+
+        return new window.webkitRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.webkitRTCPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      if (window.webkitRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.webkitRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+    } else {
+      // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+      var OrigPeerConnection = window.RTCPeerConnection;
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (pcConfig && pcConfig.iceServers) {
+          var newIceServers = [];
+          for (var i = 0; i < pcConfig.iceServers.length; i++) {
+            var server = pcConfig.iceServers[i];
+            if (!server.hasOwnProperty('urls') &&
+                server.hasOwnProperty('url')) {
+              utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+              server = JSON.parse(JSON.stringify(server));
+              server.urls = server.url;
+              newIceServers.push(server);
+            } else {
+              newIceServers.push(pcConfig.iceServers[i]);
+            }
+          }
+          pcConfig.iceServers = newIceServers;
+        }
+        return new OrigPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+      // wrap static methods. Currently just generateCertificate.
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+
+    var origGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(selector,
+        successCallback, errorCallback) {
+      var self = this;
+      var args = arguments;
+
+      // If selector is a function then we are in the old style stats so just
+      // pass back the original getStats format to avoid breaking old users.
+      if (arguments.length > 0 && typeof selector === 'function') {
+        return origGetStats.apply(this, arguments);
+      }
+
+      // When spec-style getStats is supported, return those when called with
+      // either no arguments or the selector argument is null.
+      if (origGetStats.length === 0 && (arguments.length === 0 ||
+          typeof arguments[0] !== 'function')) {
+        return origGetStats.apply(this, []);
+      }
+
+      var fixChromeStats_ = function(response) {
+        var standardReport = {};
+        var reports = response.result();
+        reports.forEach(function(report) {
+          var standardStats = {
+            id: report.id,
+            timestamp: report.timestamp,
+            type: {
+              localcandidate: 'local-candidate',
+              remotecandidate: 'remote-candidate'
+            }[report.type] || report.type
+          };
+          report.names().forEach(function(name) {
+            standardStats[name] = report.stat(name);
+          });
+          standardReport[standardStats.id] = standardStats;
+        });
+
+        return standardReport;
+      };
+
+      // shim getStats with maplike support
+      var makeMapStats = function(stats) {
+        return new Map(Object.keys(stats).map(function(key) {
+          return [key, stats[key]];
+        }));
+      };
+
+      if (arguments.length >= 2) {
+        var successCallbackWrapper_ = function(response) {
+          args[1](makeMapStats(fixChromeStats_(response)));
+        };
+
+        return origGetStats.apply(this, [successCallbackWrapper_,
+          arguments[0]]);
+      }
+
+      // promise-support
+      return new Promise(function(resolve, reject) {
+        origGetStats.apply(self, [
+          function(response) {
+            resolve(makeMapStats(fixChromeStats_(response)));
+          }, reject]);
+      }).then(successCallback, errorCallback);
+    };
+
+    // add promise support -- natively available in Chrome 51
+    if (browserDetails.version < 51) {
+      ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+          .forEach(function(method) {
+            var nativeMethod = window.RTCPeerConnection.prototype[method];
+            window.RTCPeerConnection.prototype[method] = function() {
+              var args = arguments;
+              var self = this;
+              var promise = new Promise(function(resolve, reject) {
+                nativeMethod.apply(self, [args[0], resolve, reject]);
+              });
+              if (args.length < 2) {
+                return promise;
+              }
+              return promise.then(function() {
+                args[1].apply(null, []);
+              },
+              function(err) {
+                if (args.length >= 3) {
+                  args[2].apply(null, [err]);
+                }
+              });
+            };
+          });
+    }
+
+    // promise support for createOffer and createAnswer. Available (without
+    // bugs) since M52: crbug/619289
+    if (browserDetails.version < 52) {
+      ['createOffer', 'createAnswer'].forEach(function(method) {
+        var nativeMethod = window.RTCPeerConnection.prototype[method];
+        window.RTCPeerConnection.prototype[method] = function() {
+          var self = this;
+          if (arguments.length < 1 || (arguments.length === 1 &&
+              typeof arguments[0] === 'object')) {
+            var opts = arguments.length === 1 ? arguments[0] : undefined;
+            return new Promise(function(resolve, reject) {
+              nativeMethod.apply(self, [resolve, reject, opts]);
+            });
+          }
+          return nativeMethod.apply(this, arguments);
+        };
+      });
+    }
+
+    // shim implicit creation of RTCSessionDescription/RTCIceCandidate
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+  }
+};
+
+
+// Expose public methods.
+module.exports = {
+  shimMediaStream: chromeShim.shimMediaStream,
+  shimOnTrack: chromeShim.shimOnTrack,
+  shimAddTrackRemoveTrack: chromeShim.shimAddTrackRemoveTrack,
+  shimGetSendersWithDtmf: chromeShim.shimGetSendersWithDtmf,
+  shimSourceObject: chromeShim.shimSourceObject,
+  shimPeerConnection: chromeShim.shimPeerConnection,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils.js":13,"./getusermedia":6}],6:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+var utils = require('../utils.js');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+
+  var constraintsToChrome_ = function(c) {
+    if (typeof c !== 'object' || c.mandatory || c.optional) {
+      return c;
+    }
+    var cc = {};
+    Object.keys(c).forEach(function(key) {
+      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+        return;
+      }
+      var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
+      if (r.exact !== undefined && typeof r.exact === 'number') {
+        r.min = r.max = r.exact;
+      }
+      var oldname_ = function(prefix, name) {
+        if (prefix) {
+          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
+        }
+        return (name === 'deviceId') ? 'sourceId' : name;
+      };
+      if (r.ideal !== undefined) {
+        cc.optional = cc.optional || [];
+        var oc = {};
+        if (typeof r.ideal === 'number') {
+          oc[oldname_('min', key)] = r.ideal;
+          cc.optional.push(oc);
+          oc = {};
+          oc[oldname_('max', key)] = r.ideal;
+          cc.optional.push(oc);
+        } else {
+          oc[oldname_('', key)] = r.ideal;
+          cc.optional.push(oc);
+        }
+      }
+      if (r.exact !== undefined && typeof r.exact !== 'number') {
+        cc.mandatory = cc.mandatory || {};
+        cc.mandatory[oldname_('', key)] = r.exact;
+      } else {
+        ['min', 'max'].forEach(function(mix) {
+          if (r[mix] !== undefined) {
+            cc.mandatory = cc.mandatory || {};
+            cc.mandatory[oldname_(mix, key)] = r[mix];
+          }
+        });
+      }
+    });
+    if (c.advanced) {
+      cc.optional = (cc.optional || []).concat(c.advanced);
+    }
+    return cc;
+  };
+
+  var shimConstraints_ = function(constraints, func) {
+    if (browserDetails.version >= 61) {
+      return func(constraints);
+    }
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (constraints && typeof constraints.audio === 'object') {
+      var remap = function(obj, a, b) {
+        if (a in obj && !(b in obj)) {
+          obj[b] = obj[a];
+          delete obj[a];
+        }
+      };
+      constraints = JSON.parse(JSON.stringify(constraints));
+      remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
+      remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
+      constraints.audio = constraintsToChrome_(constraints.audio);
+    }
+    if (constraints && typeof constraints.video === 'object') {
+      // Shim facingMode for mobile & surface pro.
+      var face = constraints.video.facingMode;
+      face = face && ((typeof face === 'object') ? face : {ideal: face});
+      var getSupportedFacingModeLies = browserDetails.version < 66;
+
+      if ((face && (face.exact === 'user' || face.exact === 'environment' ||
+                    face.ideal === 'user' || face.ideal === 'environment')) &&
+          !(navigator.mediaDevices.getSupportedConstraints &&
+            navigator.mediaDevices.getSupportedConstraints().facingMode &&
+            !getSupportedFacingModeLies)) {
+        delete constraints.video.facingMode;
+        var matches;
+        if (face.exact === 'environment' || face.ideal === 'environment') {
+          matches = ['back', 'rear'];
+        } else if (face.exact === 'user' || face.ideal === 'user') {
+          matches = ['front'];
+        }
+        if (matches) {
+          // Look for matches in label, or use last cam for back (typical).
+          return navigator.mediaDevices.enumerateDevices()
+          .then(function(devices) {
+            devices = devices.filter(function(d) {
+              return d.kind === 'videoinput';
+            });
+            var dev = devices.find(function(d) {
+              return matches.some(function(match) {
+                return d.label.toLowerCase().indexOf(match) !== -1;
+              });
+            });
+            if (!dev && devices.length && matches.indexOf('back') !== -1) {
+              dev = devices[devices.length - 1]; // more likely the back cam
+            }
+            if (dev) {
+              constraints.video.deviceId = face.exact ? {exact: dev.deviceId} :
+                                                        {ideal: dev.deviceId};
+            }
+            constraints.video = constraintsToChrome_(constraints.video);
+            logging('chrome: ' + JSON.stringify(constraints));
+            return func(constraints);
+          });
+        }
+      }
+      constraints.video = constraintsToChrome_(constraints.video);
+    }
+    logging('chrome: ' + JSON.stringify(constraints));
+    return func(constraints);
+  };
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        PermissionDeniedError: 'NotAllowedError',
+        InvalidStateError: 'NotReadableError',
+        DevicesNotFoundError: 'NotFoundError',
+        ConstraintNotSatisfiedError: 'OverconstrainedError',
+        TrackStartError: 'NotReadableError',
+        MediaDeviceFailedDueToShutdown: 'NotReadableError',
+        MediaDeviceKillSwitchOn: 'NotReadableError'
+      }[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraintName,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    shimConstraints_(constraints, function(c) {
+      navigator.webkitGetUserMedia(c, onSuccess, function(e) {
+        if (onError) {
+          onError(shimError_(e));
+        }
+      });
+    });
+  };
+
+  navigator.getUserMedia = getUserMedia_;
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      navigator.getUserMedia(constraints, resolve, reject);
+    });
+  };
+
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {
+      getUserMedia: getUserMediaPromise_,
+      enumerateDevices: function() {
+        return new Promise(function(resolve) {
+          var kinds = {audio: 'audioinput', video: 'videoinput'};
+          return window.MediaStreamTrack.getSources(function(devices) {
+            resolve(devices.map(function(device) {
+              return {label: device.label,
+                kind: kinds[device.kind],
+                deviceId: device.id,
+                groupId: ''};
+            }));
+          });
+        });
+      },
+      getSupportedConstraints: function() {
+        return {
+          deviceId: true, echoCancellation: true, facingMode: true,
+          frameRate: true, height: true, width: true
+        };
+      }
+    };
+  }
+
+  // A shim for getUserMedia method on the mediaDevices object.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (!navigator.mediaDevices.getUserMedia) {
+    navigator.mediaDevices.getUserMedia = function(constraints) {
+      return getUserMediaPromise_(constraints);
+    };
+  } else {
+    // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
+    // function which returns a Promise, it does not accept spec-style
+    // constraints.
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(cs) {
+      return shimConstraints_(cs, function(c) {
+        return origGetUserMedia(c).then(function(stream) {
+          if (c.audio && !stream.getAudioTracks().length ||
+              c.video && !stream.getVideoTracks().length) {
+            stream.getTracks().forEach(function(track) {
+              track.stop();
+            });
+            throw new DOMException('', 'NotFoundError');
+          }
+          return stream;
+        }, function(e) {
+          return Promise.reject(shimError_(e));
+        });
+      });
+    };
+  }
+
+  // Dummy devicechange event methods.
+  // TODO(KaptenJansson) remove once implemented in Chrome stable.
+  if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
+    navigator.mediaDevices.addEventListener = function() {
+      logging('Dummy mediaDevices.addEventListener called.');
+    };
+  }
+  if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
+    navigator.mediaDevices.removeEventListener = function() {
+      logging('Dummy mediaDevices.removeEventListener called.');
+    };
+  }
+};
+
+},{"../utils.js":13}],7:[function(require,module,exports){
+/*
+ *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var SDPUtils = require('sdp');
+var utils = require('./utils');
+
+// Wraps the peerconnection event eventNameToWrap in a function
+// which returns the modified event object.
+function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
+  if (!window.RTCPeerConnection) {
+    return;
+  }
+  var proto = window.RTCPeerConnection.prototype;
+  var nativeAddEventListener = proto.addEventListener;
+  proto.addEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap) {
+      return nativeAddEventListener.apply(this, arguments);
+    }
+    var wrappedCallback = function(e) {
+      cb(wrapper(e));
+    };
+    this._eventMap = this._eventMap || {};
+    this._eventMap[cb] = wrappedCallback;
+    return nativeAddEventListener.apply(this, [nativeEventName,
+      wrappedCallback]);
+  };
+
+  var nativeRemoveEventListener = proto.removeEventListener;
+  proto.removeEventListener = function(nativeEventName, cb) {
+    if (nativeEventName !== eventNameToWrap || !this._eventMap
+        || !this._eventMap[cb]) {
+      return nativeRemoveEventListener.apply(this, arguments);
+    }
+    var unwrappedCb = this._eventMap[cb];
+    delete this._eventMap[cb];
+    return nativeRemoveEventListener.apply(this, [nativeEventName,
+      unwrappedCb]);
+  };
+
+  Object.defineProperty(proto, 'on' + eventNameToWrap, {
+    get: function() {
+      return this['_on' + eventNameToWrap];
+    },
+    set: function(cb) {
+      if (this['_on' + eventNameToWrap]) {
+        this.removeEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap]);
+        delete this['_on' + eventNameToWrap];
+      }
+      if (cb) {
+        this.addEventListener(eventNameToWrap,
+            this['_on' + eventNameToWrap] = cb);
+      }
+    }
+  });
+}
+
+module.exports = {
+  shimRTCIceCandidate: function(window) {
+    // foundation is arbitrarily chosen as an indicator for full support for
+    // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
+    if (window.RTCIceCandidate && 'foundation' in
+        window.RTCIceCandidate.prototype) {
+      return;
+    }
+
+    var NativeRTCIceCandidate = window.RTCIceCandidate;
+    window.RTCIceCandidate = function(args) {
+      // Remove the a= which shouldn't be part of the candidate string.
+      if (typeof args === 'object' && args.candidate &&
+          args.candidate.indexOf('a=') === 0) {
+        args = JSON.parse(JSON.stringify(args));
+        args.candidate = args.candidate.substr(2);
+      }
+
+      // Augment the native candidate with the parsed fields.
+      var nativeCandidate = new NativeRTCIceCandidate(args);
+      var parsedCandidate = SDPUtils.parseCandidate(args.candidate);
+      var augmentedCandidate = Object.assign(nativeCandidate,
+          parsedCandidate);
+
+      // Add a serializer that does not serialize the extra attributes.
+      augmentedCandidate.toJSON = function() {
+        return {
+          candidate: augmentedCandidate.candidate,
+          sdpMid: augmentedCandidate.sdpMid,
+          sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
+          usernameFragment: augmentedCandidate.usernameFragment,
+        };
+      };
+      return augmentedCandidate;
+    };
+
+    // Hook up the augmented candidate in onicecandidate and
+    // addEventListener('icecandidate', ...)
+    wrapPeerConnectionEvent(window, 'icecandidate', function(e) {
+      if (e.candidate) {
+        Object.defineProperty(e, 'candidate', {
+          value: new window.RTCIceCandidate(e.candidate),
+          writable: 'false'
+        });
+      }
+      return e;
+    });
+  },
+
+  // shimCreateObjectURL must be called before shimSourceObject to avoid loop.
+
+  shimCreateObjectURL: function(window) {
+    var URL = window && window.URL;
+
+    if (!(typeof window === 'object' && window.HTMLMediaElement &&
+          'srcObject' in window.HTMLMediaElement.prototype &&
+        URL.createObjectURL && URL.revokeObjectURL)) {
+      // Only shim CreateObjectURL using srcObject if srcObject exists.
+      return undefined;
+    }
+
+    var nativeCreateObjectURL = URL.createObjectURL.bind(URL);
+    var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL);
+    var streams = new Map(), newId = 0;
+
+    URL.createObjectURL = function(stream) {
+      if ('getTracks' in stream) {
+        var url = 'polyblob:' + (++newId);
+        streams.set(url, stream);
+        utils.deprecated('URL.createObjectURL(stream)',
+            'elem.srcObject = stream');
+        return url;
+      }
+      return nativeCreateObjectURL(stream);
+    };
+    URL.revokeObjectURL = function(url) {
+      nativeRevokeObjectURL(url);
+      streams.delete(url);
+    };
+
+    var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype,
+                                              'src');
+    Object.defineProperty(window.HTMLMediaElement.prototype, 'src', {
+      get: function() {
+        return dsc.get.apply(this);
+      },
+      set: function(url) {
+        this.srcObject = streams.get(url) || null;
+        return dsc.set.apply(this, [url]);
+      }
+    });
+
+    var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute;
+    window.HTMLMediaElement.prototype.setAttribute = function() {
+      if (arguments.length === 2 &&
+          ('' + arguments[0]).toLowerCase() === 'src') {
+        this.srcObject = streams.get(arguments[1]) || null;
+      }
+      return nativeSetAttribute.apply(this, arguments);
+    };
+  }
+};
+
+},{"./utils":13,"sdp":2}],8:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var shimRTCPeerConnection = require('rtcpeerconnection-shim');
+
+module.exports = {
+  shimGetUserMedia: require('./getusermedia'),
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (window.RTCIceGatherer) {
+      // ORTC defines an RTCIceCandidate object but no constructor.
+      // Not implemented in Edge.
+      if (!window.RTCIceCandidate) {
+        window.RTCIceCandidate = function(args) {
+          return args;
+        };
+      }
+      // ORTC does not have a session description object but
+      // other browsers (i.e. Chrome) that will support both PC and ORTC
+      // in the future might have this defined already.
+      if (!window.RTCSessionDescription) {
+        window.RTCSessionDescription = function(args) {
+          return args;
+        };
+      }
+      // this adds an additional event listener to MediaStrackTrack that signals
+      // when a tracks enabled property was changed. Workaround for a bug in
+      // addStream, see below. No longer required in 15025+
+      if (browserDetails.version < 15025) {
+        var origMSTEnabled = Object.getOwnPropertyDescriptor(
+            window.MediaStreamTrack.prototype, 'enabled');
+        Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', {
+          set: function(value) {
+            origMSTEnabled.set.call(this, value);
+            var ev = new Event('enabled');
+            ev.enabled = value;
+            this.dispatchEvent(ev);
+          }
+        });
+      }
+    }
+
+    // ORTC defines the DTMF sender a bit different.
+    // https://github.com/w3c/ortc/issues/714
+    if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
+      Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
+        get: function() {
+          if (this._dtmf === undefined) {
+            if (this.track.kind === 'audio') {
+              this._dtmf = new window.RTCDtmfSender(this);
+            } else if (this.track.kind === 'video') {
+              this._dtmf = null;
+            }
+          }
+          return this._dtmf;
+        }
+      });
+    }
+
+    window.RTCPeerConnection =
+        shimRTCPeerConnection(window, browserDetails.version);
+  },
+  shimReplaceTrack: function(window) {
+    // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614
+    if (window.RTCRtpSender &&
+        !('replaceTrack' in window.RTCRtpSender.prototype)) {
+      window.RTCRtpSender.prototype.replaceTrack =
+          window.RTCRtpSender.prototype.setTrack;
+    }
+  }
+};
+
+},{"../utils":13,"./getusermedia":9,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+// Expose public methods.
+module.exports = function(window) {
+  var navigator = window && window.navigator;
+
+  var shimError_ = function(e) {
+    return {
+      name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name,
+      message: e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name;
+      }
+    };
+  };
+
+  // getUserMedia error shim.
+  var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+      bind(navigator.mediaDevices);
+  navigator.mediaDevices.getUserMedia = function(c) {
+    return origGetUserMedia(c).catch(function(e) {
+      return Promise.reject(shimError_(e));
+    });
+  };
+};
+
+},{}],10:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+
+var firefoxShim = {
+  shimOnTrack: function(window) {
+    if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
+        window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
+        get: function() {
+          return this._ontrack;
+        },
+        set: function(f) {
+          if (this._ontrack) {
+            this.removeEventListener('track', this._ontrack);
+            this.removeEventListener('addstream', this._ontrackpoly);
+          }
+          this.addEventListener('track', this._ontrack = f);
+          this.addEventListener('addstream', this._ontrackpoly = function(e) {
+            e.stream.getTracks().forEach(function(track) {
+              var event = new Event('track');
+              event.track = track;
+              event.receiver = {track: track};
+              event.transceiver = {receiver: event.receiver};
+              event.streams = [e.stream];
+              this.dispatchEvent(event);
+            }.bind(this));
+          }.bind(this));
+        }
+      });
+    }
+    if (typeof window === 'object' && window.RTCTrackEvent &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        !('transceiver' in window.RTCTrackEvent.prototype)) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimSourceObject: function(window) {
+    // Firefox has supported mozSrcObject since FF22, unprefixed in 42.
+    if (typeof window === 'object') {
+      if (window.HTMLMediaElement &&
+        !('srcObject' in window.HTMLMediaElement.prototype)) {
+        // Shim the srcObject property, once, when HTMLMediaElement is found.
+        Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
+          get: function() {
+            return this.mozSrcObject;
+          },
+          set: function(stream) {
+            this.mozSrcObject = stream;
+          }
+        });
+      }
+    }
+  },
+
+  shimPeerConnection: function(window) {
+    var browserDetails = utils.detectBrowser(window);
+
+    if (typeof window !== 'object' || !(window.RTCPeerConnection ||
+        window.mozRTCPeerConnection)) {
+      return; // probably media.peerconnection.enabled=false in about:config
+    }
+    // The RTCPeerConnection object.
+    if (!window.RTCPeerConnection) {
+      window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+        if (browserDetails.version < 38) {
+          // .urls is not supported in FF < 38.
+          // create RTCIceServers with a single url.
+          if (pcConfig && pcConfig.iceServers) {
+            var newIceServers = [];
+            for (var i = 0; i < pcConfig.iceServers.length; i++) {
+              var server = pcConfig.iceServers[i];
+              if (server.hasOwnProperty('urls')) {
+                for (var j = 0; j < server.urls.length; j++) {
+                  var newServer = {
+                    url: server.urls[j]
+                  };
+                  if (server.urls[j].indexOf('turn') === 0) {
+                    newServer.username = server.username;
+                    newServer.credential = server.credential;
+                  }
+                  newIceServers.push(newServer);
+                }
+              } else {
+                newIceServers.push(pcConfig.iceServers[i]);
+              }
+            }
+            pcConfig.iceServers = newIceServers;
+          }
+        }
+        return new window.mozRTCPeerConnection(pcConfig, pcConstraints);
+      };
+      window.RTCPeerConnection.prototype =
+          window.mozRTCPeerConnection.prototype;
+
+      // wrap static methods. Currently just generateCertificate.
+      if (window.mozRTCPeerConnection.generateCertificate) {
+        Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+          get: function() {
+            return window.mozRTCPeerConnection.generateCertificate;
+          }
+        });
+      }
+
+      window.RTCSessionDescription = window.mozRTCSessionDescription;
+      window.RTCIceCandidate = window.mozRTCIceCandidate;
+    }
+
+    // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
+    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
+        .forEach(function(method) {
+          var nativeMethod = window.RTCPeerConnection.prototype[method];
+          window.RTCPeerConnection.prototype[method] = function() {
+            arguments[0] = new ((method === 'addIceCandidate') ?
+                window.RTCIceCandidate :
+                window.RTCSessionDescription)(arguments[0]);
+            return nativeMethod.apply(this, arguments);
+          };
+        });
+
+    // support for addIceCandidate(null or undefined)
+    var nativeAddIceCandidate =
+        window.RTCPeerConnection.prototype.addIceCandidate;
+    window.RTCPeerConnection.prototype.addIceCandidate = function() {
+      if (!arguments[0]) {
+        if (arguments[1]) {
+          arguments[1].apply(null);
+        }
+        return Promise.resolve();
+      }
+      return nativeAddIceCandidate.apply(this, arguments);
+    };
+
+    // shim getStats with maplike support
+    var makeMapStats = function(stats) {
+      var map = new Map();
+      Object.keys(stats).forEach(function(key) {
+        map.set(key, stats[key]);
+        map[key] = stats[key];
+      });
+      return map;
+    };
+
+    var modernStatsTypes = {
+      inboundrtp: 'inbound-rtp',
+      outboundrtp: 'outbound-rtp',
+      candidatepair: 'candidate-pair',
+      localcandidate: 'local-candidate',
+      remotecandidate: 'remote-candidate'
+    };
+
+    var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
+    window.RTCPeerConnection.prototype.getStats = function(
+      selector,
+      onSucc,
+      onErr
+    ) {
+      return nativeGetStats.apply(this, [selector || null])
+        .then(function(stats) {
+          if (browserDetails.version < 48) {
+            stats = makeMapStats(stats);
+          }
+          if (browserDetails.version < 53 && !onSucc) {
+            // Shim only promise getStats with spec-hyphens in type names
+            // Leave callback version alone; misc old uses of forEach before Map
+            try {
+              stats.forEach(function(stat) {
+                stat.type = modernStatsTypes[stat.type] || stat.type;
+              });
+            } catch (e) {
+              if (e.name !== 'TypeError') {
+                throw e;
+              }
+              // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
+              stats.forEach(function(stat, i) {
+                stats.set(i, Object.assign({}, stat, {
+                  type: modernStatsTypes[stat.type] || stat.type
+                }));
+              });
+            }
+          }
+          return stats;
+        })
+        .then(onSucc, onErr);
+    };
+  },
+
+  shimRemoveStream: function(window) {
+    if (!window.RTCPeerConnection ||
+        'removeStream' in window.RTCPeerConnection.prototype) {
+      return;
+    }
+    window.RTCPeerConnection.prototype.removeStream = function(stream) {
+      var pc = this;
+      utils.deprecated('removeStream', 'removeTrack');
+      this.getSenders().forEach(function(sender) {
+        if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) {
+          pc.removeTrack(sender);
+        }
+      });
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimOnTrack: firefoxShim.shimOnTrack,
+  shimSourceObject: firefoxShim.shimSourceObject,
+  shimPeerConnection: firefoxShim.shimPeerConnection,
+  shimRemoveStream: firefoxShim.shimRemoveStream,
+  shimGetUserMedia: require('./getusermedia')
+};
+
+},{"../utils":13,"./getusermedia":11}],11:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var utils = require('../utils');
+var logging = utils.log;
+
+// Expose public methods.
+module.exports = function(window) {
+  var browserDetails = utils.detectBrowser(window);
+  var navigator = window && window.navigator;
+  var MediaStreamTrack = window && window.MediaStreamTrack;
+
+  var shimError_ = function(e) {
+    return {
+      name: {
+        InternalError: 'NotReadableError',
+        NotSupportedError: 'TypeError',
+        PermissionDeniedError: 'NotAllowedError',
+        SecurityError: 'NotAllowedError'
+      }[e.name] || e.name,
+      message: {
+        'The operation is insecure.': 'The request is not allowed by the ' +
+        'user agent or the platform in the current context.'
+      }[e.message] || e.message,
+      constraint: e.constraint,
+      toString: function() {
+        return this.name + (this.message && ': ') + this.message;
+      }
+    };
+  };
+
+  // getUserMedia constraints shim.
+  var getUserMedia_ = function(constraints, onSuccess, onError) {
+    var constraintsToFF37_ = function(c) {
+      if (typeof c !== 'object' || c.require) {
+        return c;
+      }
+      var require = [];
+      Object.keys(c).forEach(function(key) {
+        if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
+          return;
+        }
+        var r = c[key] = (typeof c[key] === 'object') ?
+            c[key] : {ideal: c[key]};
+        if (r.min !== undefined ||
+            r.max !== undefined || r.exact !== undefined) {
+          require.push(key);
+        }
+        if (r.exact !== undefined) {
+          if (typeof r.exact === 'number') {
+            r. min = r.max = r.exact;
+          } else {
+            c[key] = r.exact;
+          }
+          delete r.exact;
+        }
+        if (r.ideal !== undefined) {
+          c.advanced = c.advanced || [];
+          var oc = {};
+          if (typeof r.ideal === 'number') {
+            oc[key] = {min: r.ideal, max: r.ideal};
+          } else {
+            oc[key] = r.ideal;
+          }
+          c.advanced.push(oc);
+          delete r.ideal;
+          if (!Object.keys(r).length) {
+            delete c[key];
+          }
+        }
+      });
+      if (require.length) {
+        c.require = require;
+      }
+      return c;
+    };
+    constraints = JSON.parse(JSON.stringify(constraints));
+    if (browserDetails.version < 38) {
+      logging('spec: ' + JSON.stringify(constraints));
+      if (constraints.audio) {
+        constraints.audio = constraintsToFF37_(constraints.audio);
+      }
+      if (constraints.video) {
+        constraints.video = constraintsToFF37_(constraints.video);
+      }
+      logging('ff37: ' + JSON.stringify(constraints));
+    }
+    return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
+      onError(shimError_(e));
+    });
+  };
+
+  // Returns the result of getUserMedia as a Promise.
+  var getUserMediaPromise_ = function(constraints) {
+    return new Promise(function(resolve, reject) {
+      getUserMedia_(constraints, resolve, reject);
+    });
+  };
+
+  // Shim for mediaDevices on older versions.
+  if (!navigator.mediaDevices) {
+    navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
+      addEventListener: function() { },
+      removeEventListener: function() { }
+    };
+  }
+  navigator.mediaDevices.enumerateDevices =
+      navigator.mediaDevices.enumerateDevices || function() {
+        return new Promise(function(resolve) {
+          var infos = [
+            {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
+            {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
+          ];
+          resolve(infos);
+        });
+      };
+
+  if (browserDetails.version < 41) {
+    // Work around http://bugzil.la/1169665
+    var orgEnumerateDevices =
+        navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
+    navigator.mediaDevices.enumerateDevices = function() {
+      return orgEnumerateDevices().then(undefined, function(e) {
+        if (e.name === 'NotFoundError') {
+          return [];
+        }
+        throw e;
+      });
+    };
+  }
+  if (browserDetails.version < 49) {
+    var origGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      return origGetUserMedia(c).then(function(stream) {
+        // Work around https://bugzil.la/802326
+        if (c.audio && !stream.getAudioTracks().length ||
+            c.video && !stream.getVideoTracks().length) {
+          stream.getTracks().forEach(function(track) {
+            track.stop();
+          });
+          throw new DOMException('The object can not be found here.',
+                                 'NotFoundError');
+        }
+        return stream;
+      }, function(e) {
+        return Promise.reject(shimError_(e));
+      });
+    };
+  }
+  if (!(browserDetails.version > 55 &&
+      'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
+    var remap = function(obj, a, b) {
+      if (a in obj && !(b in obj)) {
+        obj[b] = obj[a];
+        delete obj[a];
+      }
+    };
+
+    var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.
+        bind(navigator.mediaDevices);
+    navigator.mediaDevices.getUserMedia = function(c) {
+      if (typeof c === 'object' && typeof c.audio === 'object') {
+        c = JSON.parse(JSON.stringify(c));
+        remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
+        remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
+      }
+      return nativeGetUserMedia(c);
+    };
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
+      var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
+      MediaStreamTrack.prototype.getSettings = function() {
+        var obj = nativeGetSettings.apply(this, arguments);
+        remap(obj, 'mozAutoGainControl', 'autoGainControl');
+        remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
+        return obj;
+      };
+    }
+
+    if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
+      var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
+      MediaStreamTrack.prototype.applyConstraints = function(c) {
+        if (this.kind === 'audio' && typeof c === 'object') {
+          c = JSON.parse(JSON.stringify(c));
+          remap(c, 'autoGainControl', 'mozAutoGainControl');
+          remap(c, 'noiseSuppression', 'mozNoiseSuppression');
+        }
+        return nativeApplyConstraints.apply(this, [c]);
+      };
+    }
+  }
+  navigator.getUserMedia = function(constraints, onSuccess, onError) {
+    if (browserDetails.version < 44) {
+      return getUserMedia_(constraints, onSuccess, onError);
+    }
+    // Replace Firefox 44+'s deprecation warning with unprefixed version.
+    utils.deprecated('navigator.getUserMedia',
+        'navigator.mediaDevices.getUserMedia');
+    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
+  };
+};
+
+},{"../utils":13}],12:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+'use strict';
+var utils = require('../utils');
+
+var safariShim = {
+  // TODO: DrAlex, should be here, double check against LayoutTests
+
+  // TODO: once the back-end for the mac port is done, add.
+  // TODO: check for webkitGTK+
+  // shimPeerConnection: function() { },
+
+  shimLocalStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getLocalStreams = function() {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        return this._localStreams;
+      };
+    }
+    if (!('getStreamById' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getStreamById = function(id) {
+        var result = null;
+        if (this._localStreams) {
+          this._localStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        if (this._remoteStreams) {
+          this._remoteStreams.forEach(function(stream) {
+            if (stream.id === id) {
+              result = stream;
+            }
+          });
+        }
+        return result;
+      };
+    }
+    if (!('addStream' in window.RTCPeerConnection.prototype)) {
+      var _addTrack = window.RTCPeerConnection.prototype.addTrack;
+      window.RTCPeerConnection.prototype.addStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        if (this._localStreams.indexOf(stream) === -1) {
+          this._localStreams.push(stream);
+        }
+        var self = this;
+        stream.getTracks().forEach(function(track) {
+          _addTrack.call(self, track, stream);
+        });
+      };
+
+      window.RTCPeerConnection.prototype.addTrack = function(track, stream) {
+        if (stream) {
+          if (!this._localStreams) {
+            this._localStreams = [stream];
+          } else if (this._localStreams.indexOf(stream) === -1) {
+            this._localStreams.push(stream);
+          }
+        }
+        return _addTrack.call(this, track, stream);
+      };
+    }
+    if (!('removeStream' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.removeStream = function(stream) {
+        if (!this._localStreams) {
+          this._localStreams = [];
+        }
+        var index = this._localStreams.indexOf(stream);
+        if (index === -1) {
+          return;
+        }
+        this._localStreams.splice(index, 1);
+        var self = this;
+        var tracks = stream.getTracks();
+        this.getSenders().forEach(function(sender) {
+          if (tracks.indexOf(sender.track) !== -1) {
+            self.removeTrack(sender);
+          }
+        });
+      };
+    }
+  },
+  shimRemoteStreamsAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
+      window.RTCPeerConnection.prototype.getRemoteStreams = function() {
+        return this._remoteStreams ? this._remoteStreams : [];
+      };
+    }
+    if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
+      Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
+        get: function() {
+          return this._onaddstream;
+        },
+        set: function(f) {
+          if (this._onaddstream) {
+            this.removeEventListener('addstream', this._onaddstream);
+            this.removeEventListener('track', this._onaddstreampoly);
+          }
+          this.addEventListener('addstream', this._onaddstream = f);
+          this.addEventListener('track', this._onaddstreampoly = function(e) {
+            var stream = e.streams[0];
+            if (!this._remoteStreams) {
+              this._remoteStreams = [];
+            }
+            if (this._remoteStreams.indexOf(stream) >= 0) {
+              return;
+            }
+            this._remoteStreams.push(stream);
+            var event = new Event('addstream');
+            event.stream = e.streams[0];
+            this.dispatchEvent(event);
+          }.bind(this));
+        }
+      });
+    }
+  },
+  shimCallbacksAPI: function(window) {
+    if (typeof window !== 'object' || !window.RTCPeerConnection) {
+      return;
+    }
+    var prototype = window.RTCPeerConnection.prototype;
+    var createOffer = prototype.createOffer;
+    var createAnswer = prototype.createAnswer;
+    var setLocalDescription = prototype.setLocalDescription;
+    var setRemoteDescription = prototype.setRemoteDescription;
+    var addIceCandidate = prototype.addIceCandidate;
+
+    prototype.createOffer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createOffer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    prototype.createAnswer = function(successCallback, failureCallback) {
+      var options = (arguments.length >= 2) ? arguments[2] : arguments[0];
+      var promise = createAnswer.apply(this, [options]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+
+    var withCallback = function(description, successCallback, failureCallback) {
+      var promise = setLocalDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setLocalDescription = withCallback;
+
+    withCallback = function(description, successCallback, failureCallback) {
+      var promise = setRemoteDescription.apply(this, [description]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.setRemoteDescription = withCallback;
+
+    withCallback = function(candidate, successCallback, failureCallback) {
+      var promise = addIceCandidate.apply(this, [candidate]);
+      if (!failureCallback) {
+        return promise;
+      }
+      promise.then(successCallback, failureCallback);
+      return Promise.resolve();
+    };
+    prototype.addIceCandidate = withCallback;
+  },
+  shimGetUserMedia: function(window) {
+    var navigator = window && window.navigator;
+
+    if (!navigator.getUserMedia) {
+      if (navigator.webkitGetUserMedia) {
+        navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
+      } else if (navigator.mediaDevices &&
+          navigator.mediaDevices.getUserMedia) {
+        navigator.getUserMedia = function(constraints, cb, errcb) {
+          navigator.mediaDevices.getUserMedia(constraints)
+          .then(cb, errcb);
+        }.bind(navigator);
+      }
+    }
+  },
+  shimRTCIceServerUrls: function(window) {
+    // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
+    var OrigPeerConnection = window.RTCPeerConnection;
+    window.RTCPeerConnection = function(pcConfig, pcConstraints) {
+      if (pcConfig && pcConfig.iceServers) {
+        var newIceServers = [];
+        for (var i = 0; i < pcConfig.iceServers.length; i++) {
+          var server = pcConfig.iceServers[i];
+          if (!server.hasOwnProperty('urls') &&
+              server.hasOwnProperty('url')) {
+            utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
+            server = JSON.parse(JSON.stringify(server));
+            server.urls = server.url;
+            delete server.url;
+            newIceServers.push(server);
+          } else {
+            newIceServers.push(pcConfig.iceServers[i]);
+          }
+        }
+        pcConfig.iceServers = newIceServers;
+      }
+      return new OrigPeerConnection(pcConfig, pcConstraints);
+    };
+    window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
+    // wrap static methods. Currently just generateCertificate.
+    if ('generateCertificate' in window.RTCPeerConnection) {
+      Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
+        get: function() {
+          return OrigPeerConnection.generateCertificate;
+        }
+      });
+    }
+  },
+  shimTrackEventTransceiver: function(window) {
+    // Add event.transceiver member over deprecated event.receiver
+    if (typeof window === 'object' && window.RTCPeerConnection &&
+        ('receiver' in window.RTCTrackEvent.prototype) &&
+        // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is
+        // defined for some reason even when window.RTCTransceiver is not.
+        !window.RTCTransceiver) {
+      Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
+        get: function() {
+          return {receiver: this.receiver};
+        }
+      });
+    }
+  },
+
+  shimCreateOfferLegacy: function(window) {
+    var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
+    window.RTCPeerConnection.prototype.createOffer = function(offerOptions) {
+      var pc = this;
+      if (offerOptions) {
+        var audioTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'audio';
+        });
+        if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
+          if (audioTransceiver.direction === 'sendrecv') {
+            audioTransceiver.setDirection('sendonly');
+          } else if (audioTransceiver.direction === 'recvonly') {
+            audioTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveAudio === true &&
+            !audioTransceiver) {
+          pc.addTransceiver('audio');
+        }
+
+        var videoTransceiver = pc.getTransceivers().find(function(transceiver) {
+          return transceiver.sender.track &&
+              transceiver.sender.track.kind === 'video';
+        });
+        if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
+          if (videoTransceiver.direction === 'sendrecv') {
+            videoTransceiver.setDirection('sendonly');
+          } else if (videoTransceiver.direction === 'recvonly') {
+            videoTransceiver.setDirection('inactive');
+          }
+        } else if (offerOptions.offerToReceiveVideo === true &&
+            !videoTransceiver) {
+          pc.addTransceiver('video');
+        }
+      }
+      return origCreateOffer.apply(pc, arguments);
+    };
+  }
+};
+
+// Expose public methods.
+module.exports = {
+  shimCallbacksAPI: safariShim.shimCallbacksAPI,
+  shimLocalStreamsAPI: safariShim.shimLocalStreamsAPI,
+  shimRemoteStreamsAPI: safariShim.shimRemoteStreamsAPI,
+  shimGetUserMedia: safariShim.shimGetUserMedia,
+  shimRTCIceServerUrls: safariShim.shimRTCIceServerUrls,
+  shimTrackEventTransceiver: safariShim.shimTrackEventTransceiver,
+  shimCreateOfferLegacy: safariShim.shimCreateOfferLegacy
+  // TODO
+  // shimPeerConnection: safariShim.shimPeerConnection
+};
+
+},{"../utils":13}],13:[function(require,module,exports){
+/*
+ *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
+ *
+ *  Use of this source code is governed by a BSD-style license
+ *  that can be found in the LICENSE file in the root of the source
+ *  tree.
+ */
+ /* eslint-env node */
+'use strict';
+
+var logDisabled_ = true;
+var deprecationWarnings_ = true;
+
+// Utility methods.
+var utils = {
+  disableLog: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    logDisabled_ = bool;
+    return (bool) ? 'adapter.js logging disabled' :
+        'adapter.js logging enabled';
+  },
+
+  /**
+   * Disable or enable deprecation warnings
+   * @param {!boolean} bool set to true to disable warnings.
+   */
+  disableWarnings: function(bool) {
+    if (typeof bool !== 'boolean') {
+      return new Error('Argument type: ' + typeof bool +
+          '. Please use a boolean.');
+    }
+    deprecationWarnings_ = !bool;
+    return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
+  },
+
+  log: function() {
+    if (typeof window === 'object') {
+      if (logDisabled_) {
+        return;
+      }
+      if (typeof console !== 'undefined' && typeof console.log === 'function') {
+        console.log.apply(console, arguments);
+      }
+    }
+  },
+
+  /**
+   * Shows a deprecation warning suggesting the modern and spec-compatible API.
+   */
+  deprecated: function(oldMethod, newMethod) {
+    if (!deprecationWarnings_) {
+      return;
+    }
+    console.warn(oldMethod + ' is deprecated, please use ' + newMethod +
+        ' instead.');
+  },
+
+  /**
+   * Extract browser version out of the provided user agent string.
+   *
+   * @param {!string} uastring userAgent string.
+   * @param {!string} expr Regular expression used as match criteria.
+   * @param {!number} pos position in the version string to be returned.
+   * @return {!number} browser version.
+   */
+  extractVersion: function(uastring, expr, pos) {
+    var match = uastring.match(expr);
+    return match && match.length >= pos && parseInt(match[pos], 10);
+  },
+
+  /**
+   * Browser detector.
+   *
+   * @return {object} result containing browser and version
+   *     properties.
+   */
+  detectBrowser: function(window) {
+    var navigator = window && window.navigator;
+
+    // Returned result object.
+    var result = {};
+    result.browser = null;
+    result.version = null;
+
+    // Fail early if it's not a browser
+    if (typeof window === 'undefined' || !window.navigator) {
+      result.browser = 'Not a browser.';
+      return result;
+    }
+
+    // Firefox.
+    if (navigator.mozGetUserMedia) {
+      result.browser = 'firefox';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Firefox\/(\d+)\./, 1);
+    } else if (navigator.webkitGetUserMedia) {
+      // Chrome, Chromium, Webview, Opera, all use the chrome shim for now
+      if (window.webkitRTCPeerConnection) {
+        result.browser = 'chrome';
+        result.version = this.extractVersion(navigator.userAgent,
+          /Chrom(e|ium)\/(\d+)\./, 2);
+      } else { // Safari (in an unpublished version) or unknown webkit-based.
+        if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
+          result.browser = 'safari';
+          result.version = this.extractVersion(navigator.userAgent,
+            /AppleWebKit\/(\d+)\./, 1);
+        } else { // unknown webkit-based browser.
+          result.browser = 'Unsupported webkit-based browser ' +
+              'with GUM support but no WebRTC support.';
+          return result;
+        }
+      }
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge.
+      result.browser = 'edge';
+      result.version = this.extractVersion(navigator.userAgent,
+          /Edge\/(\d+).(\d+)$/, 2);
+    } else if (navigator.mediaDevices &&
+        navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
+        // Safari, with webkitGetUserMedia removed.
+      result.browser = 'safari';
+      result.version = this.extractVersion(navigator.userAgent,
+          /AppleWebKit\/(\d+)\./, 1);
+    } else { // Default fallthrough: not supported.
+      result.browser = 'Not a supported browser.';
+      return result;
+    }
+
+    return result;
+  },
+
+};
+
+// Export.
+module.exports = {
+  log: utils.log,
+  deprecated: utils.deprecated,
+  disableLog: utils.disableLog,
+  disableWarnings: utils.disableWarnings,
+  extractVersion: utils.extractVersion,
+  shimCreateObjectURL: utils.shimCreateObjectURL,
+  detectBrowser: utils.detectBrowser.bind(utils)
+};
+
+},{}]},{},[3]);

+ 83 - 0
support/client/lib/vwf/view/widgets.js

@@ -0,0 +1,83 @@
+'use strict';
+
+define(function () {
+
+
+    /*
+     * Cell widgets 
+     */
+    class Widgets {
+        constructor() {
+          console.log("widget constructor")
+        }
+
+        get divider(){
+            return {
+                $cell: true,
+                $type: "hr",
+                class: "mdc-list-divider",
+            }
+        }
+
+        headerH3(headertype, label, cssclass) {
+
+            return  {
+                $cell: true,
+                $type: headertype,
+                class: cssclass,
+                $text: label
+            }
+
+        }
+
+        icontoggle(obj) {
+            return {
+                $cell: true,
+                $type: "i",
+                class: "mdc-icon-toggle material-icons",
+                role: "button",
+                $text: obj.label,
+                id: obj.id,
+                'data-toggle-on': obj.on,
+                'data-toggle-off': obj.off,
+                'aria-pressed': obj.state,
+                //'aria-hidden': true,
+                $init: obj.init
+            }
+        }
+
+        switch(obj) {
+
+            return   {
+                $cell: true,
+                $type: "div",
+                class: "mdc-switch",
+                $components: [
+                    {
+                        $type: "input",
+                        type: "checkbox",
+                        class: "mdc-switch__native-control",
+                        id: obj.id,
+                        $init: obj.init,
+                        //id: "basic-switch",
+                        onchange: obj.onchange
+                    },
+                    {
+                        $type: "div",
+                        class: "mdc-switch__background",
+                        $components: [
+                            {
+                                $type: "div",
+                                class: "mdc-switch__knob"
+                            }
+                        ]
+                    }
+                ]
+            }
+
+        }
+
+      }
+    return new Widgets;
+
+})

+ 6 - 1
support/proxy/vwf.example.com/aframe/acamera.vwf.yaml

@@ -3,4 +3,9 @@
 extends: http://vwf.example.com/aframe/aentity.vwf
 type: "a-camera"
 properties:
-  userHeight:
+  user-height:
+  far:
+  fov:
+  look-controls-enabled:
+  near:
+  wasd-controls-enabled:

+ 1 - 0
support/proxy/vwf.example.com/aframe/aentity.vwf.yaml

@@ -19,6 +19,7 @@ properties:
   visible:
   edit:
   worldPosition:
+  side:
   osc:
 methods:
   setGizmoMode:

+ 7 - 0
support/proxy/vwf.example.com/aframe/aobjmodel.vwf.yaml

@@ -0,0 +1,7 @@
+#https://aframe.io/docs/master/primitives/a-obj-model.html
+--- 
+extends: http://vwf.example.com/aframe/aentity.vwf
+type: "a-obj-model"
+properties:
+  src:
+  mtl:

+ 16 - 0
support/proxy/vwf.example.com/aframe/ascene.js

@@ -27,6 +27,22 @@ this.clientWatch = function () {
                     } else {
                         //console.log(node.id + " needed to delete!");
                         self.children.delete(self.children[node.id]);
+                        //'gearvr-'
+                        let controllerVR = self.children['gearvr-'+ node.id.slice(7)];
+                        if (controllerVR){
+                            self.children.delete(controllerVR);
+                        }
+
+                        let wmrvR = self.children['wmrvr-right-'+ node.id.slice(7)];
+                        if (wmrvR){
+                            self.children.delete(wmrvR);
+                        }
+                        
+                        let wmrvL = self.children['wmrvr-left-'+ node.id.slice(7)];
+                        if (wmrvL){
+                            self.children.delete(wmrvL);
+                        }
+                        
                     }
                 }
             });

+ 2 - 0
support/proxy/vwf.example.com/aframe/ascene.vwf.yaml

@@ -5,6 +5,8 @@ type: "a-scene"
 properties:
   fog:
   assets:
+  color:
+  transparent:
 methods:
   clientWatch:
 scripts:

+ 4 - 1
support/proxy/vwf.example.com/aframe/asky.vwf.yaml

@@ -2,4 +2,7 @@
 --- 
 extends: http://vwf.example.com/aframe/aentity.vwf
 type: "a-sky"
-properties:
+properties:
+  color:
+  side:
+  src:

+ 8 - 0
support/proxy/vwf.example.com/aframe/asound.vwf.yaml

@@ -0,0 +1,8 @@
+# https://aframe.io/docs/master/primitives/a-sound.html
+--- 
+extends: http://vwf.example.com/aframe/aentity.vwf
+type: "a-sound"
+properties:
+  src:
+  on:
+  autoplay:

+ 74 - 17
support/proxy/vwf.example.com/aframe/avatar.js

@@ -2,7 +2,7 @@ this.simpleBodyDef = {
     "extends": "http://vwf.example.com/aframe/abox.vwf",
     "properties": {
         "color": "white",
-        "position": "0 0.66 0.3",
+        "position": "0 0.66 0.7",
         "height": 1.3,
         "width": 0.65,
         "depth": 0.1,
@@ -13,7 +13,7 @@ this.modelBodyDef = {
     "extends": "http://vwf.example.com/aframe/agltfmodel.vwf",
     "properties": {
         "src": "#avatar",
-        "position": "0 0 0.5",
+        "position": "0 0 0.8",
         "rotation": "0 180 0"
     },
     "children": {
@@ -35,10 +35,17 @@ this.createAvatarBody = function (modelSrc) {
     let avatarControl = document.querySelector('#avatarControl');
 
 
-    let userHeight = avatarControl.getAttribute('camera').userHeight;
+    //let userHeight = avatarControl.getAttribute('camera').userHeight;
+    var userHeight = avatarControl.getAttribute('look-controls').userHeight; //avatarControl.getAttribute('position').y;
+
+    // if (AFRAME.utils.device.isGearVR()) {
+    //     userHeight = 0
+    // }
 
     let myColor = this.getRandomColor();
     let myBodyDef = this.simpleBodyDef;
+    //let myHandDef = this.simpleVrControllerDef;
+
     myBodyDef.properties.color = myColor;
 
     var newNode = {
@@ -49,10 +56,11 @@ this.createAvatarBody = function (modelSrc) {
         children: {
            
             "myBody": myBodyDef,
+            //"myHand": myHandDef,
             "myHead": {
                 "extends": "http://vwf.example.com/aframe/aentity.vwf",
                 "properties": {
-                    "position": "0 1.6 0.3",
+                    "position": "0 1.6 0.7",
                     "visible": true
                 },
                 children: {
@@ -61,10 +69,7 @@ this.createAvatarBody = function (modelSrc) {
                         "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
                         "type": "component",
                         "properties": {
-                            "enabled": true,
-                            "duration": 50,
-                            "deltaPos": 0,
-                            "deltaRot": 0
+                            "enabled": true
                         }
                     },
                     "visual": {
@@ -83,10 +88,10 @@ this.createAvatarBody = function (modelSrc) {
                         "id": 'camera-' + this.id,
                         "extends": "http://vwf.example.com/aframe/acamera.vwf",
                         "properties": {
-                            "position": "0 0 0",
+                            "position": "0 0 -0.7",
                             "look-controls-enabled": false,
-                            "wasd-controls": false,
-                            "userHeight": 0,
+                            "wasd-controls-enabled": false,
+                            "user-height": 0
                         }
                     },
                     "myCursor":
@@ -197,10 +202,7 @@ this.createAvatarBody = function (modelSrc) {
         "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
         "type": "component",
         "properties": {
-            "enabled": true,
-            "duration": 50,
-            "deltaPos": 0,
-            "deltaRot": 0
+            "enabled": true
         }
     }
 
@@ -238,7 +240,7 @@ this.getRandomColor = function () {
 this.followAvatarControl = function (position, rotation) {
     // this.position = AFRAME.utils.coordinates.stringify(position);
     // this.rotation = AFRAME.utils.coordinates.stringify(rotation);
-
+//debugger;
 
     this.position = AFRAME.utils.coordinates.stringify(position);
     let myRot = AFRAME.utils.coordinates.parse(this.rotation);
@@ -316,4 +318,59 @@ this.setVideoTexture = function(val){
     this.avatarNode.myHead.visual.color = "white";
     this.avatarNode.myHead.visual.src = '#temp';
     this.avatarNode.myHead.visual.src = '#'+val;
-}
+}
+
+this.removeVideoTexture = function(){
+   // this.setSmallVideoHead();
+    this.avatarNode.myHead.visual.color = this.avatarNode.myBody.color;
+    this.avatarNode.myHead.visual.src = "";
+    // this.avatarNode.myHead.visual.src = '#'+val;
+}
+
+this.removeSoundWebRTC = function(){
+
+    if (this.avatarNode.audio)
+    this.avatarNode.children.delete(this.avatarNode.audio);
+}
+
+this.setSoundWebRTC = function(val){
+    console.log(val);
+    if (this.avatarNode.audio) this.removeSoundWebRTC();
+
+    var soundNode = {
+        "extends": "http://vwf.example.com/aframe/aentity.vwf",
+        "properties": {
+        },
+        "children":{
+            "streamsound":{
+                "extends": "http://vwf.example.com/aframe/streamSoundComponent.vwf",
+                "type": "component",
+                "properties": {
+                }
+            }
+        }
+    }
+    this.avatarNode.children.create("audio", soundNode );
+   // this.setSmallVideoHead();
+
+    //this.avatarNode.audio.src = '#tempAudio';
+    //this.avatarNode.audio.src = '#'+val;
+}
+
+this.webrtcTurnOnOff = function(val){
+    console.log('WEBRTC is ', val);
+}
+
+this.webrtcMuteAudio = function(val){
+    console.log('WEBRTC Audio is ', val);
+}
+
+this.webrtcMuteVideo = function(val){
+    console.log('WEBRTC Video is ', val);
+}
+
+this.initialize = function() {
+   // this.future(0).updateAvatar();
+};
+
+

+ 16 - 0
support/proxy/vwf.example.com/aframe/avatar.vwf.yaml

@@ -9,6 +9,8 @@ properties:
     displayName:
     sharing: { audio: true, video: true }
 methods:
+    initialize:
+    updateAvatar:
     showHideCursor:
         parameters:
             - bool
@@ -29,5 +31,19 @@ methods:
     setVideoTexture:
         parameters:
             - val
+    webrtcTurnOnOff:
+        parameters:
+            - val
+    webrtcMuteAudio:
+        parameters:
+            - val
+    webrtcMuteVideo:
+        parameters:
+            - val
+    setSoundWebRTC:
+         parameters:
+            - val
+    removeSoundWebRTC:
+    removeVideoTexture:
 scripts:
 - source: "http://vwf.example.com/aframe/avatar.js"

+ 0 - 12
support/proxy/vwf.example.com/aframe/gearvr-controlsComponent.vwf.yaml

@@ -1,12 +0,0 @@
-#https://aframe.io/docs/master/components/gearvr-controls.html
----
-extends: http://vwf.example.com/aframe/aentityComponent.vwf
-type: "component"
-properties:
-  armModel:
-  buttonColor:
-  buttonTouchedColor:
-  buttonHighlightColor:
-  hand:
-  model:
-  rotationOffset:

+ 129 - 0
support/proxy/vwf.example.com/aframe/gearvrcontroller.js

@@ -0,0 +1,129 @@
+this.simpleDef = {
+    "extends": "http://vwf.example.com/aframe/abox.vwf",
+    "properties": {
+        "color": "white",
+        "position": "0 0 0",
+        "height": 0.01,
+        "width": 0.01,
+        "depth": 1,
+    },
+    children: {
+        "pointer": {
+            "extends": "http://vwf.example.com/aframe/abox.vwf",
+            "properties": {
+                "color": "green",
+                "position": "0 0 -0.8",
+                "height": 0.1,
+                "width": 0.1,
+                "depth": 0.1
+            }
+        },
+        "interpolation":
+            {
+                "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
+                "type": "component",
+                "properties": {
+                    "enabled": true
+                }
+            }
+    }
+}
+
+this.modelDef = {
+    "extends": "http://vwf.example.com/aframe/agltfmodel.vwf",
+    "properties": {
+        "src": "#gearvr",
+        "position": "0 0 0",
+        "rotation": "0 180 0"
+    },
+    "children": {
+        "animation-mixer": {
+            "extends": "http://vwf.example.com/aframe/anim-mixer-component.vwf",
+            "properties": {
+                "clip": "*",
+                "duration": 1
+            }
+        }
+
+    }
+}
+
+this.createController = function (modelSrc) {
+
+    let controllerDef = this.simpleDef;
+
+    var newNode = {
+        "extends": "http://vwf.example.com/aframe/aentity.vwf",
+        "properties": {
+            "position": [0, 0, 0]
+        },
+        children: {
+            "controller": controllerDef
+        }
+    }
+
+    if (modelSrc) {
+
+        let controllerDef = this.modelDef;
+        controllerDef.properties.src = modelSrc;
+        newNode.children.controller = controllerDef;
+    }
+
+
+    let interpolation =  {
+        "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
+        "type": "component",
+        "properties": {
+   }
+    }
+
+
+    this.children.create( "interpolation", interpolation );
+    this.children.create("handVRNode", newNode);
+
+}
+
+
+this.updateVRControl = function(position, rotation){
+
+    this.rotation = rotation;
+    this.position = position;
+
+}
+
+
+this.createSimpleController = function(){
+       if (this.handVRNode.controller) {
+        this.handVRNode.children.delete(this.handVRNode.controller);
+
+        let controllerDef = this.simpleDef;
+
+        this.handVRNode.children.create("controller", controllerDef);
+
+       }
+}
+
+this.createAvatarFromGLTF = function(modelSrc){
+
+    if (this.handVRNode.controller) {
+        this.handVRNode.children.delete(this.handVRNode.controller);
+        
+        let controllerDef = this.modelDef;
+        controllerDef.properties.src = modelSrc;
+
+        this.handVRNode.children.create("controller", controllerDef);
+
+       }
+}
+
+this.initialize = function() {
+   // this.future(0).update();
+}
+
+this.triggerdown = function() {
+    this.handVRNode.controller.pointer.color = 'red'
+ }
+
+ this.triggerup = function() {
+    this.handVRNode.controller.pointer.color = 'green'
+ }

+ 24 - 0
support/proxy/vwf.example.com/aframe/gearvrcontroller.vwf.yaml

@@ -0,0 +1,24 @@
+# gearvr controller
+# Copyright 2017 Krestianstvo.org project
+---
+extends: http://vwf.example.com/aframe/aentity.vwf
+type: "gearvr"
+properties:
+methods:
+    initialize:
+    updateController:
+    createSimpleController:
+    createControllerFromGLTF:
+        parameters:
+            - modelSrc
+    createController:
+        parameters:
+            - modelSrc
+    updateVRControl:
+        parameters:
+            - position
+            - rotation
+    triggerdown:
+    triggerup:
+scripts:
+- source: "http://vwf.example.com/aframe/gearvrcontroller.js"

+ 0 - 1
support/proxy/vwf.example.com/aframe/interpolation-component.vwf.yaml

@@ -4,6 +4,5 @@ extends: http://vwf.example.com/aframe/aentityComponent.vwf
 type: "component"
 properties:
   enabled:
-  duration:
   deltaPos:
   deltaRot:

+ 5 - 0
support/proxy/vwf.example.com/aframe/streamSoundComponent.vwf.yaml

@@ -0,0 +1,5 @@
+# StreamSoundComponent
+---
+extends: http://vwf.example.com/aframe/aentityComponent.vwf
+type: "component"
+properties:

+ 131 - 0
support/proxy/vwf.example.com/aframe/wmrvrcontroller.js

@@ -0,0 +1,131 @@
+this.simpleDef = {
+    "extends": "http://vwf.example.com/aframe/abox.vwf",
+    "properties": {
+        "color": "white",
+        "position": "0 0 0",
+        "height": 0.01,
+        "width": 0.01,
+        "depth": 1,
+    },
+    children: {
+        "pointer": {
+            "extends": "http://vwf.example.com/aframe/abox.vwf",
+            "properties": {
+                "color": "green",
+                "position": "0 0 -0.8",
+                "height": 0.1,
+                "width": 0.1,
+                "depth": 0.1
+            }
+
+        },
+        "interpolation":
+            {
+                "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
+                "type": "component",
+                "properties": {
+                    "enabled": true
+                }
+            }
+    }
+}
+
+this.modelDef = {
+    "extends": "http://vwf.example.com/aframe/agltfmodel.vwf",
+    "properties": {
+        "src": "#wmrvr",
+        "position": "0 0 0",
+        "rotation": "0 180 0"
+    },
+    "children": {
+        "animation-mixer": {
+            "extends": "http://vwf.example.com/aframe/anim-mixer-component.vwf",
+            "properties": {
+                "clip": "*",
+                "duration": 1
+            }
+        }
+
+    }
+}
+
+this.createController = function (modelSrc) {
+
+    let controllerDef = this.simpleDef;
+
+    var newNode = {
+        "extends": "http://vwf.example.com/aframe/aentity.vwf",
+        "properties": {
+            "position": [0, 0, 0]
+        },
+        children: {
+            "controller": controllerDef
+        }
+    }
+
+    if (modelSrc) {
+
+        let controllerDef = this.modelDef;
+        controllerDef.properties.src = modelSrc;
+        newNode.children.controller = controllerDef;
+    }
+
+
+    let interpolation =  {
+        "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
+        "type": "component",
+        "properties": {
+            "enabled": true
+        }
+    }
+
+
+    this.children.create( "interpolation", interpolation );
+    this.children.create("handVRNode", newNode);
+
+}
+
+
+this.updateVRControl = function(position, rotation){
+
+    this.rotation = rotation;
+    this.position = position;
+
+}
+
+
+this.createSimpleController = function(){
+       if (this.handVRNode.controller) {
+        this.handVRNode.children.delete(this.handVRNode.controller);
+
+        let controllerDef = this.simpleDef;
+
+        this.handVRNode.children.create("controller", controllerDef);
+
+       }
+}
+
+this.createAvatarFromGLTF = function(modelSrc){
+
+    if (this.handVRNode.controller) {
+        this.handVRNode.children.delete(this.handVRNode.controller);
+        
+        let controllerDef = this.modelDef;
+        controllerDef.properties.src = modelSrc;
+
+        this.handVRNode.children.create("controller", controllerDef);
+
+       }
+}
+
+this.initialize = function() {
+   // this.future(0).update();
+}
+
+this.triggerdown = function() {
+    this.handVRNode.controller.pointer.color = 'red'
+ }
+
+ this.triggerup = function() {
+    this.handVRNode.controller.pointer.color = 'green'
+ }

+ 24 - 0
support/proxy/vwf.example.com/aframe/wmrvrcontroller.vwf.yaml

@@ -0,0 +1,24 @@
+# wmrvr controller
+# Copyright 2017 Krestianstvo.org project
+---
+extends: http://vwf.example.com/aframe/aentity.vwf
+type: "wmrvr"
+properties:
+methods:
+    initialize:
+    updateController:
+    createSimpleController:
+    createControllerFromGLTF:
+        parameters:
+            - modelSrc
+    createController:
+        parameters:
+            - modelSrc
+    updateVRControl:
+        parameters:
+            - position
+            - rotation
+    triggerdown:
+    triggerup:
+scripts:
+- source: "http://vwf.example.com/aframe/wmrvrcontroller.js"

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.