Nikolay Suslov 7 년 전
부모
커밋
c4250cda91
54개의 변경된 파일5567개의 추가작업 그리고 34518개의 파일을 삭제
  1. 253 37
      package-lock.json
  2. 6 6
      package.json
  3. 10 9
      public/aframe/index.vwf.yaml
  4. 1 1
      public/aframe2/assets.json
  5. 3 16
      public/aframe2/index.vwf.yaml
  6. BIN
      public/assets/images/planeDiffuse.png
  7. 0 225
      public/assets/plane.dae
  8. 198 0
      public/assets/test.dae
  9. 0 8200
      public/web/lib/socketio/socket.io.js
  10. 0 0
      public/web/lib/socketio/socket.io.js.map
  11. 0 0
      public/web/lib/socketio/socket.io.min.js
  12. 0 6052
      public/web/lib/socketio/socket.io.slim.js
  13. 0 0
      public/web/lib/socketio/socket.io.slim.js.map
  14. 0 0
      public/web/lib/socketio/socket.io.slim.min.js
  15. 0 8200
      support/client/lib/socket.io/socket.io.js
  16. 0 0
      support/client/lib/socket.io/socket.io.js.map
  17. 0 0
      support/client/lib/socket.io/socket.io.min.js
  18. 0 6052
      support/client/lib/socket.io/socket.io.slim.js
  19. 0 0
      support/client/lib/socket.io/socket.io.slim.js.map
  20. 0 0
      support/client/lib/socket.io/socket.io.slim.min.js
  21. 66 3
      support/client/lib/vwf/model/aframe.js
  22. 3 3
      support/client/lib/vwf/model/aframe/addon/aframe-components.js
  23. 5 226
      support/client/lib/vwf/model/aframe/addon/aframe-extras.loaders.js
  24. 0 0
      support/client/lib/vwf/model/aframe/addon/aframe-extras.loaders.min.js
  25. 342 0
      support/client/lib/vwf/model/aframe/addon/aframe-interpolation 2.js
  26. 96 235
      support/client/lib/vwf/model/aframe/addon/aframe-interpolation.js
  27. 616 729
      support/client/lib/vwf/model/aframe/aframe-master.js
  28. 1 2
      support/client/lib/vwf/model/aframe/aframe-master.js.map
  29. 0 0
      support/client/lib/vwf/model/aframe/aframe-master.min.js
  30. 0 0
      support/client/lib/vwf/model/aframe/aframe-master.min.js.map
  31. 11 4
      support/client/lib/vwf/model/aframe/extras/aframe-extras.controls.js
  32. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.controls.min.js
  33. 3578 3828
      support/client/lib/vwf/model/aframe/extras/aframe-extras.js
  34. 7 270
      support/client/lib/vwf/model/aframe/extras/aframe-extras.loaders.js
  35. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.loaders.min.js
  36. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.min.js
  37. 12 6
      support/client/lib/vwf/model/aframe/extras/aframe-extras.misc.js
  38. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.misc.min.js
  39. 222 222
      support/client/lib/vwf/model/aframe/extras/aframe-extras.pathfinding.js
  40. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.pathfinding.min.js
  41. 0 0
      support/client/lib/vwf/model/aframe/extras/aframe-extras.primitives.min.js
  42. 7 3
      support/client/lib/vwf/model/aframe/extras/components/sphere-collider.js
  43. 0 0
      support/client/lib/vwf/model/aframe/extras/components/sphere-collider.min.js
  44. 0 150
      support/client/lib/vwf/model/aframe/extras/components/three-model.js
  45. 0 0
      support/client/lib/vwf/model/aframe/extras/components/three-model.min.js
  46. 0 8
      support/client/lib/vwf/model/aframeComponent.js
  47. 33 7
      support/client/lib/vwf/view/aframe.js
  48. 75 6
      support/client/lib/vwf/view/aframeComponent.js
  49. 1 0
      support/proxy/vwf.example.com/aframe/aentity.vwf.yaml
  50. 2 0
      support/proxy/vwf.example.com/aframe/ascene.vwf.yaml
  51. 4 1
      support/proxy/vwf.example.com/aframe/asky.vwf.yaml
  52. 8 9
      support/proxy/vwf.example.com/aframe/avatar.js
  53. 7 7
      support/proxy/vwf.example.com/aframe/gearvrcontroller.js
  54. 0 1
      support/proxy/vwf.example.com/aframe/interpolation-component.vwf.yaml

+ 253 - 37
package-lock.json

@@ -2,10 +2,15 @@
   "name": "livecodingspace",
   "name": "livecodingspace",
   "version": "0.0.1",
   "version": "0.0.1",
   "lockfileVersion": 1,
   "lockfileVersion": 1,
+  "requires": true,
   "dependencies": {
   "dependencies": {
     "accepts": {
     "accepts": {
       "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
       "version": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
-      "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo="
+      "integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
+      "requires": {
+        "mime-types": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz",
+        "negotiator": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz"
+      }
     },
     },
     "after": {
     "after": {
       "version": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
       "version": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
@@ -13,7 +18,10 @@
     },
     },
     "argparse": {
     "argparse": {
       "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
       "version": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
-      "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY="
+      "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=",
+      "requires": {
+        "sprintf-js": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
+      }
     },
     },
     "arraybuffer.slice": {
     "arraybuffer.slice": {
       "version": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
       "version": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
@@ -22,7 +30,10 @@
     "async": {
     "async": {
       "version": "2.4.1",
       "version": "2.4.1",
       "resolved": "https://registry.npmjs.org/async/-/async-2.4.1.tgz",
       "resolved": "https://registry.npmjs.org/async/-/async-2.4.1.tgz",
-      "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c="
+      "integrity": "sha1-YqVrJ5yYoR0JhwlqAcw+6463u9c=",
+      "requires": {
+        "lodash": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz"
+      }
     },
     },
     "backo2": {
     "backo2": {
       "version": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
       "version": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@@ -43,7 +54,10 @@
     },
     },
     "better-assert": {
     "better-assert": {
       "version": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
       "version": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
-      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI="
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "requires": {
+        "callsite": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz"
+      }
     },
     },
     "blob": {
     "blob": {
       "version": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
       "version": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
@@ -52,7 +66,11 @@
     "brace-expansion": {
     "brace-expansion": {
       "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz",
       "version": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.7.tgz",
       "integrity": "sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k=",
       "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": {
     "browser-stdout": {
       "version": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
       "version": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz",
@@ -66,7 +84,10 @@
     "commander": {
     "commander": {
       "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
       "version": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz",
       "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=",
       "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": {
     "component-bind": {
       "version": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
       "version": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
@@ -107,11 +128,19 @@
       "version": "3.0.1",
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
       "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
       "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": {
       "dependencies": {
         "jsonfile": {
         "jsonfile": {
           "version": "3.0.0",
           "version": "3.0.0",
           "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.0.tgz",
           "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 +152,15 @@
     "glob": {
     "glob": {
       "version": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
       "version": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz",
       "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=",
       "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": {
     "graceful-fs": {
       "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
       "version": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
@@ -143,6 +180,9 @@
       "version": "1.0.2",
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz",
       "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz",
       "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=",
       "integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=",
+      "requires": {
+        "isarray": "2.0.1"
+      },
       "dependencies": {
       "dependencies": {
         "isarray": {
         "isarray": {
           "version": "2.0.1",
           "version": "2.0.1",
@@ -167,7 +207,11 @@
     "inflight": {
     "inflight": {
       "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "version": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
       "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": {
     "inherits": {
       "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
       "version": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@@ -177,7 +221,11 @@
     "js-yaml": {
     "js-yaml": {
       "version": "3.8.4",
       "version": "3.8.4",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.4.tgz",
-      "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY="
+      "integrity": "sha1-UgtFZPhlc7qWZir4Woyvp7S1pvY=",
+      "requires": {
+        "argparse": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz",
+        "esprima": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz"
+      }
     },
     },
     "json3": {
     "json3": {
       "version": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
       "version": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz",
@@ -191,7 +239,11 @@
     "lodash._baseassign": {
     "lodash._baseassign": {
       "version": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
       "version": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz",
       "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=",
       "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": {
     "lodash._basecopy": {
       "version": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
       "version": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz",
@@ -216,7 +268,12 @@
     "lodash.create": {
     "lodash.create": {
       "version": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
       "version": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz",
       "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=",
       "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": {
     "lodash.isarguments": {
       "version": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
       "version": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -231,7 +288,12 @@
     "lodash.keys": {
     "lodash.keys": {
       "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
       "version": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz",
       "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
       "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": {
     "mime": {
       "version": "1.3.6",
       "version": "1.3.6",
@@ -244,12 +306,18 @@
     },
     },
     "mime-types": {
     "mime-types": {
       "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz",
       "version": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz",
-      "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0="
+      "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=",
+      "requires": {
+        "mime-db": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz"
+      }
     },
     },
     "minimatch": {
     "minimatch": {
       "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "version": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
       "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": {
     "minimist": {
       "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
       "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
@@ -259,6 +327,9 @@
       "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
       "version": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
       "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
       "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
       "dev": true,
       "dev": true,
+      "requires": {
+        "minimist": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+      },
       "dependencies": {
       "dependencies": {
         "minimist": {
         "minimist": {
           "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
           "version": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
@@ -271,11 +342,27 @@
       "version": "https://registry.npmjs.org/mocha/-/mocha-3.3.0.tgz",
       "version": "https://registry.npmjs.org/mocha/-/mocha-3.3.0.tgz",
       "integrity": "sha1-0pt0KNP1LILi5l3x7LcGThqrv7U=",
       "integrity": "sha1-0pt0KNP1LILi5l3x7LcGThqrv7U=",
       "dev": true,
       "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": {
       "dependencies": {
         "debug": {
         "debug": {
           "version": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz",
           "version": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz",
           "integrity": "sha1-vFlryr52F/Edn6FTYe3tVgi4SZs=",
           "integrity": "sha1-vFlryr52F/Edn6FTYe3tVgi4SZs=",
-          "dev": true
+          "dev": true,
+          "requires": {
+            "ms": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz"
+          }
         }
         }
       }
       }
     },
     },
@@ -295,23 +382,39 @@
     "once": {
     "once": {
       "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "version": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "wrappy": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
+      }
     },
     },
     "optimist": {
     "optimist": {
       "version": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
       "version": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
-      "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY="
+      "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"
+      }
     },
     },
     "parsejson": {
     "parsejson": {
       "version": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
       "version": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
-      "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs="
+      "integrity": "sha1-q343WfIJ7OmUN5c/fQ8fZK4OZKs=",
+      "requires": {
+        "better-assert": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz"
+      }
     },
     },
     "parseqs": {
     "parseqs": {
       "version": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
       "version": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
-      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0="
+      "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
+      "requires": {
+        "better-assert": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz"
+      }
     },
     },
     "parseuri": {
     "parseuri": {
       "version": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
       "version": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
-      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo="
+      "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
+      "requires": {
+        "better-assert": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz"
+      }
     },
     },
     "path-is-absolute": {
     "path-is-absolute": {
       "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
       "version": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
@@ -326,17 +429,31 @@
     "should": {
     "should": {
       "version": "https://registry.npmjs.org/should/-/should-11.2.1.tgz",
       "version": "https://registry.npmjs.org/should/-/should-11.2.1.tgz",
       "integrity": "sha1-kPVRRVUtAc/CAGZuToGKHJZw7aI=",
       "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": {
     "should-equal": {
       "version": "https://registry.npmjs.org/should-equal/-/should-equal-1.0.1.tgz",
       "version": "https://registry.npmjs.org/should-equal/-/should-equal-1.0.1.tgz",
       "integrity": "sha1-C26VFvJgGp+wuy3MNpr6HH4gCvc=",
       "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": {
     "should-format": {
       "version": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
       "version": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz",
       "integrity": "sha1-m/yPdPo5IFxT04w01xcwPidxJPE=",
       "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": {
     "should-type": {
       "version": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
       "version": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz",
@@ -346,7 +463,11 @@
     "should-type-adaptors": {
     "should-type-adaptors": {
       "version": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.0.1.tgz",
       "version": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.0.1.tgz",
       "integrity": "sha1-7+VVPN9oz/ZuXF9RtxLcNRx3vqo=",
       "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": {
     "should-util": {
       "version": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz",
       "version": "https://registry.npmjs.org/should-util/-/should-util-1.0.0.tgz",
@@ -357,6 +478,14 @@
       "version": "2.0.2",
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.2.tgz",
       "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.2.tgz",
       "integrity": "sha1-EzvzobZ9AvKsZRA8EfeObyxPOzo=",
       "integrity": "sha1-EzvzobZ9AvKsZRA8EfeObyxPOzo=",
+      "requires": {
+        "debug": "2.6.8",
+        "engine.io": "3.1.0",
+        "object-assign": "4.1.1",
+        "socket.io-adapter": "1.1.0",
+        "socket.io-client": "2.0.2",
+        "socket.io-parser": "3.1.2"
+      },
       "dependencies": {
       "dependencies": {
         "component-emitter": {
         "component-emitter": {
           "version": "1.2.1",
           "version": "1.2.1",
@@ -366,17 +495,36 @@
         "debug": {
         "debug": {
           "version": "2.6.8",
           "version": "2.6.8",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
-          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw="
+          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
+          "requires": {
+            "ms": "2.0.0"
+          }
         },
         },
         "engine.io": {
         "engine.io": {
           "version": "3.1.0",
           "version": "3.1.0",
           "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.0.tgz",
           "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.0.tgz",
-          "integrity": "sha1-XKQ4486f28kVxKIcjdnhJmcG5X4="
+          "integrity": "sha1-XKQ4486f28kVxKIcjdnhJmcG5X4=",
+          "requires": {
+            "accepts": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
+            "base64id": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+            "cookie": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+            "debug": "2.6.8",
+            "engine.io-parser": "2.1.1",
+            "uws": "0.14.5",
+            "ws": "2.3.1"
+          }
         },
         },
         "engine.io-parser": {
         "engine.io-parser": {
           "version": "2.1.1",
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz",
           "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz",
-          "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg="
+          "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg=",
+          "requires": {
+            "after": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+            "arraybuffer.slice": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
+            "base64-arraybuffer": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+            "blob": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+            "has-binary2": "1.0.2"
+          }
         },
         },
         "isarray": {
         "isarray": {
           "version": "2.0.1",
           "version": "2.0.1",
@@ -397,11 +545,17 @@
           "version": "1.1.0",
           "version": "1.1.0",
           "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.0.tgz",
           "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.0.tgz",
           "integrity": "sha1-x6pGUB3VVsLLiiivj/lcC14dqkw=",
           "integrity": "sha1-x6pGUB3VVsLLiiivj/lcC14dqkw=",
+          "requires": {
+            "debug": "2.3.3"
+          },
           "dependencies": {
           "dependencies": {
             "debug": {
             "debug": {
               "version": "2.3.3",
               "version": "2.3.3",
               "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
               "resolved": "https://registry.npmjs.org/debug/-/debug-2.3.3.tgz",
-              "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w="
+              "integrity": "sha1-QMRT5n5uE8kB3ewxeviYbNqe/4w=",
+              "requires": {
+                "ms": "0.7.2"
+              }
             },
             },
             "ms": {
             "ms": {
               "version": "0.7.2",
               "version": "0.7.2",
@@ -413,7 +567,13 @@
         "socket.io-parser": {
         "socket.io-parser": {
           "version": "3.1.2",
           "version": "3.1.2",
           "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
           "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
-          "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I="
+          "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=",
+          "requires": {
+            "component-emitter": "1.2.1",
+            "debug": "2.6.8",
+            "has-binary2": "1.0.2",
+            "isarray": "2.0.1"
+          }
         },
         },
         "ultron": {
         "ultron": {
           "version": "1.1.0",
           "version": "1.1.0",
@@ -423,7 +583,11 @@
         "ws": {
         "ws": {
           "version": "2.3.1",
           "version": "2.3.1",
           "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz",
           "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz",
-          "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA="
+          "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA=",
+          "requires": {
+            "safe-buffer": "5.0.1",
+            "ultron": "1.1.0"
+          }
         }
         }
       }
       }
     },
     },
@@ -431,6 +595,21 @@
       "version": "2.0.2",
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.2.tgz",
       "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.2.tgz",
       "integrity": "sha1-hvWdtZuhFockIg85viga1Jr3QsE=",
       "integrity": "sha1-hvWdtZuhFockIg85viga1Jr3QsE=",
+      "requires": {
+        "backo2": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+        "base64-arraybuffer": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+        "component-bind": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+        "component-emitter": "1.2.1",
+        "debug": "2.6.8",
+        "engine.io-client": "3.1.1",
+        "has-cors": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+        "indexof": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+        "object-component": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
+        "parseqs": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+        "parseuri": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+        "socket.io-parser": "3.1.2",
+        "to-array": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz"
+      },
       "dependencies": {
       "dependencies": {
         "component-emitter": {
         "component-emitter": {
           "version": "1.2.1",
           "version": "1.2.1",
@@ -440,17 +619,41 @@
         "debug": {
         "debug": {
           "version": "2.6.8",
           "version": "2.6.8",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
           "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
-          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw="
+          "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
+          "requires": {
+            "ms": "2.0.0"
+          }
         },
         },
         "engine.io-client": {
         "engine.io-client": {
           "version": "3.1.1",
           "version": "3.1.1",
           "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.1.tgz",
           "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.1.tgz",
-          "integrity": "sha1-QVqYUrrbFPoAj6PvHjFgjbZ2EyU="
+          "integrity": "sha1-QVqYUrrbFPoAj6PvHjFgjbZ2EyU=",
+          "requires": {
+            "component-emitter": "1.2.1",
+            "component-inherit": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+            "debug": "2.6.8",
+            "engine.io-parser": "2.1.1",
+            "has-cors": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
+            "indexof": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+            "parsejson": "https://registry.npmjs.org/parsejson/-/parsejson-0.0.3.tgz",
+            "parseqs": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
+            "parseuri": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
+            "ws": "2.3.1",
+            "xmlhttprequest-ssl": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz",
+            "yeast": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz"
+          }
         },
         },
         "engine.io-parser": {
         "engine.io-parser": {
           "version": "2.1.1",
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz",
           "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.1.tgz",
-          "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg="
+          "integrity": "sha1-4Ps/DgRi9/WLt3waUun1p+JuRmg=",
+          "requires": {
+            "after": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+            "arraybuffer.slice": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.6.tgz",
+            "base64-arraybuffer": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+            "blob": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+            "has-binary2": "1.0.2"
+          }
         },
         },
         "isarray": {
         "isarray": {
           "version": "2.0.1",
           "version": "2.0.1",
@@ -465,7 +668,13 @@
         "socket.io-parser": {
         "socket.io-parser": {
           "version": "3.1.2",
           "version": "3.1.2",
           "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
           "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
-          "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I="
+          "integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=",
+          "requires": {
+            "component-emitter": "1.2.1",
+            "debug": "2.6.8",
+            "has-binary2": "1.0.2",
+            "isarray": "2.0.1"
+          }
         },
         },
         "ultron": {
         "ultron": {
           "version": "1.1.0",
           "version": "1.1.0",
@@ -475,7 +684,11 @@
         "ws": {
         "ws": {
           "version": "2.3.1",
           "version": "2.3.1",
           "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz",
           "resolved": "https://registry.npmjs.org/ws/-/ws-2.3.1.tgz",
-          "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA="
+          "integrity": "sha1-a5Sz5EfLajY/eF6vlK9jWejoHIA=",
+          "requires": {
+            "safe-buffer": "5.0.1",
+            "ultron": "1.1.0"
+          }
         }
         }
       }
       }
     },
     },
@@ -486,7 +699,10 @@
     "supports-color": {
     "supports-color": {
       "version": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz",
       "version": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz",
       "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=",
       "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=",
-      "dev": true
+      "dev": true,
+      "requires": {
+        "has-flag": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz"
+      }
     },
     },
     "to-array": {
     "to-array": {
       "version": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
       "version": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",

+ 6 - 6
package.json

@@ -18,12 +18,12 @@
     "url": "https://github.com/NikolaySuslov/livecodingspace.git"
     "url": "https://github.com/NikolaySuslov/livecodingspace.git"
   },
   },
   "dependencies": {
   "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",
     "optimist": "0.6.1",
     "fs-extra": "3.0.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
 extends: http://vwf.example.com/aframe/ascene.vwf
 properties:
 properties:
   fog: "type: linear; color: #ECECEC; far: 9; near: 0"
   fog: "type: linear; color: #ECECEC; far: 9; near: 0"
+  transparent: true
 children:
 children:
+  sky:
+    extends: http://vwf.example.com/aframe/asky.vwf
+    properties:
+      color: "#ECECEC"
+      side: "back"
+      fog: false
   spaceText:
   spaceText:
     extends: http://vwf.example.com/aframe/atext.vwf
     extends: http://vwf.example.com/aframe/atext.vwf
     properties:
     properties:
@@ -84,7 +91,6 @@ children:
       box2:
       box2:
         extends: http://vwf.example.com/aframe/abox.vwf
         extends: http://vwf.example.com/aframe/abox.vwf
         properties:
         properties:
-          src: "#bg"
           position: "2 -0.75 0"
           position: "2 -0.75 0"
           color: "#2167a5"
           color: "#2167a5"
           depth: 1
           depth: 1
@@ -93,20 +99,15 @@ children:
             extends: http://vwf.example.com/aframe/interpolation-component.vwf
             extends: http://vwf.example.com/aframe/interpolation-component.vwf
             properties:
             properties:
               enabled: true
               enabled: true
-              duration: 50
-              deltaPos: 0.1
-              deltaRot: 1
         methods:
         methods:
           run:
           run:
             body: |
             body: |
               var time = vwf.now;
               var time = vwf.now;
+              let rot = AFRAME.utils.coordinates.parse(this.rotation);
               let pos = AFRAME.utils.coordinates.parse(this.position);
               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
               this.future( 0.01 ).run();  // schedule the next step
-  sky:
-    extends: http://vwf.example.com/aframe/asky.vwf
-    properties:
-      color: "#ECECEC"
 methods:
 methods:
   initialize:
   initialize:
     body: |
     body: |

+ 1 - 1
public/aframe2/assets.json

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

+ 3 - 16
public/aframe2/index.vwf.yaml

@@ -4,6 +4,7 @@
 extends: http://vwf.example.com/aframe/ascene.vwf
 extends: http://vwf.example.com/aframe/ascene.vwf
 properties:
 properties:
   fog: "type: linear; color: #ECECEC; far: 30; near: 0"
   fog: "type: linear; color: #ECECEC; far: 30; near: 0"
+  transparent: true
   assets: "assets.json"
   assets: "assets.json"
 children:
 children:
   myLight:
   myLight:
@@ -18,8 +19,7 @@ children:
     properties:
     properties:
       src: "#plane"
       src: "#plane"
       position: "-1.0 1.7 -3"
       position: "-1.0 1.7 -3"
-      rotation: "0 -45 0"
-      scale: "10 10 10"
+      scale: "0.5 0.5 0.5"
   spaceText:
   spaceText:
     extends: http://vwf.example.com/aframe/atext.vwf
     extends: http://vwf.example.com/aframe/atext.vwf
     properties:
     properties:
@@ -41,19 +41,6 @@ children:
       depth: 2
       depth: 2
       height: 1
       height: 1
       width: 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:
   box:
     extends: http://vwf.example.com/aframe/abox.vwf
     extends: http://vwf.example.com/aframe/abox.vwf
     properties:
     properties:
@@ -143,8 +130,8 @@ children:
   sky:
   sky:
     extends: http://vwf.example.com/aframe/asky.vwf
     extends: http://vwf.example.com/aframe/asky.vwf
     properties:
     properties:
-      color: "#ECECEC"
       src: "#sky"
       src: "#sky"
+      side: "back"
       fog: false
       fog: false
   groundPlane:
   groundPlane:
     extends: http://vwf.example.com/aframe/aplane.vwf
     extends: http://vwf.example.com/aframe/aplane.vwf

BIN
public/assets/images/planeDiffuse.png


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 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>

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 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


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 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


+ 66 - 3
support/client/lib/vwf/model/aframe.js

@@ -78,7 +78,7 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                     return found;
                     return found;
                 },
                 },
                 setAFrameProperty: function (propertyName, propertyValue, aframeObject) {
                 setAFrameProperty: function (propertyName, propertyValue, aframeObject) {
-                    
+                    //console.log(propertyValue);
                             if (propertyValue.hasOwnProperty('x')) {
                             if (propertyValue.hasOwnProperty('x')) {
                                 aframeObject.setAttribute(propertyName, propertyValue)
                                 aframeObject.setAttribute(propertyName, propertyValue)
                             } else
                             } else
@@ -345,6 +345,9 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                             aframeObject.setAttribute('repeat', propertyValue);
                             aframeObject.setAttribute('repeat', propertyValue);
                             break;
                             break;
 
 
+                        case "side":
+                            aframeObject.setAttribute('material', {'side': propertyValue});
+                            break;
 
 
                         case "look-controls-enabled":
                         case "look-controls-enabled":
                             aframeObject.setAttribute('look-controls', 'enabled', propertyValue);
                             aframeObject.setAttribute('look-controls', 'enabled', propertyValue);
@@ -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") {
                 if (value === undefined && aframeObject.nodeName == "A-TEXT") {
                     value = propertyValue;
                     value = propertyValue;
 
 
@@ -388,6 +414,14 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
 
 
                     switch (propertyName) {
                     switch (propertyName) {
 
 
+                        case "color":
+                        aframeObject.setAttribute('background', {'color': propertyValue} );
+                        break;
+
+                        case "transparent":
+                        aframeObject.setAttribute('background', {'transparent': propertyValue} );
+                        break;
+
                         case "fog":
                         case "fog":
                             aframeObject.setAttribute('fog', propertyValue);
                             aframeObject.setAttribute('fog', propertyValue);
                             break;
                             break;
@@ -674,6 +708,12 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                             value = aframeObject.getAttribute('color');
                             value = aframeObject.getAttribute('color');
                             break;
                             break;
 
 
+                        case "side":
+                        if (aframeObject.getAttribute('material')) {
+                            value = aframeObject.getAttribute('material').side;
+                        }
+                            break;
+
                         case "fog":
                         case "fog":
                             if (aframeObject.getAttribute('material')) {
                             if (aframeObject.getAttribute('material')) {
                                 value = aframeObject.getAttribute('material').fog;
                                 value = aframeObject.getAttribute('material').fog;
@@ -736,9 +776,32 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                         case "fog":
                         case "fog":
                             value = aframeObject.getAttribute('fog');
                             value = aframeObject.getAttribute('fog');
                             break;
                             break;
+                        
+                        case "color":
+                            value = aframeObject.getAttribute('background').color;
+                            break;
+
+                        case "transparent":
+                            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") {
                 if (value === undefined && aframeObject.nodeName == "A-BOX") {
 
 
                     switch (propertyName) {
                     switch (propertyName) {
@@ -923,6 +986,8 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
 
 
             self.state.scenes[node.ID] = aframeObj;
             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")) {
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/acamera.vwf")) {
             aframeObj = document.createElement('a-camera');
             aframeObj = document.createElement('a-camera');
             aframeObj.setAttribute('camera', 'active', false);
             aframeObj.setAttribute('camera', 'active', false);
@@ -931,8 +996,6 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
             aframeObj = document.createElement('a-light');
             aframeObj = document.createElement('a-light');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/acursor.vwf")) {
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/acursor.vwf")) {
             aframeObj = document.createElement('a-cursor');
             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")) {
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/a-sun-sky.vwf")) {
                 aframeObj = document.createElement('a-sun-sky');
                 aframeObj = document.createElement('a-sun-sky');
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/abox.vwf")) {
         } else if (self.state.isAFrameClass(protos, "http://vwf.example.com/aframe/abox.vwf")) {

+ 3 - 3
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);
             console.log('I was clicked at: ', evt.detail.intersection.point);
             let cursorID = 'cursor-avatar-' + vwf_view.kernel.moniker();
             let cursorID = 'cursor-avatar-' + vwf_view.kernel.moniker();
             if (evt.detail.cursorEl.id.includes(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")
             //vwf_view.kernel.fireEvent(evt.detail.target.id, "clickEvent")
@@ -157,7 +157,7 @@ AFRAME.registerComponent('raycaster-listener', {
 
 
                 } else {
                 } else {
                     console.log('I was intersected at: ', evt.detail.intersection.point);
                     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;
                 self.casters[evt.detail.el.id] = evt.detail.el;
@@ -176,7 +176,7 @@ AFRAME.registerComponent('raycaster-listener', {
                 if (self.intersected) {
                 if (self.intersected) {
                     console.log('Clear intersection');
                     console.log('Clear intersection');
                     if (Object.entries(self.casters).length == 1 && (self.casters[evt.detail.el.id] !== undefined)) {
                     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]
                     delete self.casters[evt.detail.el.id]
                 } else { }
                 } else { }

+ 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){
 (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();
 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 Kyle-Larson https://github.com/Kyle-Larson
  * @author Takahiro https://github.com/takahirox
  * @author Takahiro https://github.com/takahirox
@@ -5899,77 +5899,13 @@ var loadLoader = (function () {
 }());
 }());
 
 
 },{"../../lib/fetch-script":4}],8:[function(require,module,exports){
 },{"../../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 = {
 module.exports = {
   'animation-mixer': require('./animation-mixer'),
   'animation-mixer': require('./animation-mixer'),
   'fbx-model': require('./fbx-model'),
   'fbx-model': require('./fbx-model'),
-  'gltf-model-next': require('./gltf-model-next'),
   'gltf-model-legacy': require('./gltf-model-legacy'),
   'gltf-model-legacy': require('./gltf-model-legacy'),
   'json-model': require('./json-model'),
   'json-model': require('./json-model'),
   'object-model': require('./object-model'),
   'object-model': require('./object-model'),
   'ply-model': require('./ply-model'),
   'ply-model': require('./ply-model'),
-  'three-model': require('./three-model'),
 
 
   registerAll: function (AFRAME) {
   registerAll: function (AFRAME) {
     if (this._registered) return;
     if (this._registered) return;
@@ -5994,11 +5930,6 @@ module.exports = {
       AFRAME.registerComponent('fbx-model', this['fbx-model']);
       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
     // THREE.GLTFLoader
     if (!AFRAME.components['gltf-model-legacy']) {
     if (!AFRAME.components['gltf-model-legacy']) {
       AFRAME.registerComponent('gltf-model-legacy', this['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']);
       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;
     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
  * json-model
  *
  *
@@ -6083,7 +6009,7 @@ module.exports = {
   }
   }
 };
 };
 
 
-},{}],11:[function(require,module,exports){
+},{}],10:[function(require,module,exports){
 /**
 /**
  * object-model
  * object-model
  *
  *
@@ -6138,7 +6064,7 @@ module.exports = {
   }
   }
 };
 };
 
 
-},{}],12:[function(require,module,exports){
+},{}],11:[function(require,module,exports){
 /**
 /**
  * ply-model
  * 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)
 The MIT License (MIT)
 Copyright (c) 2017 Nikolai Suslov
 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
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 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.');
   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 () { },
   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(),
     var position = new THREE.Vector3(),
         quaternion = new THREE.Quaternion(),
         quaternion = new THREE.Quaternion(),
         scale = new THREE.Vector3();
         scale = new THREE.Vector3();
-    if (meshes[0].geometry instanceof THREE.BufferGeometry) {
+    if (meshes[0].geometry.isBufferGeometry) {
       if (meshes[0].geometry.attributes.position) {
       if (meshes[0].geometry.attributes.position) {
         tmp.fromBufferGeometry(meshes[0].geometry);
         tmp.fromBufferGeometry(meshes[0].geometry);
       }
       }
@@ -16465,7 +16465,7 @@ function getGeometry (object) {
   // Recursively merge geometry, preserving local transforms.
   // Recursively merge geometry, preserving local transforms.
   while ((mesh = meshes.pop())) {
   while ((mesh = meshes.pop())) {
     mesh.updateMatrixWorld();
     mesh.updateMatrixWorld();
-    if (mesh.geometry instanceof THREE.BufferGeometry) {
+    if (mesh.geometry.isBufferGeometry) {
       tmp.fromBufferGeometry(mesh.geometry);
       tmp.fromBufferGeometry(mesh.geometry);
       combined.merge(tmp, mesh.matrixWorld);
       combined.merge(tmp, mesh.matrixWorld);
     } else {
     } else {
@@ -16989,14 +16989,21 @@ module.exports = {
     if (this.checkpoint === checkpoint) return;
     if (this.checkpoint === checkpoint) return;
 
 
     if (this.checkpoint) {
     if (this.checkpoint) {
-      el.emit('navigation-end', {checkpoint: checkpoint});
+      el.emit('navigation-end', {checkpoint: this.checkpoint});
     }
     }
 
 
     this.checkpoint = 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});
     el.emit('navigation-start', {checkpoint: checkpoint});
 
 
     if (this.data.mode === 'teleport') {
     if (this.data.mode === 'teleport') {
-      this.sync();
       this.el.setAttribute('position', this.targetPosition);
       this.el.setAttribute('position', this.targetPosition);
       this.checkpoint = null;
       this.checkpoint = null;
       el.emit('navigation-end', {checkpoint: checkpoint});
       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 Kyle-Larson https://github.com/Kyle-Larson
  * @author Takahiro https://github.com/takahirox
  * @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');
 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){
 },{"../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 {
     } 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;
       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 = {
 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 () {
   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 = {
 module.exports = {
   schema: {
   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 = {
 module.exports = {
-  dependencies: ['velocity'],
   schema: {
   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 = {
 module.exports = {
+  schema: {
+    enabled: { default: true }
+  },
+
   init: function () {
   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: {
   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 () {
   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 {
     } 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: {
   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 () {
   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 {
       } 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){
 (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();
 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 Kyle-Larson https://github.com/Kyle-Larson
  * @author Takahiro https://github.com/takahirox
  * @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.
  * Legacy loader for glTF 1.0 models.
  * Asynchronously loads THREE.GLTFLoader from rawgit.
  * 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 = {
 module.exports = {
   schema: {type: 'model'},
   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 = {
 module.exports = {
   'animation-mixer': require('./animation-mixer'),
   'animation-mixer': require('./animation-mixer'),
   'fbx-model': require('./fbx-model'),
   'fbx-model': require('./fbx-model'),
-  'gltf-model-next': require('./gltf-model-next'),
   'gltf-model-legacy': require('./gltf-model-legacy'),
   'gltf-model-legacy': require('./gltf-model-legacy'),
   'json-model': require('./json-model'),
   'json-model': require('./json-model'),
   'object-model': require('./object-model'),
   'object-model': require('./object-model'),
   'ply-model': require('./ply-model'),
   'ply-model': require('./ply-model'),
-  'three-model': require('./three-model'),
 
 
   registerAll: function (AFRAME) {
   registerAll: function (AFRAME) {
     if (this._registered) return;
     if (this._registered) return;
@@ -6035,15 +5930,9 @@ module.exports = {
       AFRAME.registerComponent('fbx-model', this['fbx-model']);
       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
     // THREE.GLTFLoader
     if (!AFRAME.components['gltf-model-legacy']) {
     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
     // THREE.JsonLoader
@@ -6056,16 +5945,11 @@ module.exports = {
       AFRAME.registerComponent('object-model', this['object-model']);
       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;
     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
  * json-model
  *
  *
@@ -6125,7 +6009,7 @@ module.exports = {
   }
   }
 };
 };
 
 
-},{}],11:[function(require,module,exports){
+},{}],10:[function(require,module,exports){
 /**
 /**
  * object-model
  * object-model
  *
  *
@@ -6180,7 +6064,7 @@ module.exports = {
   }
   }
 };
 };
 
 
-},{}],12:[function(require,module,exports){
+},{}],11:[function(require,module,exports){
 /**
 /**
  * ply-model
  * 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(),
     var position = new THREE.Vector3(),
         quaternion = new THREE.Quaternion(),
         quaternion = new THREE.Quaternion(),
         scale = new THREE.Vector3();
         scale = new THREE.Vector3();
-    if (meshes[0].geometry instanceof THREE.BufferGeometry) {
+    if (meshes[0].geometry.isBufferGeometry) {
       if (meshes[0].geometry.attributes.position) {
       if (meshes[0].geometry.attributes.position) {
         tmp.fromBufferGeometry(meshes[0].geometry);
         tmp.fromBufferGeometry(meshes[0].geometry);
       }
       }
@@ -15699,7 +15699,7 @@ function getGeometry (object) {
   // Recursively merge geometry, preserving local transforms.
   // Recursively merge geometry, preserving local transforms.
   while ((mesh = meshes.pop())) {
   while ((mesh = meshes.pop())) {
     mesh.updateMatrixWorld();
     mesh.updateMatrixWorld();
-    if (mesh.geometry instanceof THREE.BufferGeometry) {
+    if (mesh.geometry.isBufferGeometry) {
       tmp.fromBufferGeometry(mesh.geometry);
       tmp.fromBufferGeometry(mesh.geometry);
       combined.merge(tmp, mesh.matrixWorld);
       combined.merge(tmp, mesh.matrixWorld);
     } else {
     } else {
@@ -16550,7 +16550,9 @@ module.exports = {
 
 
       for (var i = 0, contact; (contact = this.system.world.contacts[i]); i++) {
       for (var i = 0, contact; (contact = this.system.world.contacts[i]); i++) {
         // 1. Find any collisions involving this element. Get the contact
         // 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) {
         if (body.id === contact.bi.id) {
           contact.ni.negate(currentSurfaceNormal);
           contact.ni.negate(currentSurfaceNormal);
         } else if (body.id === contact.bj.id) {
         } else if (body.id === contact.bj.id) {
@@ -16685,6 +16687,7 @@ module.exports = {
     this.collisions = [];
     this.collisions = [];
 
 
     this.handleHit = this.handleHit.bind(this);
     this.handleHit = this.handleHit.bind(this);
+    this.handleHitEnd = this.handleHitEnd.bind(this);
   },
   },
 
 
   remove: function () {
   remove: function () {
@@ -16760,9 +16763,7 @@ module.exports = {
       // Remove collision state from other elements.
       // Remove collision state from other elements.
       this.collisions.filter(function (el) {
       this.collisions.filter(function (el) {
         return !distanceMap.has(el);
         return !distanceMap.has(el);
-      }).forEach(function removeState (el) {
-        el.removeState(data.state);
-      });
+      }).forEach(this.handleHitEnd);
 
 
       // Store new collisions
       // Store new collisions
       this.collisions = collisions;
       this.collisions = collisions;
@@ -16802,6 +16803,11 @@ module.exports = {
     targetEl.emit('hit');
     targetEl.emit('hit');
     targetEl.addState(this.data.state);
     targetEl.addState(this.data.state);
     this.el.emit('hit', {el: targetEl});
     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){
 (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();
 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 BinaryHeap = require('./BinaryHeap');
 const utils = require('./utils.js');
 const utils = require('./utils.js');
 
 
@@ -341,7 +125,7 @@ class AStar {
 
 
 module.exports = 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
 // javascript-astar
 // http://github.com/bgrins/javascript-astar
 // http://github.com/bgrins/javascript-astar
 // Freely distributable under the MIT License.
 // Freely distributable under the MIT License.
@@ -477,7 +261,7 @@ class BinaryHeap {
 
 
 module.exports = BinaryHeap;
 module.exports = BinaryHeap;
 
 
-},{}],8:[function(require,module,exports){
+},{}],4:[function(require,module,exports){
 const utils = require('./utils');
 const utils = require('./utils');
 
 
 class Channel {
 class Channel {
@@ -572,7 +356,7 @@ class Channel {
 
 
 module.exports = Channel;
 module.exports = Channel;
 
 
-},{"./utils":10}],9:[function(require,module,exports){
+},{"./utils":6}],5:[function(require,module,exports){
 const utils = require('./utils');
 const utils = require('./utils');
 const AStar = require('./AStar');
 const AStar = require('./AStar');
 const Channel = require('./Channel');
 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 {
 class Utils {
 
 
   static computeCentroids (geometry) {
   static computeCentroids (geometry) {
@@ -1212,4 +996,220 @@ class Utils {
 
 
 module.exports = 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.collisions = [];
 
 
     this.handleHit = this.handleHit.bind(this);
     this.handleHit = this.handleHit.bind(this);
+    this.handleHitEnd = this.handleHitEnd.bind(this);
   },
   },
 
 
   remove: function () {
   remove: function () {
@@ -103,9 +104,7 @@ module.exports = {
       // Remove collision state from other elements.
       // Remove collision state from other elements.
       this.collisions.filter(function (el) {
       this.collisions.filter(function (el) {
         return !distanceMap.has(el);
         return !distanceMap.has(el);
-      }).forEach(function removeState (el) {
-        el.removeState(data.state);
-      });
+      }).forEach(this.handleHitEnd);
 
 
       // Store new collisions
       // Store new collisions
       this.collisions = collisions;
       this.collisions = collisions;
@@ -145,6 +144,11 @@ module.exports = {
     targetEl.emit('hit');
     targetEl.emit('hit');
     targetEl.addState(this.data.state);
     targetEl.addState(this.data.state);
     this.el.emit('hit', {el: targetEl});
     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


+ 0 - 8
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);
                             parentNodeAF.setAttribute(aframeObject.compName, 'enabled', propertyValue);
                             break;
                             break;
 
 
-                        case "duration":
-                            parentNodeAF.setAttribute(aframeObject.compName, 'duration', propertyValue);
-                            break;
-
                         case "deltaPos":
                         case "deltaPos":
                             parentNodeAF.setAttribute(aframeObject.compName, 'deltaPos', propertyValue);
                             parentNodeAF.setAttribute(aframeObject.compName, 'deltaPos', propertyValue);
                             break;
                             break;
@@ -621,10 +617,6 @@ define(["module", "vwf/model", "vwf/utility"], function (module, model, utility)
                             value = parentNodeAF.getAttribute(aframeObject.compName).enabled;
                             value = parentNodeAF.getAttribute(aframeObject.compName).enabled;
                             break;
                             break;
 
 
-                        case "duration":
-                            value = parentNodeAF.getAttribute(aframeObject.compName).duration;
-                            break;
-
                         case "deltaPos":
                         case "deltaPos":
                             value = parentNodeAF.getAttribute(aframeObject.compName).deltaPos;
                             value = parentNodeAF.getAttribute(aframeObject.compName).deltaPos;
                             break;
                             break;

+ 33 - 7
support/client/lib/vwf/view/aframe.js

@@ -32,6 +32,11 @@ define(["module", "vwf/view"], function (module, view) {
             self = this;
             self = this;
             this.nodes = {};
             this.nodes = {};
 
 
+            // this.tickTime = 0;
+            // this.realTickDif = 50;
+            // this.lastrealTickDif = 50;
+            // this.lastRealTick = performance.now();
+
             this.state.appInitialized = false;
             this.state.appInitialized = false;
 
 
             if (options === undefined) { options = {}; }
             if (options === undefined) { options = {}; }
@@ -98,7 +103,12 @@ define(["module", "vwf/view"], function (module, view) {
             }
             }
 
 
             if (this.state.nodes[childID] && this.state.nodes[childID].aframeObj) {
             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]) {
             // if(this.state.nodes[childID]) {
@@ -137,6 +147,11 @@ define(["module", "vwf/view"], function (module, view) {
                 return;
                 return;
             }
             }
 
 
+            // if (propertyName == 'position') {
+            //     this.nodes[nodeId].lastTransformStep = vwf.time();
+            // }
+
+
             // var aframeObject = node.aframeObj;
             // var aframeObject = node.aframeObj;
             // switch (propertyName) {
             // switch (propertyName) {
             //     case "clickable":
             //     case "clickable":
@@ -209,13 +224,14 @@ define(["module", "vwf/view"], function (module, view) {
                 updateHandControllerVR('wmrvr-left-', '#wmrvrcontrolleft');
                 updateHandControllerVR('wmrvr-left-', '#wmrvrcontrolleft');
             }
             }
 
 
-
-            //lerpTick ()
+           // console.log(vwfTime);
+            //lerpTick ();
         }
         }
 
 
 
 
     });
     });
 
 
+
     function compareCoordinates(a, b) {
     function compareCoordinates(a, b) {
         return a.x !== b.x || a.y !== b.y || a.z !== b.z
         return a.x !== b.x || a.y !== b.y || a.z !== b.z
 
 
@@ -233,16 +249,20 @@ define(["module", "vwf/view"], function (module, view) {
             let position = el.getAttribute('position');
             let position = el.getAttribute('position');
             let rotation = el.getAttribute('rotation');
             let rotation = el.getAttribute('rotation');
 
 
+            let lastRotation = self.nodes[avatarName].selfTickRotation;
+
             let currentPosition = node.aframeObj.getAttribute('position');
             let currentPosition = node.aframeObj.getAttribute('position');
             let currentRotation = node.aframeObj.getAttribute('rotation');
             let currentRotation = node.aframeObj.getAttribute('rotation');
 
 
-            if (position && rotation && currentPosition && currentRotation) {
-                if (compareCoordinates(position, currentPosition) || rotation.y !== currentRotation.y) {
+            if (position && rotation && currentPosition && currentRotation && lastRotation) {
+            if (compareCoordinates(position, currentPosition) || rotation.y !== lastRotation.y) {
                     console.log("not equal!!")
                     console.log("not equal!!")
                     vwf_view.kernel.callMethod(avatarName, "followAvatarControl", [position, rotation]);
                     vwf_view.kernel.callMethod(avatarName, "followAvatarControl", [position, rotation]);
                 }
                 }
             }
             }
+            self.nodes[avatarName].selfTickRotation = Object.assign({}, el.getAttribute('rotation'));
         }
         }
+
     }
     }
 
 
 
 
@@ -258,11 +278,13 @@ define(["module", "vwf/view"], function (module, view) {
             let position = el.getAttribute('position');
             let position = el.getAttribute('position');
             let rotation = el.getAttribute('rotation');
             let rotation = el.getAttribute('rotation');
 
 
+            let lastRotation = self.nodes[avatarName].selfTickRotation;
+
             let currentPosition = node.aframeObj.getAttribute('position');
             let currentPosition = node.aframeObj.getAttribute('position');
             let currentRotation = node.aframeObj.getAttribute('rotation');
             let currentRotation = node.aframeObj.getAttribute('rotation');
 
 
-            if (position && rotation && currentPosition && currentRotation) {
-                if (compareCoordinates(position, currentPosition) || compareCoordinates(rotation, currentRotation)) {
+            if (position && rotation && currentPosition && currentRotation && lastRotation) {
+                if (compareCoordinates(position, currentPosition) || compareCoordinates(rotation, lastRotation)) {
                     console.log("not equal!!");
                     console.log("not equal!!");
 
 
                     vwf_view.kernel.setProperty(avatarName, "rotation", AFRAME.utils.coordinates.stringify(rotation));
                     vwf_view.kernel.setProperty(avatarName, "rotation", AFRAME.utils.coordinates.stringify(rotation));
@@ -270,6 +292,8 @@ define(["module", "vwf/view"], function (module, view) {
                 }
                 }
             }
             }
 
 
+            self.nodes[avatarName].selfTickRotation = Object.assign({}, el.getAttribute('rotation'));
+
         }
         }
     }
     }
 
 
@@ -283,8 +307,10 @@ define(["module", "vwf/view"], function (module, view) {
         controlEl.setAttribute('id', 'avatarControl');
         controlEl.setAttribute('id', 'avatarControl');
         controlEl.setAttribute('wasd-controls', {});
         controlEl.setAttribute('wasd-controls', {});
         controlEl.setAttribute('look-controls', {});
         controlEl.setAttribute('look-controls', {});
+        controlEl.setAttribute('look-controls', 'userHeight', 1.6)
         controlEl.setAttribute('gamepad-controls', {});
         controlEl.setAttribute('gamepad-controls', {});
         controlEl.setAttribute('camera', 'active', true);
         controlEl.setAttribute('camera', 'active', true);
+        controlEl.setAttribute('camera', 'userHeight', 1.6);
        // controlEl.setAttribute('camera', 'near', 0.51);
        // controlEl.setAttribute('camera', 'near', 0.51);
 
 
         aScene.appendChild(controlEl);
         aScene.appendChild(controlEl);

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

@@ -22,15 +22,21 @@
 /// @requires vwf/view
 /// @requires vwf/view
 
 
 define(["module", "vwf/view"], function (module, view) {
 define(["module", "vwf/view"], function (module, view) {
+    var self;
 
 
     return view.load(module, {
     return view.load(module, {
 
 
         // == Module Definition ====================================================================
         // == Module Definition ====================================================================
 
 
         initialize: function (options) {
         initialize: function (options) {
-            var self = this;
+            self = this;
             this.nodes = {};
             this.nodes = {};
 
 
+            this.tickTime = 0;
+            this.realTickDif = 50;
+            this.lastrealTickDif = 50;
+            this.lastRealTick = performance.now();
+
             this.state.appInitialized = false;
             this.state.appInitialized = false;
             
             
                         if (typeof options == "object") {
                         if (typeof options == "object") {
@@ -62,7 +68,10 @@ define(["module", "vwf/view"], function (module, view) {
                 this.nodes[childID] = {id:childID,extends:childExtendsID};
                 this.nodes[childID] = {id:childID,extends:childExtendsID};
             } 
             } 
             else if (this.state.nodes[childID] && this.state.nodes[childID].aframeObj) {
             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;
                             return;
                         }
                         }
 
 
-
-
+                        
             switch (propertyName) {
             switch (propertyName) {
                 case "color":
                 case "color":
                     if (propertyValue) {
                     if (propertyValue) {
@@ -113,12 +121,73 @@ define(["module", "vwf/view"], function (module, view) {
 
 
         ticked: function (vwfTime) {
         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;
+    }
 
 
 });
 });

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

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

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

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

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

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

+ 8 - 9
support/proxy/vwf.example.com/aframe/avatar.js

@@ -35,7 +35,12 @@ this.createAvatarBody = function (modelSrc) {
     let avatarControl = document.querySelector('#avatarControl');
     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 myColor = this.getRandomColor();
     let myBodyDef = this.simpleBodyDef;
     let myBodyDef = this.simpleBodyDef;
@@ -64,10 +69,7 @@ this.createAvatarBody = function (modelSrc) {
                         "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
                         "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
                         "type": "component",
                         "type": "component",
                         "properties": {
                         "properties": {
-                            "enabled": true,
-                            "duration": 50,
-                            "deltaPos": 0,
-                            "deltaRot": 0
+                            "enabled": true
                         }
                         }
                     },
                     },
                     "visual": {
                     "visual": {
@@ -200,10 +202,7 @@ this.createAvatarBody = function (modelSrc) {
         "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
         "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
         "type": "component",
         "type": "component",
         "properties": {
         "properties": {
-            "enabled": true,
-            "duration": 50,
-            "deltaPos": 0,
-            "deltaRot": 0
+            "enabled": true
         }
         }
     }
     }
 
 

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

@@ -18,13 +18,13 @@ this.simpleDef = {
                 "depth": 0.1
                 "depth": 0.1
             }
             }
         },
         },
-        "interpolation":
-            {
-                "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
-                "type": "component",
-                "properties": {
-                }
-            }
+        // "interpolation":
+        //     {
+        //         "extends": "http://vwf.example.com/aframe/interpolation-component.vwf",
+        //         "type": "component",
+        //         "properties": {
+        //         }
+        //     }
     }
     }
 }
 }
 
 

+ 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"
 type: "component"
 properties:
 properties:
   enabled:
   enabled:
-  duration:
   deltaPos:
   deltaPos:
   deltaRot:
   deltaRot:

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