├── .babelrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── dist
├── aframe-physics-extras.js
└── aframe-physics-extras.min.js
├── examples
├── assets
│ ├── basic.png
│ ├── collisionresponse.png
│ ├── examples.css
│ └── textures
│ │ ├── blue.png
│ │ ├── green.png
│ │ └── red.png
├── basic
│ ├── demo-recording.json
│ └── index.html
├── body_merger
│ └── index.html
├── build.js
├── collision_response
│ ├── demo-recording.json
│ └── index.html
├── index.html
└── main.js
├── index.js
├── machinima_tests
├── __init.test.js
├── assets
│ ├── blue.png
│ ├── green.png
│ └── red.png
├── karma.conf.js
├── main.js
├── recordings
│ └── physics-extras.json
├── scenes
│ ├── index.html
│ ├── scene.html
│ └── static.html
└── tests
│ └── component.test.js
├── package.json
├── readme_files
└── physics.gif
├── src
├── body-merger.js
├── physics-collider.js
├── physics-collision-filter.js
└── physics-sleepy.js
├── tests
├── __init.test.js
├── components
│ ├── physics-collider.test.js
│ ├── physics-collision-filter.test.js
│ └── physics-sleepy.test.js
├── helpers.js
├── karma.conf.js
└── testDependencies.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["env", {
3 | "targets": {
4 | "browsers": [
5 | "last 1 Samsung versions", "last 2 Firefox versions",
6 | "last 2 Chrome versions", "last 2 FirefoxAndroid versions",
7 | "last 2 iOS versions"
8 | ]
9 | }
10 | }]],
11 | "env": {
12 | "production": {
13 | "presets": ["minify"]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | firefox/
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | addons:
3 | firefox: 'latest'
4 | node_js:
5 | - '6.9.2'
6 |
7 | install:
8 | - npm install
9 | - ./node_modules/.bin/mozilla-download ./firefox/ --product firefox --branch mozilla-central
10 | - export FIREFOX_NIGHTLY_BIN="./firefox/firefox/firefox-bin"
11 |
12 | before_script:
13 | - export DISPLAY=:99.0
14 | - sh -e /etc/init.d/xvfb start
15 |
16 | script:
17 | - $CI_ACTION
18 |
19 | env:
20 | global:
21 | - TEST_SUITE=unit
22 | - CXX=g++-4.8
23 | matrix:
24 | - CI_ACTION="npm run test:ci"
25 | - CI_ACTION="npm run build"
26 | - CI_ACTION="npm run lint"
27 |
28 | cache:
29 | directories:
30 | - node_modules
31 |
32 | addons:
33 | apt:
34 | sources:
35 | - ubuntu-toolchain-r-test
36 | packages:
37 | - g++-4.8
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Will Murphy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A-Frame Physics Extras
2 | [](https://www.npmjs.com/package/aframe-physics-extras)
3 | [](https://www.npmjs.com/package/aframe-physics-extras)
4 | [](https://travis-ci.org/wmurphyrd/aframe-physics-extras)
5 |
6 | Add-on components for the
7 | [`aframe-physics-system`](https://github.com/donmccurdy/aframe-physics-system)
8 | to add additional collision detection and behavior control options.
9 |
10 | 
11 |
12 | * [physics-collider](#physics-collider)
13 | * [collision-filter](#collision-filter)
14 | * [sleepy](#sleepy)
15 |
16 | ## physics-collider
17 |
18 | A collision detection component powered by the physics simulation with low
19 | overhead and precise collision zones. This is intended to be placed on
20 | tracked controller entities to monitor collisions and report them to
21 | a gesture interpretation component such as
22 | [super-hands](https://github.com/wmurphyrd/aframe-super-hands-component).
23 |
24 | ### API
25 |
26 | | Property | Description | Default Value |
27 | | -------- | ----------- | ------------- |
28 | | ignoreSleep | Wake sleeping bodies on collision? | `true` |
29 |
30 | `physics-collider` can also report collisions with static bodies when
31 | `ignoreSleep` is `true`. This can be useful to create collision detection zones
32 | for interactivity with things other than dynamic bodies.
33 |
34 | ### Events
35 |
36 | | Type | Description | Detail object |
37 | | --- | --- | --- |
38 | | collisions | Emitted each tick if there are changes to the collision list | `els`: array of new collisions. `cleardEls`: array of collisions which have ended. |
39 |
40 | ## collision-filter
41 |
42 | Control which physics bodies interact with each other or ignore each other.
43 | This can improve physics system performance by skipping unnecessary
44 | collision checks. It also controls which entities can be interacted with
45 | via `physics-collider`
46 |
47 | ### API
48 |
49 | | Property | Description | Default Value |
50 | | -------- | ----------- | ------------- |
51 | | group | Collision group this entity belongs to | `'default'` |
52 | | collidesWith | Array of collision groups this entity will interact with | `'default'` |
53 | | collisionForces | Should other bodies react to collisions with this body? | `true` |
54 |
55 | `collisionForces` controls whether collisions with this body generate any
56 | forces. Setting this to `false` allows for collisions to be registered and
57 | tracked without causing any corresponding movement. This is useful for
58 | your controller entities with `physics-collider` because it is difficult
59 | to pick things up if they are constantly bumped away when your hand gets close.
60 | This can be toggles through events with a controller button press
61 | if you want to be able to bump other
62 | objects sometimes and reach inside to pick them up other times.
63 | [There is an example of this on the examples page](#examples).
64 |
65 | Turning off `collisionForces` can also be useful
66 | for setting static bodies as collision zones to detect the presence
67 | of other entities without disturbing them.
68 |
69 | ## sleepy
70 |
71 | Make entities settle down and be still after physics collisions. Very useful
72 | for zero-gravity user interfaces to keep entities from floating away. Also
73 | can help performance as sleeping bodies are handled efficiently by the physics
74 | simulation.
75 |
76 | ### API
77 |
78 | | Property | Description | Default Value |
79 | | -------- | ----------- | ------------- |
80 | | allowSleep | Enable sleep for this body | `true` |
81 | | speedLimit | Maximum velocity for sleep to initiate | `0.25` |
82 | | delay | Time interval to check for sleep initiation (seconds) | `0.25` |
83 | | linearDamping | Deceleration of liner forces on the entity (0 to 1) | `0.99` |
84 | | angularDamping | Deceleration of angular forces on the entity (0 to 1) | `0.99` |
85 | | holdState | Entity state in which sleep is suspended | `'grabbed'` |
86 |
87 | Adding `sleepy` to any body will activate sleep for the entire physics system
88 | and will affect other bodies because the cannon defaults for all bodies
89 | are to allow sleep with a speed limit of 0.1 and delay of 1 second. You can
90 | add `sleepy="allowSleep: false; linearDamping: 0.01; angularDamping: 0.01"`
91 | to restore default behavior to an entity if needed.
92 | Sleeping bodies will ignore static bodies
93 | (hence why `physics-collider` has an `ignoreSleep` setting) until they
94 | are woken by a dynamic or kinematic body. Sleep will break constraints,
95 | so the `holdState` property allows you to suspend sleep during interactions
96 | such as grabbing/carrying the entity.
97 |
98 | ## Examples
99 |
100 | [View the examples page](http://wmurphyrd.github.io/aframe-physics-extras/examples/) to see `aframe-physics-extras` in action.
101 |
102 | ## Installation
103 |
104 | ### Browser
105 |
106 | Install and use by directly including the [browser files](dist):
107 |
108 | [](https://glitch.com/edit/#!/remix/blue-animal)
109 |
110 | ```html
111 |
112 |
113 |
114 | My A-Frame Scene
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
134 |
135 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
145 |
146 |
149 |
151 |
152 |
153 |
156 |
157 |
158 |
159 | ```
160 |
161 | ### npm
162 |
163 | Install via npm:
164 |
165 | ```bash
166 | npm install
167 | ```
168 |
169 | Then require and use.
170 |
171 | ```js
172 | require('aframe');
173 | require('aframe-physics-system')
174 | require('aframe-physics-extras');
175 | ```
176 |
--------------------------------------------------------------------------------
/dist/aframe-physics-extras.js:
--------------------------------------------------------------------------------
1 | (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 {
23 | if (evt.target === this.el) {
24 | this.el.removeEventListener('body-loaded', doMerge);
25 | this.merge();
26 | }
27 | };
28 | if (this.el.body) {
29 | this.merge();
30 | } else {
31 | this.el.addEventListener('body-loaded', doMerge);
32 | }
33 | },
34 | merge: function () {
35 | const body = this.el.body;
36 | const tmpMat = new THREE.Matrix4();
37 | const tmpQuat = new THREE.Quaternion();
38 | const tmpPos = new THREE.Vector3();
39 | const tmpScale = new THREE.Vector3(1, 1, 1); // todo: apply worldScale
40 | const offset = new CANNON.Vec3();
41 | const orientation = new CANNON.Quaternion();
42 | for (let child of this.el.childNodes) {
43 | if (!child.body || !child.getAttribute(this.data)) {
44 | continue;
45 | }
46 | child.object3D.updateMatrix();
47 | while (child.body.shapes.length) {
48 | tmpPos.copy(child.body.shapeOffsets.pop());
49 | tmpQuat.copy(child.body.shapeOrientations.pop());
50 | tmpMat.compose(tmpPos, tmpQuat, tmpScale);
51 | tmpMat.multiply(child.object3D.matrix);
52 | tmpMat.decompose(tmpPos, tmpQuat, tmpScale);
53 | offset.copy(tmpPos);
54 | orientation.copy(tmpQuat);
55 | body.addShape(child.body.shapes.pop(), offset, orientation);
56 | }
57 | child.removeAttribute(this.data);
58 | }
59 | }
60 | });
61 |
62 | },{}],3:[function(require,module,exports){
63 | 'use strict';
64 |
65 | /* global AFRAME */
66 | AFRAME.registerComponent('physics-collider', {
67 | schema: {
68 | ignoreSleep: { default: true }
69 | },
70 | init: function () {
71 | this.collisions = new Set();
72 | this.currentCollisions = new Set();
73 | this.newCollisions = [];
74 | this.clearedCollisions = [];
75 | this.collisionEventDetails = {
76 | els: this.newCollisions,
77 | clearedEls: this.clearedCollisions
78 | };
79 | },
80 | update: function () {
81 | if (this.el.body) {
82 | this.updateBody();
83 | } else {
84 | this.el.addEventListener('body-loaded', this.updateBody.bind(this), { once: true });
85 | }
86 | },
87 | tick: function () {
88 | const uppperMask = 0xFFFF0000;
89 | const lowerMask = 0x0000FFFF;
90 | return function () {
91 | if (!this.el.body) return;
92 | const currentCollisions = this.currentCollisions;
93 | const thisBodyId = this.el.body.id;
94 | const worldCollisions = this.el.body.world.bodyOverlapKeeper.current;
95 | const worldBodyMap = this.el.body.world.idToBodyMap;
96 | const collisions = this.collisions;
97 | const newCollisions = this.newCollisions;
98 | const clearedCollisions = this.clearedCollisions;
99 | let i = 0;
100 | let upperId = (worldCollisions[i] & uppperMask) >> 16;
101 | let target;
102 | newCollisions.length = clearedCollisions.length = 0;
103 | currentCollisions.clear();
104 | while (i < worldCollisions.length && upperId < thisBodyId) {
105 | if (worldBodyMap[upperId]) {
106 | target = worldBodyMap[upperId].el;
107 | if ((worldCollisions[i] & lowerMask) === thisBodyId) {
108 | currentCollisions.add(target);
109 | if (!collisions.has(target)) {
110 | newCollisions.push(target);
111 | }
112 | }
113 | }
114 | upperId = (worldCollisions[++i] & uppperMask) >> 16;
115 | }
116 | while (i < worldCollisions.length && upperId === thisBodyId) {
117 | if (worldBodyMap[worldCollisions[i] & lowerMask]) {
118 | target = worldBodyMap[worldCollisions[i] & lowerMask].el;
119 | currentCollisions.add(target);
120 | if (!collisions.has(target)) {
121 | newCollisions.push(target);
122 | }
123 | }
124 | upperId = (worldCollisions[++i] & uppperMask) >> 16;
125 | }
126 |
127 | for (let col of collisions) {
128 | if (!currentCollisions.has(col)) {
129 | clearedCollisions.push(col);
130 | collisions.delete(col);
131 | }
132 | }
133 | for (let col of newCollisions) {
134 | collisions.add(col);
135 | }
136 | if (newCollisions.length || clearedCollisions.length) {
137 | this.el.emit('collisions', this.collisionEventDetails);
138 | }
139 | };
140 | }(),
141 | remove: function () {
142 | if (this.originalSleepConfig) {
143 | AFRAME.utils.extend(this.el.body, this.originalSleepConfig);
144 | }
145 | },
146 | updateBody: function (evt) {
147 | // ignore bubbled 'body-loaded' events
148 | if (evt !== undefined && evt.target !== this.el) {
149 | return;
150 | }
151 | if (this.data.ignoreSleep) {
152 | // ensure sleep doesn't disable collision detection
153 | this.el.body.allowSleep = false;
154 | /* naiveBroadphase ignores collisions between sleeping & static bodies */
155 | this.el.body.type = window.CANNON.Body.KINEMATIC;
156 | // Kinematics must have velocity >= their sleep limit to wake others
157 | this.el.body.sleepSpeedLimit = 0;
158 | } else if (this.originalSleepConfig === undefined) {
159 | this.originalSleepConfig = {
160 | allowSleep: this.el.body.allowSleep,
161 | sleepSpeedLimit: this.el.body.sleepSpeedLimit,
162 | type: this.el.body.type
163 | };
164 | } else {
165 | // restore original settings
166 | AFRAME.utils.extend(this.el.body, this.originalSleepConfig);
167 | }
168 | }
169 | });
170 |
171 | },{}],4:[function(require,module,exports){
172 | 'use strict';
173 |
174 | /* global AFRAME */
175 | AFRAME.registerComponent('collision-filter', {
176 | schema: {
177 | group: { default: 'default' },
178 | collidesWith: { default: ['default'] },
179 | collisionForces: { default: true }
180 | },
181 | init: function () {
182 | this.updateBodyBound = this.updateBody.bind(this);
183 | this.system.registerMe(this);
184 | this.el.addEventListener('body-loaded', this.updateBodyBound);
185 | },
186 | update: function () {
187 | // register any new groups
188 | this.system.registerMe(this);
189 | if (this.el.body) {
190 | this.updateBody();
191 | }
192 | },
193 | remove: function () {
194 | this.el.removeEventListener('body-loaded', this.updateBodyBound);
195 | },
196 | updateBody: function (evt) {
197 | // ignore bubbled 'body-loaded' events
198 | if (evt !== undefined && evt.target !== this.el) {
199 | return;
200 | }
201 | this.el.body.collisionFilterMask = this.system.getFilterCode(this.data.collidesWith);
202 | this.el.body.collisionFilterGroup = this.system.getFilterCode(this.data.group);
203 | this.el.body.collisionResponse = this.data.collisionForces;
204 | }
205 | });
206 |
207 | AFRAME.registerSystem('collision-filter', {
208 | schema: {
209 | collisionGroups: { default: ['default'] }
210 | },
211 | dependencies: ['physics'],
212 | init: function () {
213 | this.maxGroups = Math.log2(Number.MAX_SAFE_INTEGER);
214 | },
215 | registerMe: function (comp) {
216 | // add any unknown groups to the master list
217 | const newGroups = [comp.data.group, ...comp.data.collidesWith].filter(group => this.data.collisionGroups.indexOf(group) === -1);
218 | this.data.collisionGroups.push(...newGroups);
219 | if (this.data.collisionGroups.length > this.maxGroups) {
220 | throw new Error('Too many collision groups');
221 | }
222 | },
223 | getFilterCode: function (elGroups) {
224 | let code = 0;
225 | if (!Array.isArray(elGroups)) {
226 | elGroups = [elGroups];
227 | }
228 | // each group corresponds to a bit which is turned on when matched
229 | // floor negates any unmatched groups (2^-1 = 0.5)
230 | elGroups.forEach(group => {
231 | code += Math.floor(Math.pow(2, this.data.collisionGroups.indexOf(group)));
232 | });
233 | return code;
234 | }
235 | });
236 |
237 | },{}],5:[function(require,module,exports){
238 | 'use strict';
239 |
240 | // Make dynamic bodies idle when not grabbed
241 | /* global AFRAME */
242 | AFRAME.registerComponent('sleepy', {
243 | schema: {
244 | allowSleep: { default: true },
245 | speedLimit: { default: 0.25, type: 'number' },
246 | delay: { default: 0.25, type: 'number' },
247 | linearDamping: { default: 0.99, type: 'number' },
248 | angularDamping: { default: 0.99, type: 'number' },
249 | holdState: { default: 'grabbed' }
250 | },
251 | init: function () {
252 | this.updateBodyBound = this.updateBody.bind(this);
253 | this.holdStateBound = this.holdState.bind(this);
254 | this.resumeStateBound = this.resumeState.bind(this);
255 |
256 | this.el.addEventListener('body-loaded', this.updateBodyBound);
257 | },
258 | update: function () {
259 | if (this.el.body) {
260 | this.updateBody();
261 | }
262 | },
263 | remove: function () {
264 | this.el.removeEventListener('body-loaded', this.updateBodyBound);
265 | this.el.removeEventListener('stateadded', this.holdStateBound);
266 | this.el.removeEventListener('stateremoved', this.resumeStateBound);
267 | },
268 | updateBody: function (evt) {
269 | // ignore bubbled 'body-loaded' events
270 | if (evt !== undefined && evt.target !== this.el) {
271 | return;
272 | }
273 | if (this.data.allowSleep) {
274 | // only "local" driver compatable
275 | try {
276 | this.el.body.world.allowSleep = true;
277 | } catch (err) {
278 | console.error('Unable to activate sleep in physics.' + '`sleepy` requires "local" physics driver');
279 | }
280 | }
281 | this.el.body.allowSleep = this.data.allowSleep;
282 | this.el.body.sleepSpeedLimit = this.data.speedLimit;
283 | this.el.body.sleepTimeLimit = this.data.delay;
284 | this.el.body.linearDamping = this.data.linearDamping;
285 | this.el.body.angularDamping = this.data.angularDamping;
286 | if (this.data.allowSleep) {
287 | this.el.addEventListener('stateadded', this.holdStateBound);
288 | this.el.addEventListener('stateremoved', this.resumeStateBound);
289 | } else {
290 | this.el.removeEventListener('stateadded', this.holdStateBound);
291 | this.el.removeEventListener('stateremoved', this.resumeStateBound);
292 | }
293 | },
294 | // disble the sleeping during interactions because sleep will break constraints
295 | holdState: function (evt) {
296 | let state = this.data.holdState;
297 | // api change in A-Frame v0.8.0
298 | if (evt.detail === state || evt.detail.state === state) {
299 | this.el.body.allowSleep = false;
300 | }
301 | },
302 | resumeState: function (evt) {
303 | let state = this.data.holdState;
304 | if (evt.detail === state || evt.detail.state === state) {
305 | this.el.body.allowSleep = this.data.allowSleep;
306 | }
307 | }
308 |
309 | });
310 |
311 | },{}]},{},[1]);
312 |
--------------------------------------------------------------------------------
/dist/aframe-physics-extras.min.js:
--------------------------------------------------------------------------------
1 | (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{b.target===this.el&&(this.el.removeEventListener('body-loaded',a),this.merge())};this.el.body?this.merge():this.el.addEventListener('body-loaded',a)},merge:function(){const a=this.el.body,b=new THREE.Matrix4,c=new THREE.Quaternion,d=new THREE.Vector3,e=new THREE.Vector3(1,1,1),f=new CANNON.Vec3,g=new CANNON.Quaternion;for(let h of this.el.childNodes)if(h.body&&h.getAttribute(this.data)){for(h.object3D.updateMatrix();h.body.shapes.length;)d.copy(h.body.shapeOffsets.pop()),c.copy(h.body.shapeOrientations.pop()),b.compose(d,c,e),b.multiply(h.object3D.matrix),b.decompose(d,c,e),f.copy(d),g.copy(c),a.addShape(h.body.shapes.pop(),f,g);h.removeAttribute(this.data)}}});
6 |
7 | },{}],3:[function(require,module,exports){
8 | 'use strict';AFRAME.registerComponent('physics-collider',{schema:{ignoreSleep:{default:!0}},init:function(){this.collisions=new Set,this.currentCollisions=new Set,this.newCollisions=[],this.clearedCollisions=[],this.collisionEventDetails={els:this.newCollisions,clearedEls:this.clearedCollisions}},update:function(){this.el.body?this.updateBody():this.el.addEventListener('body-loaded',this.updateBody.bind(this),{once:!0})},tick:function(){const a=4294901760,b=65535;return function(){if(!this.el.body)return;const c=this.currentCollisions,d=this.el.body.id,e=this.el.body.world.bodyOverlapKeeper.current,f=this.el.body.world.idToBodyMap,g=this.collisions,h=this.newCollisions,j=this.clearedCollisions;let k,l=0,i=(e[l]&a)>>16;for(h.length=j.length=0,c.clear();l>16;for(;l>16;for(let a of g)c.has(a)||(j.push(a),g.delete(a));for(let a of h)g.add(a);(h.length||j.length)&&this.el.emit('collisions',this.collisionEventDetails)}}(),remove:function(){this.originalSleepConfig&&AFRAME.utils.extend(this.el.body,this.originalSleepConfig)},updateBody:function(a){void 0!==a&&a.target!==this.el||(this.data.ignoreSleep?(this.el.body.allowSleep=!1,this.el.body.type=window.CANNON.Body.KINEMATIC,this.el.body.sleepSpeedLimit=0):this.originalSleepConfig===void 0?this.originalSleepConfig={allowSleep:this.el.body.allowSleep,sleepSpeedLimit:this.el.body.sleepSpeedLimit,type:this.el.body.type}:AFRAME.utils.extend(this.el.body,this.originalSleepConfig))}});
9 |
10 | },{}],4:[function(require,module,exports){
11 | 'use strict';AFRAME.registerComponent('collision-filter',{schema:{group:{default:'default'},collidesWith:{default:['default']},collisionForces:{default:!0}},init:function(){this.updateBodyBound=this.updateBody.bind(this),this.system.registerMe(this),this.el.addEventListener('body-loaded',this.updateBodyBound)},update:function(){this.system.registerMe(this),this.el.body&&this.updateBody()},remove:function(){this.el.removeEventListener('body-loaded',this.updateBodyBound)},updateBody:function(a){void 0!==a&&a.target!==this.el||(this.el.body.collisionFilterMask=this.system.getFilterCode(this.data.collidesWith),this.el.body.collisionFilterGroup=this.system.getFilterCode(this.data.group),this.el.body.collisionResponse=this.data.collisionForces)}}),AFRAME.registerSystem('collision-filter',{schema:{collisionGroups:{default:['default']}},dependencies:['physics'],init:function(){this.maxGroups=Math.log2(Number.MAX_SAFE_INTEGER)},registerMe:function(a){const b=[a.data.group,...a.data.collidesWith].filter((a)=>-1===this.data.collisionGroups.indexOf(a));if(this.data.collisionGroups.push(...b),this.data.collisionGroups.length>this.maxGroups)throw new Error('Too many collision groups')},getFilterCode:function(a){let b=0;return Array.isArray(a)||(a=[a]),a.forEach((a)=>{b+=Math.floor(Math.pow(2,this.data.collisionGroups.indexOf(a)))}),b}});
12 |
13 | },{}],5:[function(require,module,exports){
14 | 'use strict';AFRAME.registerComponent('sleepy',{schema:{allowSleep:{default:!0},speedLimit:{default:0.25,type:'number'},delay:{default:0.25,type:'number'},linearDamping:{default:0.99,type:'number'},angularDamping:{default:0.99,type:'number'},holdState:{default:'grabbed'}},init:function(){this.updateBodyBound=this.updateBody.bind(this),this.holdStateBound=this.holdState.bind(this),this.resumeStateBound=this.resumeState.bind(this),this.el.addEventListener('body-loaded',this.updateBodyBound)},update:function(){this.el.body&&this.updateBody()},remove:function(){this.el.removeEventListener('body-loaded',this.updateBodyBound),this.el.removeEventListener('stateadded',this.holdStateBound),this.el.removeEventListener('stateremoved',this.resumeStateBound)},updateBody:function(a){if(void 0===a||a.target===this.el){if(this.data.allowSleep)try{this.el.body.world.allowSleep=!0}catch(a){console.error('Unable to activate sleep in physics.`sleepy` requires "local" physics driver')}this.el.body.allowSleep=this.data.allowSleep,this.el.body.sleepSpeedLimit=this.data.speedLimit,this.el.body.sleepTimeLimit=this.data.delay,this.el.body.linearDamping=this.data.linearDamping,this.el.body.angularDamping=this.data.angularDamping,this.data.allowSleep?(this.el.addEventListener('stateadded',this.holdStateBound),this.el.addEventListener('stateremoved',this.resumeStateBound)):(this.el.removeEventListener('stateadded',this.holdStateBound),this.el.removeEventListener('stateremoved',this.resumeStateBound))}},holdState:function(a){let b=this.data.holdState;(a.detail===b||a.detail.state===b)&&(this.el.body.allowSleep=!1)},resumeState:function(a){let b=this.data.holdState;(a.detail===b||a.detail.state===b)&&(this.el.body.allowSleep=this.data.allowSleep)}});
15 |
16 | },{}]},{},[1]);
17 |
--------------------------------------------------------------------------------
/examples/assets/basic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/examples/assets/basic.png
--------------------------------------------------------------------------------
/examples/assets/collisionresponse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/examples/assets/collisionresponse.png
--------------------------------------------------------------------------------
/examples/assets/examples.css:
--------------------------------------------------------------------------------
1 | #replayer-button {
2 | position: fixed;
3 | bottom: 10px;
4 | left: 50%;
5 | transform: translate(-50%, 0);
6 | padding: 5px;
7 | z-index: 100;
8 | }
9 |
--------------------------------------------------------------------------------
/examples/assets/textures/blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/examples/assets/textures/blue.png
--------------------------------------------------------------------------------
/examples/assets/textures/green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/examples/assets/textures/green.png
--------------------------------------------------------------------------------
/examples/assets/textures/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/examples/assets/textures/red.png
--------------------------------------------------------------------------------
/examples/basic/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A-Frame Physics Extras Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Don't have a Vive or Rift handy? Click here for a preview.
16 |
17 |
18 |
19 |
20 |
21 |
22 |
30 |
31 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
44 |
45 |
47 |
48 |
49 |
51 |
52 |
54 |
55 |
56 |
59 |
60 |
63 |
64 |
65 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/examples/body_merger/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A-Frame Physics Extras Example
5 |
6 |
7 |
8 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
26 |
27 |
28 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/examples/build.js:
--------------------------------------------------------------------------------
1 | (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 {
13 | let c = document.querySelector('[camera]');
14 | window.setTimeout(function () {
15 | c.setAttribute('position', '0 1.6 2');
16 | c.setAttribute('rotation', '0 0 0');
17 | });
18 | });
19 | s.setAttribute('avatar-replayer', {
20 | src: './demo-recording.json',
21 | spectatorMode: spectate === undefined ? true : spectate,
22 | spectatorPosition: { x: 0, y: 1.6, z: 2 }
23 | });
24 | };
25 |
26 | },{"../index.js":2,"aframe-motion-capture-components":9}],2:[function(require,module,exports){
27 | 'use strict';
28 |
29 | /* global AFRAME */
30 |
31 | if (typeof AFRAME === 'undefined') {
32 | throw new Error('Component attempted to register before AFRAME was available.');
33 | }
34 |
35 | require('./src/physics-collider.js');
36 | require('./src/physics-collision-filter.js');
37 | require('./src/physics-sleepy.js');
38 | require('./src/body-merger.js');
39 |
40 | },{"./src/body-merger.js":12,"./src/physics-collider.js":13,"./src/physics-collision-filter.js":14,"./src/physics-sleepy.js":15}],3:[function(require,module,exports){
41 | /* global THREE, AFRAME */
42 | var constants = require('../constants');
43 | var log = AFRAME.utils.debug('aframe-motion-capture:avatar-recorder:info');
44 | var warn = AFRAME.utils.debug('aframe-motion-capture:avatar-recorder:warn');
45 |
46 | /**
47 | * Wrapper around individual motion-capture-recorder components for recording camera and
48 | * controllers together.
49 | */
50 | AFRAME.registerComponent('avatar-recorder', {
51 | schema: {
52 | autoPlay: {default: false},
53 | autoRecord: {default: false},
54 | cameraOverride: {type: 'selector'},
55 | localStorage: {default: true},
56 | recordingName: {default: constants.DEFAULT_RECORDING_NAME},
57 | loop: {default: true}
58 | },
59 |
60 | init: function () {
61 | this.cameraEl = null;
62 | this.isRecording = false;
63 | this.trackedControllerEls = {};
64 | this.recordingData = null;
65 |
66 | this.onKeyDown = AFRAME.utils.bind(this.onKeyDown, this);
67 | this.tick = AFRAME.utils.throttle(this.throttledTick, 100, this);
68 | },
69 |
70 | /**
71 | * Poll for tracked controllers.
72 | */
73 | throttledTick: function () {
74 | var self = this;
75 | var trackedControllerEls = this.el.querySelectorAll('[tracked-controls]');
76 | this.trackedControllerEls = {};
77 | trackedControllerEls.forEach(function setupController (trackedControllerEl) {
78 | if (!trackedControllerEl.id) {
79 | warn('Found a tracked controller entity without an ID. ' +
80 | 'Provide an ID or this controller will not be recorded');
81 | return;
82 | }
83 | trackedControllerEl.setAttribute('motion-capture-recorder', {
84 | autoRecord: false,
85 | visibleStroke: false
86 | });
87 | self.trackedControllerEls[trackedControllerEl.id] = trackedControllerEl;
88 | if (self.isRecording) {
89 | trackedControllerEl.components['motion-capture-recorder'].startRecording();
90 | }
91 | });
92 | },
93 |
94 | play: function () {
95 | window.addEventListener('keydown', this.onKeyDown);
96 | },
97 |
98 | pause: function () {
99 | window.removeEventListener('keydown', this.onKeyDown);
100 | },
101 |
102 | /**
103 | * Keyboard shortcuts.
104 | */
105 | onKeyDown: function (evt) {
106 | var key = evt.keyCode;
107 | var KEYS = {space: 32};
108 | switch (key) {
109 | // : Toggle recording.
110 | case KEYS.space: {
111 | this.toggleRecording();
112 | break;
113 | }
114 | }
115 | },
116 |
117 | /**
118 | * Start or stop recording.
119 | */
120 | toggleRecording: function () {
121 | if (this.isRecording) {
122 | this.stopRecording();
123 | } else {
124 | this.startRecording();
125 | }
126 | },
127 |
128 | /**
129 | * Set motion capture recorder on the camera once the camera is ready.
130 | */
131 | setupCamera: function (doneCb) {
132 | var el = this.el;
133 | var self = this;
134 |
135 | if (this.data.cameraOverride) {
136 | prepareCamera(this.data.cameraOverride);
137 | return;
138 | }
139 |
140 | // Grab camera.
141 | if (el.camera && el.camera.el) {
142 | prepareCamera(el.camera.el);
143 | return;
144 | }
145 |
146 | el.addEventListener('camera-set-active', function setup (evt) {
147 | prepareCamera(evt.detail.cameraEl);
148 | el.removeEventListener('camera-set-active', setup);
149 | });
150 |
151 | function prepareCamera (cameraEl) {
152 | if (self.cameraEl) {
153 | self.cameraEl.removeAttribute('motion-capture-recorder');
154 | }
155 | self.cameraEl = cameraEl;
156 | cameraEl.setAttribute('motion-capture-recorder', {
157 | autoRecord: false,
158 | visibleStroke: false
159 | });
160 | doneCb(cameraEl)
161 | }
162 | },
163 |
164 | /**
165 | * Start recording camera and tracked controls.
166 | */
167 | startRecording: function () {
168 | var trackedControllerEls = this.trackedControllerEls;
169 | var self = this;
170 |
171 | if (this.isRecording) { return; }
172 |
173 | log('Starting recording!');
174 |
175 | if (this.el.components['avatar-replayer']) {
176 | this.el.components['avatar-replayer'].stopReplaying();
177 | }
178 |
179 | // Get camera.
180 | this.setupCamera(function cameraSetUp () {
181 | self.isRecording = true;
182 | // Record camera.
183 | self.cameraEl.components['motion-capture-recorder'].startRecording();
184 | // Record tracked controls.
185 | Object.keys(trackedControllerEls).forEach(function startRecordingController (id) {
186 | trackedControllerEls[id].components['motion-capture-recorder'].startRecording();
187 | });
188 | });
189 | },
190 |
191 | /**
192 | * Tell camera and tracked controls motion-capture-recorder components to stop recording.
193 | * Store recording and replay if autoPlay is on.
194 | */
195 | stopRecording: function () {
196 | var trackedControllerEls = this.trackedControllerEls;
197 |
198 | if (!this.isRecording) { return; }
199 |
200 | log('Stopped recording.');
201 | this.isRecording = false;
202 | this.cameraEl.components['motion-capture-recorder'].stopRecording();
203 | Object.keys(trackedControllerEls).forEach(function (id) {
204 | trackedControllerEls[id].components['motion-capture-recorder'].stopRecording();
205 | });
206 | this.recordingData = this.getJSONData();
207 | this.storeRecording(this.recordingData);
208 |
209 | if (this.data.autoPlay) {
210 | this.replayRecording();
211 | }
212 | },
213 |
214 | /**
215 | * Gather the JSON data from the camera and tracked controls motion-capture-recorder
216 | * components. Combine them together, keyed by the (active) `camera` and by the
217 | * tracked controller IDs.
218 | */
219 | getJSONData: function () {
220 | var data = {};
221 | var trackedControllerEls = this.trackedControllerEls;
222 |
223 | if (this.isRecording) { return; }
224 |
225 | // Camera.
226 | data.camera = this.cameraEl.components['motion-capture-recorder'].getJSONData();
227 |
228 | // Tracked controls.
229 | Object.keys(trackedControllerEls).forEach(function getControllerData (id) {
230 | data[id] = trackedControllerEls[id].components['motion-capture-recorder'].getJSONData();
231 | });
232 |
233 | return data;
234 | },
235 |
236 | /**
237 | * Store recording in IndexedDB using recordingdb system.
238 | */
239 | storeRecording: function (recordingData) {
240 | var data = this.data;
241 | if (!data.localStorage) { return; }
242 | log('Recording stored in localStorage.');
243 | this.el.systems.recordingdb.addRecording(data.recordingName, recordingData);
244 | }
245 | });
246 |
247 | },{"../constants":8}],4:[function(require,module,exports){
248 | /* global THREE, AFRAME */
249 | var constants = require('../constants');
250 |
251 | var bind = AFRAME.utils.bind;
252 | var error = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:error');
253 | var log = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:info');
254 | var warn = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:warn');
255 |
256 | var fileLoader = new THREE.FileLoader();
257 |
258 | AFRAME.registerComponent('avatar-replayer', {
259 | schema: {
260 | autoPlay: {default: true},
261 | cameraOverride: {type: 'selector'},
262 | loop: {default: false},
263 | recordingName: {default: constants.DEFAULT_RECORDING_NAME},
264 | spectatorMode: {default: false},
265 | spectatorPosition: {default: {x: 0, y: 1.6, z: 2}, type: 'vec3'},
266 | src: {default: ''}
267 | },
268 |
269 | init: function () {
270 | var sceneEl = this.el;
271 |
272 | // Bind methods.
273 | this.onKeyDown = bind(this.onKeyDown, this);
274 |
275 | // Prepare camera.
276 | this.setupCamera = bind(this.setupCamera, this);
277 | if (sceneEl.camera) {
278 | this.setupCamera();
279 | } else {
280 | sceneEl.addEventListener('camera-set-active', this.setupCamera);
281 | }
282 |
283 | if (this.data.autoPlay) {
284 | this.replayRecordingFromSource();
285 | }
286 | },
287 |
288 | update: function (oldData) {
289 | var data = this.data;
290 | var spectatorModeUrlParam;
291 |
292 | spectatorModeUrlParam =
293 | window.location.search.indexOf('spectatormode') !== -1 ||
294 | window.location.search.indexOf('spectatorMode') !== -1;
295 |
296 | // Handle toggling spectator mode. Don't run on initialization. Want to activate after
297 | // the player camera is initialized.
298 | if (oldData.spectatorMode !== data.spectatorMode ||
299 | spectatorModeUrlParam) {
300 | if (data.spectatorMode || spectatorModeUrlParam) {
301 | this.activateSpectatorCamera();
302 | } else if (oldData.spectatorMode === true) {
303 | this.deactivateSpectatorCamera();
304 | }
305 | }
306 |
307 | // Handle `src` changing.
308 | if (data.src && oldData.src !== data.src && data.autoPlay) {
309 | this.replayRecordingFromSource();
310 | }
311 | },
312 |
313 | play: function () {
314 | window.addEventListener('keydown', this.onKeyDown);
315 | },
316 |
317 | pause: function () {
318 | window.removeEventListener('keydown', this.onKeyDown);
319 | },
320 |
321 | remove: function () {
322 | this.stopReplaying();
323 | this.cameraEl.removeObject3D('replayerMesh');
324 | },
325 |
326 | /**
327 | * Grab a handle to the "original" camera.
328 | * Initialize spectator camera and dummy geometry for original camera.
329 | */
330 | setupCamera: function () {
331 | var data = this.data;
332 | var sceneEl = this.el;
333 |
334 | if (data.cameraOverride) {
335 | // Specify which camera is the original camera (e.g., used by Inspector).
336 | this.cameraEl = data.cameraOverride;
337 | } else {
338 | // Default camera.
339 | this.cameraEl = sceneEl.camera.el;
340 | // Make sure A-Frame doesn't automatically remove this camera.
341 | this.cameraEl.removeAttribute('data-aframe-default-camera');
342 | }
343 | this.cameraEl.setAttribute('data-aframe-avatar-replayer-camera', '');
344 |
345 | sceneEl.removeEventListener('camera-set-active', this.setupCamera);
346 |
347 | this.configureHeadGeometry();
348 |
349 | // Create spectator camera for either if we are in spectator mode or toggling to it.
350 | this.initSpectatorCamera();
351 | },
352 |
353 | /**
354 | * q: Toggle spectator camera.
355 | */
356 | onKeyDown: function (evt) {
357 | switch (evt.keyCode) {
358 | // q.
359 | case 81: {
360 | this.el.setAttribute('avatar-replayer', 'spectatorMode', !this.data.spectatorMode);
361 | break;
362 | }
363 | }
364 | },
365 |
366 | /**
367 | * Activate spectator camera, show replayer mesh.
368 | */
369 | activateSpectatorCamera: function () {
370 | var spectatorCameraEl = this.spectatorCameraEl;
371 |
372 | if (!spectatorCameraEl) {
373 | this.el.addEventListener('spectatorcameracreated',
374 | bind(this.activateSpectatorCamera, this));
375 | return;
376 | }
377 |
378 | if (!spectatorCameraEl.hasLoaded) {
379 | spectatorCameraEl.addEventListener('loaded', bind(this.activateSpectatorCamera, this));
380 | return;
381 | }
382 |
383 | log('Activating spectator camera');
384 | spectatorCameraEl.setAttribute('camera', 'active', true);
385 | this.cameraEl.getObject3D('replayerMesh').visible = true;
386 | },
387 |
388 | /**
389 | * Deactivate spectator camera (by setting original camera active), hide replayer mesh.
390 | */
391 | deactivateSpectatorCamera: function () {
392 | log('Deactivating spectator camera');
393 | this.cameraEl.setAttribute('camera', 'active', true);
394 | this.cameraEl.getObject3D('replayerMesh').visible = false;
395 | },
396 |
397 | /**
398 | * Create and activate spectator camera if in spectator mode.
399 | */
400 | initSpectatorCamera: function () {
401 | var data = this.data;
402 | var sceneEl = this.el;
403 | var spectatorCameraEl;
404 | var spectatorCameraRigEl;
405 |
406 | // Developer-defined spectator rig.
407 | if (this.el.querySelector('#spectatorCameraRig')) {
408 | this.spectatorCameraEl = sceneEl.querySelector('#spectatorCameraRig');
409 | return;
410 | }
411 |
412 | // Create spectator camera rig.
413 | spectatorCameraRigEl = sceneEl.querySelector('#spectatorCameraRig') ||
414 | document.createElement('a-entity');
415 | spectatorCameraRigEl.id = 'spectatorCameraRig';
416 | spectatorCameraRigEl.setAttribute('position', data.spectatorPosition);
417 | this.spectatorCameraRigEl = spectatorCameraRigEl;
418 |
419 | // Create spectator camera.
420 | spectatorCameraEl = sceneEl.querySelector('#spectatorCamera') ||
421 | document.createElement('a-entity');
422 | spectatorCameraEl.id = 'spectatorCamera';
423 | spectatorCameraEl.setAttribute('camera', {active: data.spectatorMode, userHeight: 0});
424 | spectatorCameraEl.setAttribute('look-controls', '');
425 | spectatorCameraEl.setAttribute('wasd-controls', {fly: true});
426 | this.spectatorCameraEl = spectatorCameraEl;
427 |
428 | // Append rig.
429 | spectatorCameraRigEl.appendChild(spectatorCameraEl);
430 | sceneEl.appendChild(spectatorCameraRigEl);
431 | sceneEl.emit('spectatorcameracreated');
432 | },
433 |
434 | /**
435 | * Check for recording sources and play.
436 | */
437 | replayRecordingFromSource: function () {
438 | var data = this.data;
439 | var recordingdb = this.el.systems.recordingdb;;
440 | var recordingNames;
441 | var src;
442 | var self = this;
443 |
444 | // Allow override to display replayer from query param.
445 | if (new URLSearchParams(window.location.search).get('avatar-replayer-disabled') !== null) {
446 | return;
447 | }
448 |
449 | recordingdb.getRecordingNames().then(function (recordingNames) {
450 | // See if recording defined in query parameter.
451 | var queryParamSrc = self.getSrcFromSearchParam();
452 |
453 | // 1. Try `avatar-recorder` query parameter as recording name from IndexedDB.
454 | if (recordingNames.indexOf(queryParamSrc) !== -1) {
455 | log('Replaying `' + queryParamSrc + '` from IndexedDB.');
456 | recordingdb.getRecording(queryParamSrc).then(bind(self.startReplaying, self));
457 | return;
458 | }
459 |
460 | // 2. Use `avatar-recorder` query parameter or `data.src` as URL.
461 | src = queryParamSrc || self.data.src;
462 | if (src) {
463 | if (self.data.src) {
464 | log('Replaying from component `src`', src);
465 | } else if (queryParamSrc) {
466 | log('Replaying from query parameter `recording`', src);
467 | }
468 | self.loadRecordingFromUrl(src, false, bind(self.startReplaying, self));
469 | return;
470 | }
471 |
472 | // 3. Use `data.recordingName` as recording name from IndexedDB.
473 | if (recordingNames.indexOf(self.data.recordingName) !== -1) {
474 | log('Replaying `' + self.data.recordingName + '` from IndexedDB.');
475 | recordingdb.getRecording(self.data.recordingName).then(bind(self.startReplaying, self));
476 | }
477 | });
478 | },
479 |
480 | /**
481 | * Defined for test stubbing.
482 | */
483 | getSrcFromSearchParam: function () {
484 | var search = new URLSearchParams(window.location.search);
485 | return search.get('recording') || search.get('avatar-recording');
486 | },
487 |
488 | /**
489 | * Set player on camera and controllers (marked by ID).
490 | *
491 | * @params {object} replayData - {
492 | * camera: {poses: [], events: []},
493 | * [c1ID]: {poses: [], events: []},
494 | * [c2ID]: {poses: [], events: []}
495 | * }
496 | */
497 | startReplaying: function (replayData) {
498 | var data = this.data;
499 | var self = this;
500 | var sceneEl = this.el;
501 |
502 | if (this.isReplaying) { return; }
503 |
504 | // Wait for camera.
505 | if (!this.el.camera) {
506 | this.el.addEventListener('camera-set-active', function waitForCamera () {
507 | self.startReplaying(replayData);
508 | self.el.removeEventListener('camera-set-active', waitForCamera);
509 | });
510 | return;
511 | }
512 |
513 | this.replayData = replayData;
514 | this.isReplaying = true;
515 |
516 | this.cameraEl.removeAttribute('motion-capture-replayer');
517 |
518 | Object.keys(replayData).forEach(function setReplayer (key) {
519 | var replayingEl;
520 |
521 | if (key === 'camera') {
522 | // Grab camera.
523 | replayingEl = self.cameraEl;
524 | } else {
525 | // Grab other entities.
526 | replayingEl = sceneEl.querySelector('#' + key);
527 | if (!replayingEl) {
528 | error('No element found with ID ' + key + '.');
529 | return;
530 | }
531 | }
532 |
533 | log('Setting motion-capture-replayer on ' + key + '.');
534 | replayingEl.setAttribute('motion-capture-replayer', {loop: data.loop});
535 | replayingEl.components['motion-capture-replayer'].startReplaying(replayData[key]);
536 | });
537 | },
538 |
539 | /**
540 | * Create head geometry for spectator mode.
541 | * Always created in case we want to toggle, but only visible during spectator mode.
542 | */
543 | configureHeadGeometry: function () {
544 | var cameraEl = this.cameraEl;
545 | var headMesh;
546 | var leftEyeMesh;
547 | var rightEyeMesh;
548 | var leftEyeBallMesh;
549 | var rightEyeBallMesh;
550 |
551 | if (cameraEl.getObject3D('mesh') || cameraEl.getObject3D('replayerMesh')) { return; }
552 |
553 | // Head.
554 | headMesh = new THREE.Mesh();
555 | headMesh.geometry = new THREE.BoxBufferGeometry(0.3, 0.3, 0.2);
556 | headMesh.material = new THREE.MeshStandardMaterial({color: 'pink'});
557 | headMesh.visible = this.data.spectatorMode;
558 |
559 | // Left eye.
560 | leftEyeMesh = new THREE.Mesh();
561 | leftEyeMesh.geometry = new THREE.SphereBufferGeometry(0.05);
562 | leftEyeMesh.material = new THREE.MeshBasicMaterial({color: 'white'});
563 | leftEyeMesh.position.x -= 0.1;
564 | leftEyeMesh.position.y += 0.1;
565 | leftEyeMesh.position.z -= 0.1;
566 | leftEyeBallMesh = new THREE.Mesh();
567 | leftEyeBallMesh.geometry = new THREE.SphereBufferGeometry(0.025);
568 | leftEyeBallMesh.material = new THREE.MeshBasicMaterial({color: 'black'});
569 | leftEyeBallMesh.position.z -= 0.04;
570 | leftEyeMesh.add(leftEyeBallMesh);
571 | headMesh.add(leftEyeMesh);
572 |
573 | // Right eye.
574 | rightEyeMesh = new THREE.Mesh();
575 | rightEyeMesh.geometry = new THREE.SphereBufferGeometry(0.05);
576 | rightEyeMesh.material = new THREE.MeshBasicMaterial({color: 'white'});
577 | rightEyeMesh.position.x += 0.1;
578 | rightEyeMesh.position.y += 0.1;
579 | rightEyeMesh.position.z -= 0.1;
580 | rightEyeBallMesh = new THREE.Mesh();
581 | rightEyeBallMesh.geometry = new THREE.SphereBufferGeometry(0.025);
582 | rightEyeBallMesh.material = new THREE.MeshBasicMaterial({color: 'black'});
583 | rightEyeBallMesh.position.z -= 0.04;
584 | rightEyeMesh.add(rightEyeBallMesh);
585 | headMesh.add(rightEyeMesh);
586 |
587 | cameraEl.setObject3D('replayerMesh', headMesh);
588 | },
589 |
590 | /**
591 | * Remove motion-capture-replayer components.
592 | */
593 | stopReplaying: function () {
594 | var self = this;
595 |
596 | if (!this.isReplaying || !this.replayData) { return; }
597 |
598 | this.isReplaying = false;
599 | Object.keys(this.replayData).forEach(function removeReplayer (key) {
600 | if (key === 'camera') {
601 | self.cameraEl.removeComponent('motion-capture-replayer');
602 | } else {
603 | el = document.querySelector('#' + key);
604 | if (!el) {
605 | warn('No element with id ' + key);
606 | return;
607 | }
608 | el.removeComponent('motion-capture-replayer');
609 | }
610 | });
611 | },
612 |
613 | /**
614 | * XHR for data.
615 | */
616 | loadRecordingFromUrl: function (url, binary, callback) {
617 | var data;
618 | var self = this;
619 | fileLoader.crossOrigin = 'anonymous';
620 | if (binary === true) {
621 | fileLoader.setResponseType('arraybuffer');
622 | }
623 | fileLoader.load(url, function (buffer) {
624 | if (binary === true) {
625 | data = self.loadStrokeBinary(buffer);
626 | } else {
627 | data = JSON.parse(buffer);
628 | }
629 | if (callback) { callback(data); }
630 | });
631 | }
632 | });
633 |
634 | },{"../constants":8}],5:[function(require,module,exports){
635 | /* global AFRAME, THREE */
636 |
637 | var EVENTS = {
638 | axismove: {id: 0, props: ['id', 'axis', 'changed']},
639 | buttonchanged: {id: 1, props: ['id', 'state']},
640 | buttondown: {id: 2, props: ['id', 'state']},
641 | buttonup: {id: 3, props: ['id', 'state']},
642 | touchstart: {id: 4, props: ['id', 'state']},
643 | touchend: {id: 5, props: ['id', 'state']}
644 | };
645 |
646 | var EVENTS_DECODE = {
647 | 0: 'axismove',
648 | 1: 'buttonchanged',
649 | 2: 'buttondown',
650 | 3: 'buttonup',
651 | 4: 'touchstart',
652 | 5: 'touchend'
653 | };
654 |
655 | AFRAME.registerComponent('motion-capture-recorder', {
656 | schema: {
657 | autoRecord: {default: false},
658 | enabled: {default: true},
659 | hand: {default: 'right'},
660 | recordingControls: {default: false},
661 | persistStroke: {default: false},
662 | visibleStroke: {default: true}
663 | },
664 |
665 | init: function () {
666 | this.drawing = false;
667 | this.recordedEvents = [];
668 | this.recordedPoses = [];
669 | this.addEventListeners();
670 | },
671 |
672 | addEventListeners: function () {
673 | var el = this.el;
674 | this.recordEvent = this.recordEvent.bind(this);
675 | el.addEventListener('axismove', this.recordEvent);
676 | el.addEventListener('buttonchanged', this.onTriggerChanged.bind(this));
677 | el.addEventListener('buttonchanged', this.recordEvent);
678 | el.addEventListener('buttonup', this.recordEvent);
679 | el.addEventListener('buttondown', this.recordEvent);
680 | el.addEventListener('touchstart', this.recordEvent);
681 | el.addEventListener('touchend', this.recordEvent);
682 | },
683 |
684 | recordEvent: function (evt) {
685 | var detail;
686 | if (!this.isRecording) { return; }
687 |
688 | // Filter out `target`, not serializable.
689 | if ('detail' in evt && 'state' in evt.detail && typeof evt.detail.state === 'object' &&
690 | 'target' in evt.detail.state) {
691 | delete evt.detail.state.target;
692 | }
693 |
694 | detail = {};
695 | EVENTS[evt.type].props.forEach(function buildDetail (propName) {
696 | // Convert GamepadButton to normal JS object.
697 | if (propName === 'state') {
698 | var stateProp;
699 | detail.state = {};
700 | for (stateProp in evt.detail.state) {
701 | detail.state[stateProp] = evt.detail.state[stateProp];
702 | }
703 | return;
704 | }
705 | detail[propName] = evt.detail[propName];
706 | });
707 |
708 | this.recordedEvents.push({
709 | name: evt.type,
710 | detail: detail,
711 | timestamp: this.lastTimestamp
712 | });
713 | },
714 |
715 | onTriggerChanged: function (evt) {
716 | var data = this.data;
717 | var value;
718 | if (!data.enabled || data.autoRecord) { return; }
719 | // Not Trigger
720 | if (evt.detail.id !== 1 || !this.data.recordingControls) { return; }
721 | value = evt.detail.state.value;
722 | if (value <= 0.1) {
723 | if (this.isRecording) { this.stopRecording(); }
724 | return;
725 | }
726 | if (!this.isRecording) { this.startRecording(); }
727 | },
728 |
729 | getJSONData: function () {
730 | var data;
731 | var trackedControlsComponent = this.el.components['tracked-controls'];
732 | var controller = trackedControlsComponent && trackedControlsComponent.controller;
733 | if (!this.recordedPoses) { return; }
734 | data = {
735 | poses: this.getStrokeJSON(this.recordedPoses),
736 | events: this.recordedEvents
737 | };
738 | if (controller) {
739 | data.gamepad = {
740 | id: controller.id,
741 | hand: controller.hand,
742 | index: controller.index
743 | };
744 | }
745 | return data;
746 | },
747 |
748 | getStrokeJSON: function (stroke) {
749 | var point;
750 | var points = [];
751 | for (var i = 0; i < stroke.length; i++) {
752 | point = stroke[i];
753 | points.push({
754 | position: point.position,
755 | rotation: point.rotation,
756 | timestamp: point.timestamp
757 | });
758 | }
759 | return points;
760 | },
761 |
762 | saveCapture: function (binary) {
763 | var jsonData = JSON.stringify(this.getJSONData());
764 | var type = binary ? 'application/octet-binary' : 'application/json';
765 | var blob = new Blob([jsonData], {type: type});
766 | var url = URL.createObjectURL(blob);
767 | var fileName = 'motion-capture-' + document.title + '-' + Date.now() + '.json';
768 | var aEl = document.createElement('a');
769 | aEl.setAttribute('class', 'motion-capture-download');
770 | aEl.href = url;
771 | aEl.setAttribute('download', fileName);
772 | aEl.innerHTML = 'downloading...';
773 | aEl.style.display = 'none';
774 | document.body.appendChild(aEl);
775 | setTimeout(function () {
776 | aEl.click();
777 | document.body.removeChild(aEl);
778 | }, 1);
779 | },
780 |
781 | update: function () {
782 | var el = this.el;
783 | var data = this.data;
784 | if (this.data.autoRecord) {
785 | this.startRecording();
786 | } else {
787 | // Don't try to record camera with controllers.
788 | if (el.components.camera) { return; }
789 |
790 | if (data.recordingControls) {
791 | el.setAttribute('vive-controls', {hand: data.hand});
792 | el.setAttribute('oculus-touch-controls', {hand: data.hand});
793 | }
794 | el.setAttribute('stroke', '');
795 | }
796 | },
797 |
798 | tick: (function () {
799 | var position = new THREE.Vector3();
800 | var rotation = new THREE.Quaternion();
801 | var scale = new THREE.Vector3();
802 |
803 | return function (time, delta) {
804 | var newPoint;
805 | var pointerPosition;
806 | this.lastTimestamp = time;
807 | if (!this.data.enabled || !this.isRecording) { return; }
808 | newPoint = {
809 | position: AFRAME.utils.clone(this.el.getAttribute('position')),
810 | rotation: AFRAME.utils.clone(this.el.getAttribute('rotation')),
811 | timestamp: time
812 | };
813 | this.recordedPoses.push(newPoint);
814 | if (!this.data.visibleStroke) { return; }
815 | this.el.object3D.updateMatrixWorld();
816 | this.el.object3D.matrixWorld.decompose(position, rotation, scale);
817 | pointerPosition = this.getPointerPosition(position, rotation);
818 | this.el.components.stroke.drawPoint(position, rotation, time, pointerPosition);
819 | };
820 | })(),
821 |
822 | getPointerPosition: (function () {
823 | var pointerPosition = new THREE.Vector3();
824 | var offset = new THREE.Vector3(0, 0.7, 1);
825 | return function getPointerPosition (position, orientation) {
826 | var pointer = offset
827 | .clone()
828 | .applyQuaternion(orientation)
829 | .normalize()
830 | .multiplyScalar(-0.03);
831 | pointerPosition.copy(position).add(pointer);
832 | return pointerPosition;
833 | };
834 | })(),
835 |
836 | startRecording: function () {
837 | var el = this.el;
838 | if (this.isRecording) { return; }
839 | if (el.components.stroke) { el.components.stroke.reset(); }
840 | this.isRecording = true;
841 | this.recordedPoses = [];
842 | this.recordedEvents = [];
843 | el.emit('strokestarted', {entity: el, poses: this.recordedPoses});
844 | },
845 |
846 | stopRecording: function () {
847 | var el = this.el;
848 | if (!this.isRecording) { return; }
849 | el.emit('strokeended', {poses: this.recordedPoses});
850 | this.isRecording = false;
851 | if (!this.data.visibleStroke || this.data.persistStroke) { return; }
852 | el.components.stroke.reset();
853 | }
854 | });
855 |
856 | },{}],6:[function(require,module,exports){
857 | /* global THREE, AFRAME */
858 | AFRAME.registerComponent('motion-capture-replayer', {
859 | schema: {
860 | enabled: {default: true},
861 | recorderEl: {type: 'selector'},
862 | loop: {default: false},
863 | src: {default: ''},
864 | spectatorCamera: {default: false}
865 | },
866 |
867 | init: function () {
868 | this.currentPoseTime = 0;
869 | this.currentEventTime = 0;
870 | this.currentPoseIndex = 0;
871 | this.currentEventIndex = 0;
872 | this.onStrokeStarted = this.onStrokeStarted.bind(this);
873 | this.onStrokeEnded = this.onStrokeEnded.bind(this);
874 | this.playComponent = this.playComponent.bind(this);
875 | this.el.addEventListener('pause', this.playComponent);
876 | this.discardedFrames = 0;
877 | this.playingEvents = [];
878 | this.playingPoses = [];
879 | this.gamepadData = null;
880 | },
881 |
882 | remove: function () {
883 | var el = this.el;
884 | var gamepadData = this.gamepadData;
885 | var gamepads;
886 | var found = -1;
887 |
888 | el.removeEventListener('pause', this.playComponent);
889 | this.stopReplaying();
890 | el.pause();
891 | el.play();
892 |
893 | // Remove gamepad from system.
894 | if (this.gamepadData) {
895 | gamepads = el.sceneEl.systems['motion-capture-replayer'].gamepads;
896 | gamepads.forEach(function (gamepad, i) {
897 | if (gamepad === gamepadData) { found = i; }
898 | });
899 | if (found !== -1) {
900 | gamepads.splice(found, 1);
901 | }
902 | }
903 | },
904 |
905 | update: function (oldData) {
906 | var data = this.data;
907 | this.updateRecorder(data.recorderEl, oldData.recorderEl);
908 | if (!this.el.isPlaying) { this.playComponent(); }
909 | if (oldData.src === data.src) { return; }
910 | if (data.src) { this.updateSrc(data.src); }
911 | },
912 |
913 | updateRecorder: function (newRecorderEl, oldRecorderEl) {
914 | if (oldRecorderEl && oldRecorderEl !== newRecorderEl) {
915 | oldRecorderEl.removeEventListener('strokestarted', this.onStrokeStarted);
916 | oldRecorderEl.removeEventListener('strokeended', this.onStrokeEnded);
917 | }
918 | if (!newRecorderEl || oldRecorderEl === newRecorderEl) { return; }
919 | newRecorderEl.addEventListener('strokestarted', this.onStrokeStarted);
920 | newRecorderEl.addEventListener('strokeended', this.onStrokeEnded);
921 | },
922 |
923 | updateSrc: function (src) {
924 | this.el.sceneEl.systems['motion-capture-recorder'].loadRecordingFromUrl(
925 | src, false, this.startReplaying.bind(this));
926 | },
927 |
928 | onStrokeStarted: function(evt) {
929 | this.reset();
930 | },
931 |
932 | onStrokeEnded: function(evt) {
933 | this.startReplayingPoses(evt.detail.poses);
934 | },
935 |
936 | play: function () {
937 | if (this.playingStroke) { this.playStroke(this.playingStroke); }
938 | },
939 |
940 | playComponent: function () {
941 | this.el.isPlaying = true;
942 | this.play();
943 | },
944 |
945 | /**
946 | * @param {object} data - Recording data.
947 | */
948 | startReplaying: function (data) {
949 | var el = this.el;
950 |
951 | this.ignoredFrames = 0;
952 | this.storeInitialPose();
953 | this.isReplaying = true;
954 | this.startReplayingPoses(data.poses);
955 | this.startReplayingEvents(data.events);
956 |
957 | // Add gamepad metadata to system.
958 | if (data.gamepad) {
959 | this.gamepadData = data.gamepad;
960 | el.sceneEl.systems['motion-capture-replayer'].gamepads.push(data.gamepad);
961 | el.emit('gamepadconnected');
962 | }
963 |
964 | el.emit('replayingstarted');
965 | },
966 |
967 | stopReplaying: function () {
968 | this.isReplaying = false;
969 | this.restoreInitialPose();
970 | this.el.emit('replayingstopped');
971 | },
972 |
973 | storeInitialPose: function () {
974 | var el = this.el;
975 | this.initialPose = {
976 | position: AFRAME.utils.clone(el.getAttribute('position')),
977 | rotation: AFRAME.utils.clone(el.getAttribute('rotation'))
978 | };
979 | },
980 |
981 | restoreInitialPose: function () {
982 | var el = this.el;
983 | if (!this.initialPose) { return; }
984 | el.setAttribute('position', this.initialPose.position);
985 | el.setAttribute('rotation', this.initialPose.rotation);
986 | },
987 |
988 | startReplayingPoses: function (poses) {
989 | this.isReplaying = true;
990 | this.currentPoseIndex = 0;
991 | if (poses.length === 0) { return; }
992 | this.playingPoses = poses;
993 | this.currentPoseTime = poses[0].timestamp;
994 | },
995 |
996 | /**
997 | * @param events {Array} - Array of events with timestamp, name, and detail.
998 | */
999 | startReplayingEvents: function (events) {
1000 | var firstEvent;
1001 | this.isReplaying = true;
1002 | this.currentEventIndex = 0;
1003 | if (events.length === 0) { return; }
1004 | firstEvent = events[0];
1005 | this.playingEvents = events;
1006 | this.currentEventTime = firstEvent.timestamp;
1007 | this.el.emit(firstEvent.name, firstEvent.detail);
1008 | },
1009 |
1010 | // Reset player
1011 | reset: function () {
1012 | this.playingPoses = null;
1013 | this.currentTime = undefined;
1014 | this.currentPoseIndex = undefined;
1015 | },
1016 |
1017 | /**
1018 | * Called on tick.
1019 | */
1020 | playRecording: function (delta) {
1021 | var currentPose;
1022 | var currentEvent
1023 | var playingPoses = this.playingPoses;
1024 | var playingEvents = this.playingEvents;
1025 | currentPose = playingPoses && playingPoses[this.currentPoseIndex]
1026 | currentEvent = playingEvents && playingEvents[this.currentEventIndex];
1027 | this.currentPoseTime += delta;
1028 | this.currentEventTime += delta;
1029 | // Determine next pose.
1030 | // Comparing currentPoseTime to currentEvent.timestamp is not a typo.
1031 | while ((currentPose && this.currentPoseTime >= currentPose.timestamp) ||
1032 | (currentEvent && this.currentPoseTime >= currentEvent.timestamp)) {
1033 | // Pose.
1034 | if (currentPose && this.currentPoseTime >= currentPose.timestamp) {
1035 | if (this.currentPoseIndex === playingPoses.length - 1) {
1036 | if (this.data.loop) {
1037 | this.currentPoseIndex = 0;
1038 | this.currentPoseTime = playingPoses[0].timestamp;
1039 | } else {
1040 | this.stopReplaying();
1041 | }
1042 | }
1043 | applyPose(this.el, currentPose);
1044 | this.currentPoseIndex += 1;
1045 | currentPose = playingPoses[this.currentPoseIndex];
1046 | }
1047 | // Event.
1048 | if (currentEvent && this.currentPoseTime >= currentEvent.timestamp) {
1049 | if (this.currentEventIndex === playingEvents.length && this.data.loop) {
1050 | this.currentEventIndex = 0;
1051 | this.currentEventTime = playingEvents[0].timestamp;
1052 | }
1053 | this.el.emit(currentEvent.name, currentEvent.detail);
1054 | this.currentEventIndex += 1;
1055 | currentEvent = this.playingEvents[this.currentEventIndex];
1056 | }
1057 | }
1058 | },
1059 |
1060 | tick: function (time, delta) {
1061 | // Ignore the first couple of frames that come from window.RAF on Firefox.
1062 | if (this.ignoredFrames !== 2 && !window.debug) {
1063 | this.ignoredFrames++;
1064 | return;
1065 | }
1066 |
1067 | if (!this.isReplaying) { return; }
1068 | this.playRecording(delta);
1069 | }
1070 | });
1071 |
1072 | function applyPose (el, pose) {
1073 | el.setAttribute('position', pose.position);
1074 | el.setAttribute('rotation', pose.rotation);
1075 | };
1076 |
1077 | },{}],7:[function(require,module,exports){
1078 | /* global THREE AFRAME */
1079 | AFRAME.registerComponent('stroke', {
1080 | schema: {
1081 | enabled: {default: true},
1082 | color: {default: '#ef2d5e', type: 'color'}
1083 | },
1084 |
1085 | init: function () {
1086 | var maxPoints = this.maxPoints = 3000;
1087 | var strokeEl;
1088 | this.idx = 0;
1089 | this.numPoints = 0;
1090 |
1091 | // Buffers
1092 | this.vertices = new Float32Array(maxPoints*3*3);
1093 | this.normals = new Float32Array(maxPoints*3*3);
1094 | this.uvs = new Float32Array(maxPoints*2*2);
1095 |
1096 | // Geometries
1097 | this.geometry = new THREE.BufferGeometry();
1098 | this.geometry.setDrawRange(0, 0);
1099 | this.geometry.addAttribute('position', new THREE.BufferAttribute(this.vertices, 3).setDynamic(true));
1100 | this.geometry.addAttribute('uv', new THREE.BufferAttribute(this.uvs, 2).setDynamic(true));
1101 | this.geometry.addAttribute('normal', new THREE.BufferAttribute(this.normals, 3).setDynamic(true));
1102 |
1103 | this.material = new THREE.MeshStandardMaterial({
1104 | color: this.data.color,
1105 | roughness: 0.75,
1106 | metalness: 0.25,
1107 | side: THREE.DoubleSide
1108 | });
1109 |
1110 | var mesh = new THREE.Mesh(this.geometry, this.material);
1111 | mesh.drawMode = THREE.TriangleStripDrawMode;
1112 | mesh.frustumCulled = false;
1113 |
1114 | // Injects stroke entity
1115 | strokeEl = document.createElement('a-entity');
1116 | strokeEl.setObject3D('stroke', mesh);
1117 | this.el.sceneEl.appendChild(strokeEl);
1118 | },
1119 |
1120 | update: function() {
1121 | this.material.color.set(this.data.color);
1122 | },
1123 |
1124 | drawPoint: (function () {
1125 | var direction = new THREE.Vector3();
1126 | var positionA = new THREE.Vector3();
1127 | var positionB = new THREE.Vector3();
1128 | return function (position, orientation, timestamp, pointerPosition) {
1129 | var uv = 0;
1130 | var numPoints = this.numPoints;
1131 | var brushSize = 0.01;
1132 | if (numPoints === this.maxPoints) { return; }
1133 | for (i = 0; i < numPoints; i++) {
1134 | this.uvs[uv++] = i / (numPoints - 1);
1135 | this.uvs[uv++] = 0;
1136 |
1137 | this.uvs[uv++] = i / (numPoints - 1);
1138 | this.uvs[uv++] = 1;
1139 | }
1140 |
1141 | direction.set(1, 0, 0);
1142 | direction.applyQuaternion(orientation);
1143 | direction.normalize();
1144 |
1145 | positionA.copy(pointerPosition);
1146 | positionB.copy(pointerPosition);
1147 | positionA.add(direction.clone().multiplyScalar(brushSize / 2));
1148 | positionB.add(direction.clone().multiplyScalar(-brushSize / 2));
1149 |
1150 | this.vertices[this.idx++] = positionA.x;
1151 | this.vertices[this.idx++] = positionA.y;
1152 | this.vertices[this.idx++] = positionA.z;
1153 |
1154 | this.vertices[this.idx++] = positionB.x;
1155 | this.vertices[this.idx++] = positionB.y;
1156 | this.vertices[this.idx++] = positionB.z;
1157 |
1158 | this.computeVertexNormals();
1159 | this.geometry.attributes.normal.needsUpdate = true;
1160 | this.geometry.attributes.position.needsUpdate = true;
1161 | this.geometry.attributes.uv.needsUpdate = true;
1162 |
1163 | this.geometry.setDrawRange(0, numPoints * 2);
1164 | this.numPoints += 1;
1165 | return true;
1166 | }
1167 | })(),
1168 |
1169 | reset: function () {
1170 | var idx = 0;
1171 | var vertices = this.vertices;
1172 | for (i = 0; i < this.numPoints; i++) {
1173 | vertices[idx++] = 0;
1174 | vertices[idx++] = 0;
1175 | vertices[idx++] = 0;
1176 |
1177 | vertices[idx++] = 0;
1178 | vertices[idx++] = 0;
1179 | vertices[idx++] = 0;
1180 | }
1181 | this.geometry.setDrawRange(0, 0);
1182 | this.idx = 0;
1183 | this.numPoints = 0;
1184 | },
1185 |
1186 | computeVertexNormals: function () {
1187 | var pA = new THREE.Vector3();
1188 | var pB = new THREE.Vector3();
1189 | var pC = new THREE.Vector3();
1190 | var cb = new THREE.Vector3();
1191 | var ab = new THREE.Vector3();
1192 |
1193 | for (var i = 0, il = this.idx; i < il; i++) {
1194 | this.normals[ i ] = 0;
1195 | }
1196 |
1197 | var pair = true;
1198 | for (i = 0, il = this.idx; i < il; i += 3) {
1199 | if (pair) {
1200 | pA.fromArray(this.vertices, i);
1201 | pB.fromArray(this.vertices, i + 3);
1202 | pC.fromArray(this.vertices, i + 6);
1203 | } else {
1204 | pA.fromArray(this.vertices, i + 3);
1205 | pB.fromArray(this.vertices, i);
1206 | pC.fromArray(this.vertices, i + 6);
1207 | }
1208 | pair = !pair;
1209 |
1210 | cb.subVectors(pC, pB);
1211 | ab.subVectors(pA, pB);
1212 | cb.cross(ab);
1213 | cb.normalize();
1214 |
1215 | this.normals[i] += cb.x;
1216 | this.normals[i + 1] += cb.y;
1217 | this.normals[i + 2] += cb.z;
1218 |
1219 | this.normals[i + 3] += cb.x;
1220 | this.normals[i + 4] += cb.y;
1221 | this.normals[i + 5] += cb.z;
1222 |
1223 | this.normals[i + 6] += cb.x;
1224 | this.normals[i + 7] += cb.y;
1225 | this.normals[i + 8] += cb.z;
1226 | }
1227 |
1228 | /*
1229 | first and last vertice (0 and 8) belongs just to one triangle
1230 | second and penultimate (1 and 7) belongs to two triangles
1231 | the rest of the vertices belongs to three triangles
1232 |
1233 | 1_____3_____5_____7
1234 | /\ /\ /\ /\
1235 | / \ / \ / \ / \
1236 | /____\/____\/____\/____\
1237 | 0 2 4 6 8
1238 | */
1239 |
1240 | // Vertices that are shared across three triangles
1241 | for (i = 2 * 3, il = this.idx - 2 * 3; i < il; i++) {
1242 | this.normals[ i ] = this.normals[ i ] / 3;
1243 | }
1244 |
1245 | // Second and penultimate triangle, that shares just two triangles
1246 | this.normals[ 3 ] = this.normals[ 3 ] / 2;
1247 | this.normals[ 3 + 1 ] = this.normals[ 3 + 1 ] / 2;
1248 | this.normals[ 3 + 2 ] = this.normals[ 3 * 1 + 2 ] / 2;
1249 |
1250 | this.normals[ this.idx - 2 * 3 ] = this.normals[ this.idx - 2 * 3 ] / 2;
1251 | this.normals[ this.idx - 2 * 3 + 1 ] = this.normals[ this.idx - 2 * 3 + 1 ] / 2;
1252 | this.normals[ this.idx - 2 * 3 + 2 ] = this.normals[ this.idx - 2 * 3 + 2 ] / 2;
1253 |
1254 | this.geometry.normalizeNormals();
1255 | }
1256 | });
1257 |
1258 | },{}],8:[function(require,module,exports){
1259 | module.exports.LOCALSTORAGE_RECORDINGS = 'avatarRecordings';
1260 | module.exports.DEFAULT_RECORDING_NAME = 'default';
1261 |
1262 | },{}],9:[function(require,module,exports){
1263 | if (typeof AFRAME === 'undefined') {
1264 | throw new Error('Component attempted to register before AFRAME was available.');
1265 | }
1266 |
1267 | // Components.
1268 | require('./components/motion-capture-recorder.js');
1269 | require('./components/motion-capture-replayer.js');
1270 | require('./components/avatar-recorder.js');
1271 | require('./components/avatar-replayer.js');
1272 | require('./components/stroke.js');
1273 |
1274 | // Systems.
1275 | require('./systems/motion-capture-replayer.js');
1276 | require('./systems/recordingdb.js');
1277 |
1278 | },{"./components/avatar-recorder.js":3,"./components/avatar-replayer.js":4,"./components/motion-capture-recorder.js":5,"./components/motion-capture-replayer.js":6,"./components/stroke.js":7,"./systems/motion-capture-replayer.js":10,"./systems/recordingdb.js":11}],10:[function(require,module,exports){
1279 | AFRAME.registerSystem('motion-capture-replayer', {
1280 | init: function () {
1281 | var sceneEl = this.sceneEl;
1282 | var trackedControlsComponent;
1283 | var trackedControlsSystem;
1284 | var trackedControlsTick;
1285 |
1286 | trackedControlsSystem = sceneEl.systems['tracked-controls'];
1287 | trackedControlsTick = AFRAME.components['tracked-controls'].Component.prototype.tick;
1288 |
1289 | // Gamepad data stored in recording and added here by `motion-capture-replayer` component.
1290 | this.gamepads = [];
1291 |
1292 | // Wrap `updateControllerList`.
1293 | this.updateControllerListOriginal = trackedControlsSystem.updateControllerList.bind(
1294 | trackedControlsSystem);
1295 | trackedControlsSystem.updateControllerList = this.updateControllerList.bind(this);
1296 |
1297 | // Wrap `tracked-controls` tick.
1298 | trackedControlsComponent = AFRAME.components['tracked-controls'].Component.prototype;
1299 | trackedControlsComponent.tick = this.trackedControlsTickWrapper;
1300 | trackedControlsComponent.trackedControlsTick = trackedControlsTick;
1301 | },
1302 |
1303 | remove: function () {
1304 | // restore modified objects
1305 | var trackedControlsComponent = AFRAME.components['tracked-controls'].Component.prototype;
1306 | var trackedControlsSystem = this.sceneEl.systems['tracked-controls'];
1307 | trackedControlsComponent.tick = trackedControlsComponent.trackedControlsTick;
1308 | delete trackedControlsComponent.trackedControlsTick;
1309 | trackedControlsSystem.updateControllerList = this.updateControllerListOriginal;
1310 | },
1311 |
1312 | trackedControlsTickWrapper: function (time, delta) {
1313 | if (this.el.components['motion-capture-replayer']) { return; }
1314 | this.trackedControlsTick(time, delta);
1315 | },
1316 |
1317 | /**
1318 | * Wrap `updateControllerList` to stub in the gamepads and emit `controllersupdated`.
1319 | */
1320 | updateControllerList: function () {
1321 | var i;
1322 | var sceneEl = this.sceneEl;
1323 | var trackedControlsSystem = sceneEl.systems['tracked-controls'];
1324 |
1325 | this.updateControllerListOriginal();
1326 |
1327 | this.gamepads.forEach(function (gamepad) {
1328 | if (trackedControlsSystem.controllers[gamepad.index]) { return; }
1329 | trackedControlsSystem.controllers[gamepad.index] = gamepad;
1330 | });
1331 |
1332 | for (i = 0; i < trackedControlsSystem.controllers.length; i++) {
1333 | if (trackedControlsSystem.controllers[i]) { continue; }
1334 | trackedControlsSystem.controllers[i] = {id: '___', index: -1, hand: 'finger'};
1335 | }
1336 |
1337 | sceneEl.emit('controllersupdated', undefined, false);
1338 | }
1339 | });
1340 |
1341 | },{}],11:[function(require,module,exports){
1342 | /* global indexedDB */
1343 | var constants = require('../constants');
1344 |
1345 | var DB_NAME = 'motionCaptureRecordings';
1346 | var OBJECT_STORE_NAME = 'recordings';
1347 | var VERSION = 1;
1348 |
1349 | /**
1350 | * Interface for storing and accessing recordings from Indexed DB.
1351 | */
1352 | AFRAME.registerSystem('recordingdb', {
1353 | init: function () {
1354 | var request;
1355 | var self = this;
1356 |
1357 | this.db = null;
1358 | this.hasLoaded = false;
1359 |
1360 | request = indexedDB.open(DB_NAME, VERSION);
1361 |
1362 | request.onerror = function () {
1363 | console.error('Error opening IndexedDB for motion capture.', request.error);
1364 | };
1365 |
1366 | // Initialize database.
1367 | request.onupgradeneeded = function (evt) {
1368 | var db = self.db = evt.target.result;
1369 | var objectStore;
1370 |
1371 | // Create object store.
1372 | objectStore = db.createObjectStore('recordings', {
1373 | autoIncrement: false
1374 | });
1375 | objectStore.createIndex('recordingName', 'recordingName', {unique: true});
1376 | self.objectStore = objectStore;
1377 | };
1378 |
1379 | // Got database.
1380 | request.onsuccess = function (evt) {
1381 | self.db = evt.target.result;
1382 | self.hasLoaded = true;
1383 | self.sceneEl.emit('recordingdbinitialized');
1384 | };
1385 | },
1386 |
1387 | /**
1388 | * Need a new transaction for everything.
1389 | */
1390 | getTransaction: function () {
1391 | var transaction = this.db.transaction([OBJECT_STORE_NAME], 'readwrite');
1392 | return transaction.objectStore(OBJECT_STORE_NAME);
1393 | },
1394 |
1395 | getRecordingNames: function () {
1396 | var self = this;
1397 | return new Promise(function (resolve) {
1398 | var recordingNames = [];
1399 |
1400 | self.waitForDb(function () {
1401 | self.getTransaction().openCursor().onsuccess = function (evt) {
1402 | var cursor = evt.target.result;
1403 |
1404 | // No recordings.
1405 | if (!cursor) {
1406 | resolve(recordingNames.sort());
1407 | return;
1408 | }
1409 |
1410 | recordingNames.push(cursor.key);
1411 | cursor.continue();
1412 | };
1413 | });
1414 | });
1415 | },
1416 |
1417 | getRecordings: function (cb) {
1418 | var self = this;
1419 | return new Promise(function getRecordings (resolve) {
1420 | self.waitForDb(function () {
1421 | self.getTransaction().openCursor().onsuccess = function (evt) {
1422 | var cursor = evt.target.result;
1423 | var recordings = [cursor.value];
1424 | while (cursor.ontinue()) {
1425 | recordings.push(cursor.value);
1426 | }
1427 | resolve(recordings);
1428 | };
1429 | });
1430 | });
1431 | },
1432 |
1433 | getRecording: function (name) {
1434 | var self = this;
1435 | return new Promise(function getRecording (resolve) {
1436 | self.waitForDb(function () {
1437 | self.getTransaction().get(name).onsuccess = function (evt) {
1438 | resolve(evt.target.result);
1439 | };
1440 | });
1441 | });
1442 | },
1443 |
1444 | addRecording: function (name, data) {
1445 | this.getTransaction().add(data, name);
1446 | },
1447 |
1448 | deleteRecording: function (name) {
1449 | this.getTransaction().delete(name);
1450 | },
1451 |
1452 | /**
1453 | * Helper to wait for store to be initialized before using it.
1454 | */
1455 | waitForDb: function (cb) {
1456 | if (this.hasLoaded) {
1457 | cb();
1458 | return;
1459 | }
1460 | this.sceneEl.addEventListener('recordingdbinitialized', cb);
1461 | }
1462 | });
1463 |
1464 | },{"../constants":8}],12:[function(require,module,exports){
1465 | 'use strict';
1466 |
1467 | /* global AFRAME, THREE, CANNON */
1468 | AFRAME.registerComponent('body-merger', {
1469 | schema: { default: 'static-body' },
1470 | init: function () {
1471 | const doMerge = evt => {
1472 | if (evt.target === this.el) {
1473 | this.el.removeEventListener('body-loaded', doMerge);
1474 | this.merge();
1475 | }
1476 | };
1477 | if (this.el.body) {
1478 | this.merge();
1479 | } else {
1480 | this.el.addEventListener('body-loaded', doMerge);
1481 | }
1482 | },
1483 | merge: function () {
1484 | const body = this.el.body;
1485 | const tmpMat = new THREE.Matrix4();
1486 | const tmpQuat = new THREE.Quaternion();
1487 | const tmpPos = new THREE.Vector3();
1488 | const tmpScale = new THREE.Vector3(1, 1, 1); // todo: apply worldScale
1489 | const offset = new CANNON.Vec3();
1490 | const orientation = new CANNON.Quaternion();
1491 | for (let child of this.el.childNodes) {
1492 | if (!child.body || !child.getAttribute(this.data)) {
1493 | continue;
1494 | }
1495 | child.object3D.updateMatrix();
1496 | while (child.body.shapes.length) {
1497 | tmpPos.copy(child.body.shapeOffsets.pop());
1498 | tmpQuat.copy(child.body.shapeOrientations.pop());
1499 | tmpMat.compose(tmpPos, tmpQuat, tmpScale);
1500 | tmpMat.multiply(child.object3D.matrix);
1501 | tmpMat.decompose(tmpPos, tmpQuat, tmpScale);
1502 | offset.copy(tmpPos);
1503 | orientation.copy(tmpQuat);
1504 | body.addShape(child.body.shapes.pop(), offset, orientation);
1505 | }
1506 | child.removeAttribute(this.data);
1507 | }
1508 | }
1509 | });
1510 |
1511 | },{}],13:[function(require,module,exports){
1512 | 'use strict';
1513 |
1514 | /* global AFRAME */
1515 | AFRAME.registerComponent('physics-collider', {
1516 | schema: {
1517 | ignoreSleep: { default: true }
1518 | },
1519 | init: function () {
1520 | this.collisions = new Set();
1521 | this.currentCollisions = new Set();
1522 | this.newCollisions = [];
1523 | this.clearedCollisions = [];
1524 | this.collisionEventDetails = {
1525 | els: this.newCollisions,
1526 | clearedEls: this.clearedCollisions
1527 | };
1528 | },
1529 | update: function () {
1530 | if (this.el.body) {
1531 | this.updateBody();
1532 | } else {
1533 | this.el.addEventListener('body-loaded', this.updateBody.bind(this), { once: true });
1534 | }
1535 | },
1536 | tick: function () {
1537 | const uppperMask = 0xFFFF0000;
1538 | const lowerMask = 0x0000FFFF;
1539 | return function () {
1540 | if (!this.el.body) return;
1541 | const currentCollisions = this.currentCollisions;
1542 | const thisBodyId = this.el.body.id;
1543 | const worldCollisions = this.el.body.world.bodyOverlapKeeper.current;
1544 | const worldBodyMap = this.el.body.world.idToBodyMap;
1545 | const collisions = this.collisions;
1546 | const newCollisions = this.newCollisions;
1547 | const clearedCollisions = this.clearedCollisions;
1548 | let i = 0;
1549 | let upperId = (worldCollisions[i] & uppperMask) >> 16;
1550 | let target;
1551 | newCollisions.length = clearedCollisions.length = 0;
1552 | currentCollisions.clear();
1553 | while (i < worldCollisions.length && upperId < thisBodyId) {
1554 | if (worldBodyMap[upperId]) {
1555 | target = worldBodyMap[upperId].el;
1556 | if ((worldCollisions[i] & lowerMask) === thisBodyId) {
1557 | currentCollisions.add(target);
1558 | if (!collisions.has(target)) {
1559 | newCollisions.push(target);
1560 | }
1561 | }
1562 | }
1563 | upperId = (worldCollisions[++i] & uppperMask) >> 16;
1564 | }
1565 | while (i < worldCollisions.length && upperId === thisBodyId) {
1566 | if (worldBodyMap[worldCollisions[i] & lowerMask]) {
1567 | target = worldBodyMap[worldCollisions[i] & lowerMask].el;
1568 | currentCollisions.add(target);
1569 | if (!collisions.has(target)) {
1570 | newCollisions.push(target);
1571 | }
1572 | }
1573 | upperId = (worldCollisions[++i] & uppperMask) >> 16;
1574 | }
1575 |
1576 | for (let col of collisions) {
1577 | if (!currentCollisions.has(col)) {
1578 | clearedCollisions.push(col);
1579 | collisions.delete(col);
1580 | }
1581 | }
1582 | for (let col of newCollisions) {
1583 | collisions.add(col);
1584 | }
1585 | if (newCollisions.length || clearedCollisions.length) {
1586 | this.el.emit('collisions', this.collisionEventDetails);
1587 | }
1588 | };
1589 | }(),
1590 | remove: function () {
1591 | if (this.originalSleepConfig) {
1592 | AFRAME.utils.extend(this.el.body, this.originalSleepConfig);
1593 | }
1594 | },
1595 | updateBody: function (evt) {
1596 | // ignore bubbled 'body-loaded' events
1597 | if (evt !== undefined && evt.target !== this.el) {
1598 | return;
1599 | }
1600 | if (this.data.ignoreSleep) {
1601 | // ensure sleep doesn't disable collision detection
1602 | this.el.body.allowSleep = false;
1603 | /* naiveBroadphase ignores collisions between sleeping & static bodies */
1604 | this.el.body.type = window.CANNON.Body.KINEMATIC;
1605 | // Kinematics must have velocity >= their sleep limit to wake others
1606 | this.el.body.sleepSpeedLimit = 0;
1607 | } else if (this.originalSleepConfig === undefined) {
1608 | this.originalSleepConfig = {
1609 | allowSleep: this.el.body.allowSleep,
1610 | sleepSpeedLimit: this.el.body.sleepSpeedLimit,
1611 | type: this.el.body.type
1612 | };
1613 | } else {
1614 | // restore original settings
1615 | AFRAME.utils.extend(this.el.body, this.originalSleepConfig);
1616 | }
1617 | }
1618 | });
1619 |
1620 | },{}],14:[function(require,module,exports){
1621 | 'use strict';
1622 |
1623 | /* global AFRAME */
1624 | AFRAME.registerComponent('collision-filter', {
1625 | schema: {
1626 | group: { default: 'default' },
1627 | collidesWith: { default: ['default'] },
1628 | collisionForces: { default: true }
1629 | },
1630 | init: function () {
1631 | this.updateBodyBound = this.updateBody.bind(this);
1632 | this.system.registerMe(this);
1633 | this.el.addEventListener('body-loaded', this.updateBodyBound);
1634 | },
1635 | update: function () {
1636 | // register any new groups
1637 | this.system.registerMe(this);
1638 | if (this.el.body) {
1639 | this.updateBody();
1640 | }
1641 | },
1642 | remove: function () {
1643 | this.el.removeEventListener('body-loaded', this.updateBodyBound);
1644 | },
1645 | updateBody: function (evt) {
1646 | // ignore bubbled 'body-loaded' events
1647 | if (evt !== undefined && evt.target !== this.el) {
1648 | return;
1649 | }
1650 | this.el.body.collisionFilterMask = this.system.getFilterCode(this.data.collidesWith);
1651 | this.el.body.collisionFilterGroup = this.system.getFilterCode(this.data.group);
1652 | this.el.body.collisionResponse = this.data.collisionForces;
1653 | }
1654 | });
1655 |
1656 | AFRAME.registerSystem('collision-filter', {
1657 | schema: {
1658 | collisionGroups: { default: ['default'] }
1659 | },
1660 | dependencies: ['physics'],
1661 | init: function () {
1662 | this.maxGroups = Math.log2(Number.MAX_SAFE_INTEGER);
1663 | },
1664 | registerMe: function (comp) {
1665 | // add any unknown groups to the master list
1666 | const newGroups = [comp.data.group, ...comp.data.collidesWith].filter(group => this.data.collisionGroups.indexOf(group) === -1);
1667 | this.data.collisionGroups.push(...newGroups);
1668 | if (this.data.collisionGroups.length > this.maxGroups) {
1669 | throw new Error('Too many collision groups');
1670 | }
1671 | },
1672 | getFilterCode: function (elGroups) {
1673 | let code = 0;
1674 | if (!Array.isArray(elGroups)) {
1675 | elGroups = [elGroups];
1676 | }
1677 | // each group corresponds to a bit which is turned on when matched
1678 | // floor negates any unmatched groups (2^-1 = 0.5)
1679 | elGroups.forEach(group => {
1680 | code += Math.floor(Math.pow(2, this.data.collisionGroups.indexOf(group)));
1681 | });
1682 | return code;
1683 | }
1684 | });
1685 |
1686 | },{}],15:[function(require,module,exports){
1687 | 'use strict';
1688 |
1689 | // Make dynamic bodies idle when not grabbed
1690 | /* global AFRAME */
1691 | AFRAME.registerComponent('sleepy', {
1692 | schema: {
1693 | allowSleep: { default: true },
1694 | speedLimit: { default: 0.25, type: 'number' },
1695 | delay: { default: 0.25, type: 'number' },
1696 | linearDamping: { default: 0.99, type: 'number' },
1697 | angularDamping: { default: 0.99, type: 'number' },
1698 | holdState: { default: 'grabbed' }
1699 | },
1700 | init: function () {
1701 | this.updateBodyBound = this.updateBody.bind(this);
1702 | this.holdStateBound = this.holdState.bind(this);
1703 | this.resumeStateBound = this.resumeState.bind(this);
1704 |
1705 | this.el.addEventListener('body-loaded', this.updateBodyBound);
1706 | },
1707 | update: function () {
1708 | if (this.el.body) {
1709 | this.updateBody();
1710 | }
1711 | },
1712 | remove: function () {
1713 | this.el.removeEventListener('body-loaded', this.updateBodyBound);
1714 | this.el.removeEventListener('stateadded', this.holdStateBound);
1715 | this.el.removeEventListener('stateremoved', this.resumeStateBound);
1716 | },
1717 | updateBody: function (evt) {
1718 | // ignore bubbled 'body-loaded' events
1719 | if (evt !== undefined && evt.target !== this.el) {
1720 | return;
1721 | }
1722 | if (this.data.allowSleep) {
1723 | // only "local" driver compatable
1724 | try {
1725 | this.el.body.world.allowSleep = true;
1726 | } catch (err) {
1727 | console.error('Unable to activate sleep in physics.' + '`sleepy` requires "local" physics driver');
1728 | }
1729 | }
1730 | this.el.body.allowSleep = this.data.allowSleep;
1731 | this.el.body.sleepSpeedLimit = this.data.speedLimit;
1732 | this.el.body.sleepTimeLimit = this.data.delay;
1733 | this.el.body.linearDamping = this.data.linearDamping;
1734 | this.el.body.angularDamping = this.data.angularDamping;
1735 | if (this.data.allowSleep) {
1736 | this.el.addEventListener('stateadded', this.holdStateBound);
1737 | this.el.addEventListener('stateremoved', this.resumeStateBound);
1738 | } else {
1739 | this.el.removeEventListener('stateadded', this.holdStateBound);
1740 | this.el.removeEventListener('stateremoved', this.resumeStateBound);
1741 | }
1742 | },
1743 | // disble the sleeping during interactions because sleep will break constraints
1744 | holdState: function (evt) {
1745 | let state = this.data.holdState;
1746 | // api change in A-Frame v0.8.0
1747 | if (evt.detail === state || evt.detail.state === state) {
1748 | this.el.body.allowSleep = false;
1749 | }
1750 | },
1751 | resumeState: function (evt) {
1752 | let state = this.data.holdState;
1753 | if (evt.detail === state || evt.detail.state === state) {
1754 | this.el.body.allowSleep = this.data.allowSleep;
1755 | }
1756 | }
1757 |
1758 | });
1759 |
1760 | },{}]},{},[1]);
1761 |
--------------------------------------------------------------------------------
/examples/collision_response/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A-Frame Physics Extras Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
27 |
28 |
29 |
30 |
31 |
32 | Don't have a Vive or Rift handy? Click here for a preview.
33 |
34 |
35 |
36 |
37 |
38 |
39 |
50 |
51 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
63 |
64 |
66 |
67 |
68 |
70 |
71 |
73 |
74 |
75 |
78 |
79 |
82 |
83 |
84 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | A-Frame Physics Extras
4 |
61 |
62 |
63 | A-Frame Physics Extras Example Page
64 | This is the examples page for the
65 |
66 | aframe-physics-extras
67 |
68 | WebVR package.
69 |
70 |
71 | Collision Physics Settings
72 |
73 |
74 |
75 |
76 | Open hands can pass through objects
77 | With a pointing gesture (trackpad on Vive wands), hands become
78 | solid and can push objects around
79 | Reach inside objects and use grip or trigger to pick up and move
80 |
81 | Example of toggling the collisionForces
property of
82 | collision-filter
to affect whether the controllers have
83 | physical interactions with the environment or not.
84 |
85 |
86 | Merging Physics Bodies
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/examples/main.js:
--------------------------------------------------------------------------------
1 | require('../index.js')
2 | require('aframe-motion-capture-components')
3 | /* used in examples to allow a desktop playback without HMD
4 | defined here to keep example files clear of clutter */
5 | window.playDemoRecording = function (spectate) {
6 | let l = document.querySelector('a-link, a-entity[link]')
7 | let s = document.querySelector('a-scene')
8 | l && l.setAttribute('visible', 'false')
9 | s.addEventListener('replayingstopped', e => {
10 | let c = document.querySelector('[camera]')
11 | window.setTimeout(function () {
12 | c.setAttribute('position', '0 1.6 2')
13 | c.setAttribute('rotation', '0 0 0')
14 | })
15 | })
16 | s.setAttribute('avatar-replayer', {
17 | src: './demo-recording.json',
18 | spectatorMode: spectate === undefined ? true : spectate,
19 | spectatorPosition: {x: 0, y: 1.6, z: 2}
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 |
3 | if (typeof AFRAME === 'undefined') {
4 | throw new Error('Component attempted to register before AFRAME was available.')
5 | }
6 |
7 | require('./src/physics-collider.js')
8 | require('./src/physics-collision-filter.js')
9 | require('./src/physics-sleepy.js')
10 | require('./src/body-merger.js')
11 |
--------------------------------------------------------------------------------
/machinima_tests/__init.test.js:
--------------------------------------------------------------------------------
1 | /* global sinon, setup, teardown */
2 | var machinima = require('aframe-machinima-testing')
3 | /**
4 | * __init.test.js is run before every test case.
5 | */
6 | window.debug = true
7 |
8 | setup(function () {
9 | this.sinon = sinon.sandbox.create()
10 | })
11 |
12 | teardown(function () {
13 | machinima.teardownReplayer()
14 | // Clean up any attached elements.
15 | const attachedEls = ['canvas', 'a-assets', 'a-scene']
16 | var els = document.querySelectorAll(attachedEls.join(','))
17 |
18 | for (var i = 0; i < els.length; i++) {
19 | els[i].parentNode.removeChild(els[i])
20 | }
21 | this.sinon.restore()
22 | })
23 |
--------------------------------------------------------------------------------
/machinima_tests/assets/blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/machinima_tests/assets/blue.png
--------------------------------------------------------------------------------
/machinima_tests/assets/green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/machinima_tests/assets/green.png
--------------------------------------------------------------------------------
/machinima_tests/assets/red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/machinima_tests/assets/red.png
--------------------------------------------------------------------------------
/machinima_tests/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma configuration
2 | var karmaConf = {
3 | browserify: {
4 | debug: true
5 | },
6 | browsers: ['Firefox', 'Chrome'],
7 | // prevent timeout during recording playback
8 | browserNoActivityTimeout: 600000,
9 | client: {
10 | captureConsole: false,
11 | mocha: {'ui': 'tdd'}
12 | },
13 | files: [
14 | // module and dependencies
15 | {pattern: 'main.js', included: true},
16 | // test files.
17 | {pattern: './**/*.test.js'},
18 | // HTML machinima scenes (pre-processed by html2js)
19 | {pattern: 'scenes/*.html'},
20 | // machinima recording files (served at base/recordings/)
21 | {pattern: 'recordings/*.json', included: false, served: true},
22 | // assets
23 | {pattern: 'assets/*.*', included: false, served: true}
24 | ],
25 | frameworks: ['mocha', 'sinon-chai', 'browserify'],
26 | preprocessors: {
27 | 'main.js': ['browserify'],
28 | './**/*.js': ['browserify'],
29 | // process machinima scene files into window.__html__ array
30 | 'scenes/*.html': ['html2js']
31 | },
32 | reporters: ['mocha'],
33 | // machinima: make scene html available
34 | html2JsPreprocessor: {
35 | stripPrefix: 'scenes/'
36 | }
37 | }
38 |
39 | // Apply configuration
40 | module.exports = function (config) {
41 | config.set(karmaConf)
42 | }
43 |
--------------------------------------------------------------------------------
/machinima_tests/main.js:
--------------------------------------------------------------------------------
1 | window.debug = true
2 | // include all dependencies via require - don't use script tags in scenes
3 | require('aframe')
4 | require('aframe-motion-capture-components')
5 | require('aframe-physics-system')
6 | require('super-hands')
7 | require('aframe-environment-component')
8 | // require your package entry point:
9 | require('../index.js')
10 |
--------------------------------------------------------------------------------
/machinima_tests/scenes/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Scene Files
5 |
6 |
7 | scene.html
8 |
9 |
10 |
--------------------------------------------------------------------------------
/machinima_tests/scenes/scene.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A-Frame Physics Extras Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
35 |
36 |
38 |
39 |
40 |
42 |
43 |
45 |
46 |
47 |
50 |
51 |
54 |
55 |
56 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/machinima_tests/scenes/static.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A-Frame Physics Extras Example
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
21 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
39 |
40 |
41 |
44 |
45 |
48 |
49 |
50 |
54 |
55 |
59 |
60 |
61 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/machinima_tests/tests/component.test.js:
--------------------------------------------------------------------------------
1 | /* global assert, process, setup, suite */
2 |
3 | const machinima = require('aframe-machinima-testing')
4 |
5 | suite('basic example scene', function () {
6 | setup(function (done) {
7 | this.timeout(0)
8 | machinima.setupScene('scene.html')
9 | this.scene = document.querySelector('a-scene')
10 | this.scene.addEventListener('loaded', e => {
11 | done()
12 | })
13 | })
14 | machinima.test(
15 | 'basic component function',
16 | 'base/recordings/physics-extras.json',
17 | function () {
18 | const rh = document.getElementById('redHigh').getAttribute('position')
19 | const gh = document.getElementById('greenHigh').getAttribute('position')
20 | const gl = document.getElementById('greenLow').getAttribute('position')
21 | const bhb = document.getElementById('blueHigh').body
22 | assert.isBelow(rh.x, 0, 'Red upper moved left')
23 | assert.isAbove(rh.x, -2, 'Red upper slept')
24 | assert.deepEqual(gh, {x: -1, y: 1.6, z: -1}, 'Green/red collisions filtered')
25 | assert.isAbove(gl.x, 5, 'Green doesnt sleep')
26 | assert.isAbove(bhb.angularVelocity.length(), 5, 'Blue rotation not dampened')
27 | assert.isBelow(bhb.velocity.length(), 1, 'Blue translation is dampened')
28 | }
29 | )
30 | })
31 | suite('static body scene', function () {
32 | setup(function (done) {
33 | machinima.setupScene('static.html')
34 | this.scene = document.querySelector('a-scene')
35 | this.scene.addEventListener('loaded', e => {
36 | done()
37 | })
38 | })
39 | machinima.test(
40 | 'physics-collider detects collisions with static bodies',
41 | 'base/recordings/physics-extras.json',
42 | function () {
43 | const rh = document.getElementById('redHigh').getAttribute('material')
44 | const rl = document.getElementById('redLow').getAttribute('material')
45 | const gh = document.getElementById('greenHigh').getAttribute('material')
46 | const gl = document.getElementById('greenLow').getAttribute('material')
47 | const bh = document.getElementById('blueHigh').getAttribute('material')
48 | const bl = document.getElementById('blueLow').getAttribute('material')
49 | assert.isTrue(rh.transparent, 'red high clicked')
50 | assert.isTrue(rl.transparent, 'red low clicked')
51 | assert.isTrue(gl.transparent, 'green low clicked')
52 | assert.isTrue(bh.transparent, 'blue high clicked')
53 | assert.isFalse(bl.transparent, 'blue low not clicked')
54 | assert.isFalse(gh.transparent, 'green high not clicked')
55 | }
56 | )
57 | })
58 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aframe-physics-extras",
3 | "version": "0.1.3",
4 | "description": "Cannon API interface components the A-Frame Physics System.",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "browserify examples/main.js -o examples/build.js -t [ babelify ]",
8 | "dev": "budo examples/main.js:build.js --dir examples --port 8000 --live --open",
9 | "dist": "browserify index.js -o dist/aframe-physics-extras.js -t [ babelify ] && cross-env NODE_ENV=production browserify index.js -o dist/aframe-physics-extras.min.js -t [ babelify ]",
10 | "lint": "standard -v | snazzy",
11 | "prepublish": "npm run dist",
12 | "preghpages": "npm run build && shx rm -rf gh-pages && shx mkdir gh-pages && shx cp -r examples/* gh-pages",
13 | "ghpages": "npm run preghpages && ghpages -p gh-pages",
14 | "start": "npm run dev",
15 | "test": "karma start ./tests/karma.conf.js",
16 | "test:ci": "TEST_ENV=ci karma start ./tests/karma.conf.js --single-run --browsers Firefox",
17 | "test:machinima": "karma start ./machinima_tests/karma.conf.js",
18 | "record:machinima": "budo machinima_tests/main.js:build.js --dir machinima_tests/scenes --port 8000 --live --open"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/wmurphyrd/aframe-physics-extras.git"
23 | },
24 | "keywords": [
25 | "aframe",
26 | "aframe-component",
27 | "aframe-vr",
28 | "vr",
29 | "mozvr",
30 | "webvr",
31 | "foo"
32 | ],
33 | "author": "Will Murphy ",
34 | "license": "MIT",
35 | "bugs": {
36 | "url": "https://github.com/wmurphyrd/aframe-physics-extras/issues"
37 | },
38 | "homepage": "https://github.com/wmurphyrd/aframe-physics-extras#readme",
39 | "devDependencies": {
40 | "aframe": "^0.7.0",
41 | "aframe-environment-component": "^1.0.0",
42 | "aframe-machinima-testing": "^0.1.2",
43 | "aframe-motion-capture-components": "git+https://git@github.com/wmurphyrd/aframe-motion-capture-components.git#v0.2.8a",
44 | "aframe-physics-system": "^2.1.0",
45 | "babel-preset-env": "^1.6.0",
46 | "babel-preset-minify": "^0.2.0",
47 | "babelify": "^7.3.0",
48 | "browserify": "^13.0.0",
49 | "budo": "^8.2.2",
50 | "chai": "^4.1.2",
51 | "cross-env": "^5.0.5",
52 | "ghpages": "^0.0.8",
53 | "karma": "^1.7.1",
54 | "karma-browserify": "^5.1.1",
55 | "karma-chrome-launcher": "^2.2.0",
56 | "karma-firefox-launcher": "^1.0.1",
57 | "karma-html2js-preprocessor": "^1.1.0",
58 | "karma-mocha": "^1.3.0",
59 | "karma-mocha-reporter": "^2.1.0",
60 | "karma-sinon-chai": "^1.3.2",
61 | "mocha": "^3.5.3",
62 | "mozilla-download": "^1.1.1",
63 | "randomcolor": "^0.4.4",
64 | "shelljs": "^0.7.0",
65 | "shx": "^0.1.1",
66 | "sinon": "^2.4.1",
67 | "sinon-chai": "^2.14.0",
68 | "snazzy": "^4.0.0",
69 | "standard": "^10.0.3",
70 | "super-hands": "^2.0.2"
71 | },
72 | "standard": {
73 | "ignore": [
74 | "examples/build.js",
75 | "dist/**"
76 | ]
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/readme_files/physics.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wmurphyrd/aframe-physics-extras/47e46cc9177ce4c5aa6cb27ba124e5861928345f/readme_files/physics.gif
--------------------------------------------------------------------------------
/src/body-merger.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME, THREE, CANNON */
2 | AFRAME.registerComponent('body-merger', {
3 | schema: {default: 'static-body'},
4 | init: function () {
5 | const doMerge = evt => {
6 | if (evt.target === this.el) {
7 | this.el.removeEventListener('body-loaded', doMerge)
8 | this.merge()
9 | }
10 | }
11 | if (this.el.body) {
12 | this.merge()
13 | } else {
14 | this.el.addEventListener('body-loaded', doMerge)
15 | }
16 | },
17 | merge: function () {
18 | const body = this.el.body
19 | const tmpMat = new THREE.Matrix4()
20 | const tmpQuat = new THREE.Quaternion()
21 | const tmpPos = new THREE.Vector3()
22 | const tmpScale = new THREE.Vector3(1, 1, 1) // todo: apply worldScale
23 | const offset = new CANNON.Vec3()
24 | const orientation = new CANNON.Quaternion()
25 | for (let child of this.el.childNodes) {
26 | if (!child.body || !child.getAttribute(this.data)) { continue }
27 | child.object3D.updateMatrix()
28 | while (child.body.shapes.length) {
29 | tmpPos.copy(child.body.shapeOffsets.pop())
30 | tmpQuat.copy(child.body.shapeOrientations.pop())
31 | tmpMat.compose(tmpPos, tmpQuat, tmpScale)
32 | tmpMat.multiply(child.object3D.matrix)
33 | tmpMat.decompose(tmpPos, tmpQuat, tmpScale)
34 | offset.copy(tmpPos)
35 | orientation.copy(tmpQuat)
36 | body.addShape(child.body.shapes.pop(), offset, orientation)
37 | }
38 | child.removeAttribute(this.data)
39 | }
40 | }
41 | })
42 |
--------------------------------------------------------------------------------
/src/physics-collider.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | AFRAME.registerComponent('physics-collider', {
3 | schema: {
4 | ignoreSleep: {default: true}
5 | },
6 | init: function () {
7 | this.collisions = new Set()
8 | this.currentCollisions = new Set()
9 | this.newCollisions = []
10 | this.clearedCollisions = []
11 | this.collisionEventDetails = {
12 | els: this.newCollisions,
13 | clearedEls: this.clearedCollisions
14 | }
15 | },
16 | update: function () {
17 | if (this.el.body) {
18 | this.updateBody()
19 | } else {
20 | this.el.addEventListener(
21 | 'body-loaded',
22 | this.updateBody.bind(this),
23 | { once: true }
24 | )
25 | }
26 | },
27 | tick: (function () {
28 | const uppperMask = 0xFFFF0000
29 | const lowerMask = 0x0000FFFF
30 | return function () {
31 | if (!(this.el.body && this.el.body.world)) return
32 | const currentCollisions = this.currentCollisions
33 | const thisBodyId = this.el.body.id
34 | const worldCollisions = this.el.body.world.bodyOverlapKeeper.current
35 | const worldBodyMap = this.el.body.world.idToBodyMap
36 | const collisions = this.collisions
37 | const newCollisions = this.newCollisions
38 | const clearedCollisions = this.clearedCollisions
39 | let i = 0
40 | let upperId = (worldCollisions[i] & uppperMask) >> 16
41 | let target
42 | newCollisions.length = clearedCollisions.length = 0
43 | currentCollisions.clear()
44 | while (i < worldCollisions.length && upperId < thisBodyId) {
45 | if (worldBodyMap[upperId]) {
46 | target = worldBodyMap[upperId].el
47 | if ((worldCollisions[i] & lowerMask) === thisBodyId) {
48 | currentCollisions.add(target)
49 | if (!collisions.has(target)) { newCollisions.push(target) }
50 | }
51 | }
52 | upperId = (worldCollisions[++i] & uppperMask) >> 16
53 | }
54 | while (i < worldCollisions.length && upperId === thisBodyId) {
55 | if (worldBodyMap[worldCollisions[i] & lowerMask]) {
56 | target = worldBodyMap[worldCollisions[i] & lowerMask].el
57 | currentCollisions.add(target)
58 | if (!collisions.has(target)) { newCollisions.push(target) }
59 | }
60 | upperId = (worldCollisions[++i] & uppperMask) >> 16
61 | }
62 |
63 | for (let col of collisions) {
64 | if (!currentCollisions.has(col)) {
65 | clearedCollisions.push(col)
66 | collisions.delete(col)
67 | }
68 | }
69 | for (let col of newCollisions) {
70 | collisions.add(col)
71 | }
72 | if (newCollisions.length || clearedCollisions.length) {
73 | this.el.emit('collisions', this.collisionEventDetails)
74 | }
75 | }
76 | })(),
77 | remove: function () {
78 | if (this.originalSleepConfig) {
79 | AFRAME.utils.extend(this.el.body, this.originalSleepConfig)
80 | }
81 | },
82 | updateBody: function (evt) {
83 | // ignore bubbled 'body-loaded' events
84 | if (evt !== undefined && evt.target !== this.el) { return }
85 | if (this.data.ignoreSleep) {
86 | // ensure sleep doesn't disable collision detection
87 | this.el.body.allowSleep = false
88 | /* naiveBroadphase ignores collisions between sleeping & static bodies */
89 | this.el.body.type = window.CANNON.Body.KINEMATIC
90 | // Kinematics must have velocity >= their sleep limit to wake others
91 | this.el.body.sleepSpeedLimit = 0
92 | } else if (this.originalSleepConfig === undefined) {
93 | this.originalSleepConfig = {
94 | allowSleep: this.el.body.allowSleep,
95 | sleepSpeedLimit: this.el.body.sleepSpeedLimit,
96 | type: this.el.body.type
97 | }
98 | } else {
99 | // restore original settings
100 | AFRAME.utils.extend(this.el.body, this.originalSleepConfig)
101 | }
102 | }
103 | })
104 |
--------------------------------------------------------------------------------
/src/physics-collision-filter.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME */
2 | AFRAME.registerComponent('collision-filter', {
3 | schema: {
4 | group: {default: 'default'},
5 | collidesWith: {default: ['default']},
6 | collisionForces: {default: true}
7 | },
8 | init: function () {
9 | this.updateBodyBound = this.updateBody.bind(this)
10 | this.system.registerMe(this)
11 | this.el.addEventListener('body-loaded', this.updateBodyBound)
12 | },
13 | update: function () {
14 | // register any new groups
15 | this.system.registerMe(this)
16 | if (this.el.body) {
17 | this.updateBody()
18 | }
19 | },
20 | remove: function () {
21 | this.el.removeEventListener('body-loaded', this.updateBodyBound)
22 | },
23 | updateBody: function (evt) {
24 | // ignore bubbled 'body-loaded' events
25 | if (evt !== undefined && evt.target !== this.el) { return }
26 | this.el.body.collisionFilterMask =
27 | this.system.getFilterCode(this.data.collidesWith)
28 | this.el.body.collisionFilterGroup =
29 | this.system.getFilterCode(this.data.group)
30 | this.el.body.collisionResponse = this.data.collisionForces
31 | }
32 | })
33 |
34 | AFRAME.registerSystem('collision-filter', {
35 | schema: {
36 | collisionGroups: {default: ['default']}
37 | },
38 | dependencies: ['physics'],
39 | init: function () {
40 | this.maxGroups = Math.log2(Number.MAX_SAFE_INTEGER)
41 | },
42 | registerMe: function (comp) {
43 | // add any unknown groups to the master list
44 | const newGroups = [comp.data.group, ...comp.data.collidesWith]
45 | .filter(group => this.data.collisionGroups.indexOf(group) === -1)
46 | this.data.collisionGroups.push(...newGroups)
47 | if (this.data.collisionGroups.length > this.maxGroups) {
48 | throw new Error('Too many collision groups')
49 | }
50 | },
51 | getFilterCode: function (elGroups) {
52 | let code = 0
53 | if (!Array.isArray(elGroups)) { elGroups = [elGroups] }
54 | // each group corresponds to a bit which is turned on when matched
55 | // floor negates any unmatched groups (2^-1 = 0.5)
56 | elGroups.forEach(group => {
57 | code += Math.floor(Math.pow(2, this.data.collisionGroups.indexOf(group)))
58 | })
59 | return code
60 | }
61 | })
62 |
--------------------------------------------------------------------------------
/src/physics-sleepy.js:
--------------------------------------------------------------------------------
1 | // Make dynamic bodies idle when not grabbed
2 | /* global AFRAME */
3 | AFRAME.registerComponent('sleepy', {
4 | schema: {
5 | allowSleep: {default: true},
6 | speedLimit: {default: 0.25, type: 'number'},
7 | delay: {default: 0.25, type: 'number'},
8 | linearDamping: {default: 0.99, type: 'number'},
9 | angularDamping: {default: 0.99, type: 'number'},
10 | holdState: {default: 'grabbed'}
11 | },
12 | init: function () {
13 | this.updateBodyBound = this.updateBody.bind(this)
14 | this.holdStateBound = this.holdState.bind(this)
15 | this.resumeStateBound = this.resumeState.bind(this)
16 |
17 | this.el.addEventListener('body-loaded', this.updateBodyBound)
18 | },
19 | update: function () {
20 | if (this.el.body) {
21 | this.updateBody()
22 | }
23 | },
24 | remove: function () {
25 | this.el.removeEventListener('body-loaded', this.updateBodyBound)
26 | this.el.removeEventListener('stateadded', this.holdStateBound)
27 | this.el.removeEventListener('stateremoved', this.resumeStateBound)
28 | },
29 | updateBody: function (evt) {
30 | // ignore bubbled 'body-loaded' events
31 | if (evt !== undefined && evt.target !== this.el) { return }
32 | if (this.data.allowSleep) {
33 | // only "local" driver compatable
34 | try {
35 | this.el.body.world.allowSleep = true
36 | } catch (err) {
37 | console.error('Unable to activate sleep in physics.' +
38 | '`sleepy` requires "local" physics driver')
39 | }
40 | }
41 | this.el.body.allowSleep = this.data.allowSleep
42 | this.el.body.sleepSpeedLimit = this.data.speedLimit
43 | this.el.body.sleepTimeLimit = this.data.delay
44 | this.el.body.linearDamping = this.data.linearDamping
45 | this.el.body.angularDamping = this.data.angularDamping
46 | if (this.data.allowSleep) {
47 | this.el.addEventListener('stateadded', this.holdStateBound)
48 | this.el.addEventListener('stateremoved', this.resumeStateBound)
49 | } else {
50 | this.el.removeEventListener('stateadded', this.holdStateBound)
51 | this.el.removeEventListener('stateremoved', this.resumeStateBound)
52 | }
53 | },
54 | // disble the sleeping during interactions because sleep will break constraints
55 | holdState: function (evt) {
56 | let state = this.data.holdState
57 | // api change in A-Frame v0.8.0
58 | if (evt.detail === state || evt.detail.state === state) {
59 | this.el.body.allowSleep = false
60 | }
61 | },
62 | resumeState: function (evt) {
63 | let state = this.data.holdState
64 | if (evt.detail === state || evt.detail.state === state) {
65 | this.el.body.allowSleep = this.data.allowSleep
66 | }
67 | }
68 |
69 | })
70 |
--------------------------------------------------------------------------------
/tests/__init.test.js:
--------------------------------------------------------------------------------
1 | /* global sinon, setup, teardown */
2 |
3 | /**
4 | * __init.test.js is run before every test case.
5 | */
6 | window.debug = true
7 | var AScene = require('aframe').AScene
8 |
9 | navigator.getVRDisplays = function () {
10 | var resolvePromise = Promise.resolve()
11 | var mockVRDisplay = {
12 | requestPresent: resolvePromise,
13 | exitPresent: resolvePromise,
14 | getPose: function () { return {orientation: null, position: null} },
15 | requestAnimationFrame: function () { return 1 }
16 | }
17 | return Promise.resolve([mockVRDisplay])
18 | }
19 |
20 | setup(function () {
21 | this.sinon = sinon.sandbox.create()
22 | // Stubs to not create a WebGL context since Travis CI runs headless.
23 | this.sinon.stub(AScene.prototype, 'render')
24 | this.sinon.stub(AScene.prototype, 'resize')
25 | this.sinon.stub(AScene.prototype, 'setupRenderer')
26 | })
27 |
28 | teardown(function () {
29 | // Clean up any attached elements.
30 | var attachedEls = ['canvas', 'a-assets', 'a-scene']
31 | var els = document.querySelectorAll(attachedEls.join(','))
32 | for (var i = 0; i < els.length; i++) {
33 | els[i].parentNode.removeChild(els[i])
34 | }
35 | this.sinon.restore()
36 | })
37 |
--------------------------------------------------------------------------------
/tests/components/physics-collider.test.js:
--------------------------------------------------------------------------------
1 | /* global assert, process, setup, suite, test */
2 |
3 | const helpers = require('../helpers')
4 | const entityFactory = helpers.entityFactory
5 |
6 | suite('physics-collider', function () {
7 | setup(function (done) {
8 | var el = this.el = entityFactory()
9 | window.CANNON = {Body: {KINEMATIC: 4}}
10 | el.body = {el: el, id: 2}
11 | this.scene = el.sceneEl
12 | this.el.setAttribute('physics-collider', '')
13 | this.target1 = document.createElement('a-entity')
14 | this.scene.appendChild(this.target1)
15 | this.target1.body = {el: this.target1, id: 1}
16 | this.target2 = document.createElement('a-entity')
17 | this.scene.appendChild(this.target2)
18 | this.target2.body = {el: this.target2, id: 3}
19 | this.scene.addEventListener('loaded', () => {
20 | this.comp = this.el.components['physics-collider']
21 | done()
22 | })
23 | })
24 | suite('lifecyle', function () {
25 | test('component attaches and removes without errors', function (done) {
26 | this.el.removeAttribute('physics-collider')
27 | process.nextTick(done)
28 | })
29 | })
30 | suite('collisions', function () {
31 | test('finds collided entities in contacts array', function () {
32 | const hitSpy = this.sinon.spy()
33 | this.el.addEventListener('collisions', hitSpy)
34 | this.el.body.world = {
35 | bodyOverlapKeeper: {current: [
36 | (this.target1.body.id << 16) + this.el.body.id,
37 | (this.el.body.id << 16) + this.target2.body.id
38 | ]},
39 | idToBodyMap: [undefined, this.target1.body, this.el.body, this.target2.body]
40 | }
41 | this.comp.tick()
42 | assert.isTrue(
43 | hitSpy.calledWithMatch({detail: {els: [this.target1, this.target2]}}),
44 | 'finds new collisions'
45 | )
46 | this.comp.tick()
47 | assert.strictEqual(this.comp.collisions.size, 2, 'ignores duplicates')
48 | this.el.body.world.bodyOverlapKeeper.current.pop()
49 | this.comp.tick()
50 | assert.isTrue(
51 | hitSpy.calledWithMatch({detail: {els: [], clearedEls: [this.target2]}}),
52 | 'clears old collisions and ignores duplicates'
53 | )
54 | assert.strictEqual(this.comp.collisions.size, 1, 'keeps ongoing collisions')
55 | assert.isTrue(this.comp.collisions.has(this.target1), 'keeps ongoing collisions')
56 | })
57 | test('Handles bodies removed while collided', function () {
58 | this.el.body.world = {
59 | bodyOverlapKeeper: {current: [
60 | (this.target1.body.id << 16) + this.el.body.id,
61 | (this.el.body.id << 16) + this.target2.body.id
62 | ]},
63 | idToBodyMap: [undefined, this.target1.body, this.el.body, this.target2.body]
64 | }
65 | this.comp.tick()
66 | this.el.body.world.idToBodyMap[3] = undefined
67 | this.comp.tick()
68 | assert.isFalse(this.comp.collisions.has(this.target2), 'lower loop')
69 | this.el.body.world.idToBodyMap[1] = undefined
70 | this.comp.tick()
71 | assert.isFalse(this.comp.collisions.has(this.target1), 'upper loop')
72 | })
73 | })
74 | })
75 |
--------------------------------------------------------------------------------
/tests/components/physics-collision-filter.test.js:
--------------------------------------------------------------------------------
1 | /* global assert, process, setup, suite, test */
2 |
3 | const helpers = require('../helpers')
4 | const entityFactory = helpers.entityFactory
5 |
6 | suite('collision-filter', function () {
7 | setup(function (done) {
8 | var el = this.el = entityFactory()
9 | el.body = {el: el}
10 | this.scene = el.sceneEl
11 | this.el.setAttribute('collision-filter', '')
12 | this.target1 = document.createElement('a-entity')
13 | this.scene.appendChild(this.target1)
14 | this.target1.setAttribute('collision-filter', '')
15 | this.target2 = document.createElement('a-entity')
16 | this.scene.appendChild(this.target2)
17 | this.target2.setAttribute('collision-filter', '')
18 | this.scene.addEventListener('loaded', () => {
19 | this.comp = this.el.components['collision-filter']
20 | this.system = this.comp.system
21 | done()
22 | })
23 | })
24 | suite('lifecyle', function () {
25 | test('component attaches and removes without errors', function (done) {
26 | this.el.removeAttribute('collision-filter')
27 | process.nextTick(done)
28 | })
29 | })
30 | suite('filter codes', function () {
31 | test('returns unique bit code for each group', function () {
32 | assert.strictEqual(this.system.getFilterCode('default'), 1)
33 | this.el.setAttribute('collision-filter', {group: 'group1'})
34 | this.el.setAttribute('collision-filter', {
35 | group: 'group1',
36 | collidesWith: ['group2', 'group3']
37 | })
38 | assert.strictEqual(this.system.getFilterCode('group1'), 2)
39 | assert.strictEqual(this.system.getFilterCode('group2'), 4)
40 | assert.strictEqual(this.system.getFilterCode('group3'), 8)
41 | })
42 | test('adds filter codes', function () {
43 | this.el.setAttribute('collision-filter', {
44 | collidesWith: ['group1', 'group2']
45 | })
46 | assert.strictEqual(this.system.getFilterCode(['group1', 'group2']), 6)
47 | assert.strictEqual(this.system.getFilterCode(['default', 'group2']), 5)
48 | })
49 | test('sets filter masks on body', function () {
50 | this.el.setAttribute('collision-filter', {group: 'group1'})
51 | this.el.body = {}
52 | this.el.emit('body-loaded')
53 | assert.strictEqual(this.el.body.collisionFilterGroup, 2)
54 | this.el.setAttribute('collision-filter', {
55 | collidesWith: ['group2', 'group3']
56 | })
57 | assert.strictEqual(this.el.body.collisionFilterMask, 12)
58 | })
59 | })
60 | suite('settings', function () {
61 | test('collisionForces can be disabled', function () {
62 | assert.isTrue(this.el.body.collisionResponse)
63 | this.el.setAttribute('collision-filter', {collisionForces: false})
64 | assert.isFalse(this.el.body.collisionResponse)
65 | })
66 | })
67 | })
68 |
--------------------------------------------------------------------------------
/tests/components/physics-sleepy.test.js:
--------------------------------------------------------------------------------
1 | /* global assert, process, setup, suite, test */
2 |
3 | const helpers = require('../helpers')
4 | const entityFactory = helpers.entityFactory
5 |
6 | suite('sleepy', function () {
7 | setup(function (done) {
8 | var el = this.el = entityFactory()
9 | this.scene = el.sceneEl
10 | this.el.setAttribute('sleepy', '')
11 | this.scene.addEventListener('loaded', () => {
12 | this.comp = this.el.components['sleepy']
13 | done()
14 | })
15 | })
16 | suite('lifecyle', function () {
17 | test('component attaches and removes without errors', function (done) {
18 | this.el.removeAttribute('sleepy')
19 | process.nextTick(done)
20 | })
21 | })
22 | suite('applies settings', function () {
23 | test('initial settings applied to body loaded later', function () {
24 | this.el.body = {world: {}}
25 | this.el.emit('body-loaded')
26 | assert.isTrue(this.el.body.allowSleep)
27 | assert.isTrue(this.el.body.world.allowSleep)
28 | assert.strictEqual(this.el.body.sleepSpeedLimit, 0.25)
29 | assert.strictEqual(this.el.body.sleepTimeLimit, 0.25)
30 | assert.strictEqual(this.el.body.linearDamping, 0.99)
31 | assert.strictEqual(this.el.body.angularDamping, 0.99)
32 | })
33 | test('updates applied to existing body', function () {
34 | this.el.body = {world: {}}
35 | this.el.setAttribute('sleepy', {
36 | allowSleep: false,
37 | speedLimit: 1,
38 | delay: 1,
39 | linearDamping: 0,
40 | angularDamping: 0
41 | })
42 | assert.strictEqual(this.el.body.sleepSpeedLimit, 1)
43 | assert.strictEqual(this.el.body.sleepTimeLimit, 1)
44 | assert.strictEqual(this.el.body.linearDamping, 0)
45 | assert.strictEqual(this.el.body.angularDamping, 0)
46 | assert.isFalse(this.el.body.allowSleep)
47 | })
48 | })
49 | suite('hold state', function () {
50 | test('turns sleep on and off with grabbed state', function () {
51 | this.el.body = {world: {}}
52 | this.el.emit('body-loaded')
53 | assert.isTrue(this.el.body.allowSleep)
54 | this.el.emit('stateadded', {state: 'grabbed'})
55 | assert.isFalse(this.el.body.allowSleep)
56 | this.el.emit('stateremoved', {state: 'grabbed'})
57 | assert.isTrue(this.el.body.allowSleep)
58 | })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/tests/helpers.js:
--------------------------------------------------------------------------------
1 | /* global suite */
2 |
3 | /**
4 | * Helper method to create a scene, create an entity, add entity to scene,
5 | * add scene to document.
6 | *
7 | * @returns {object} An `` element.
8 | */
9 | module.exports.entityFactory = function (opts, usePhysics) {
10 | var scene = document.createElement('a-scene')
11 | var assets = document.createElement('a-assets')
12 | var entity = document.createElement('a-entity')
13 | scene.appendChild(assets)
14 | scene.appendChild(entity)
15 | if (usePhysics) { scene.setAttribute('physics', '') }
16 | opts = opts || {}
17 |
18 | if (opts.assets) {
19 | opts.assets.forEach(function (asset) {
20 | assets.appendChild(asset)
21 | })
22 | }
23 |
24 | document.body.appendChild(scene)
25 | // convenience link to scene because new entities in FF don't get .sceneEl until loaded
26 | entity.sceneEl = scene
27 | return entity
28 | }
29 |
30 | /**
31 | * Creates and attaches a mixin element (and an `` element if necessary).
32 | *
33 | * @param {string} id - ID of mixin.
34 | * @param {object} obj - Map of component names to attribute values.
35 | * @param {Element} scene - Indicate which scene to apply mixin to if necessary.
36 | * @returns {object} An attached `` element.
37 | */
38 | module.exports.mixinFactory = function (id, obj, scene) {
39 | var mixinEl = document.createElement('a-mixin')
40 | mixinEl.setAttribute('id', id)
41 | Object.keys(obj).forEach(function (componentName) {
42 | mixinEl.setAttribute(componentName, obj[componentName])
43 | })
44 |
45 | var assetsEl = scene ? scene.querySelector('a-assets') : document.querySelector('a-assets')
46 | assetsEl.appendChild(mixinEl)
47 |
48 | return mixinEl
49 | }
50 |
51 | /**
52 | * Test that is only run locally and is skipped on CI.
53 | */
54 | module.exports.getSkipCISuite = function () {
55 | if (window.__env__.TEST_ENV === 'ci') {
56 | return suite.skip
57 | } else {
58 | return suite
59 | }
60 | }
61 |
62 | /**
63 | * Creates and attaches a hand controller entity with a control component
64 | *
65 | * @param {object} comps - Map of component names to attribute values.
66 | * @param {Element} scene - Indicate which scene to apply mixin to if necessary.
67 | * @returns {bool} controllerOverride - Set true if comps already contains a controller component and does not need the default added.
68 | */
69 | module.exports.controllerFactory = function (comps, controllerOverride, scene) {
70 | var contrEl = document.createElement('a-entity')
71 | comps = comps || {}
72 | if (!controllerOverride) {
73 | comps['vive-controls'] = 'hand: right'
74 | }
75 | Object.keys(comps).forEach(function (componentName) {
76 | contrEl.setAttribute(componentName, comps[componentName])
77 | })
78 | scene = scene || document.querySelector('a-scene')
79 | scene.appendChild(contrEl)
80 | return contrEl
81 | }
82 |
83 | module.exports.emitCancelable = function (target, name, detail) {
84 | const data = {bubbles: true, cancelable: true, detail: detail || {}}
85 | let evt
86 | data.detail.target = data.detail.target || target
87 | evt = new window.CustomEvent(name, data)
88 | return target.dispatchEvent(evt)
89 | }
90 |
--------------------------------------------------------------------------------
/tests/karma.conf.js:
--------------------------------------------------------------------------------
1 | // karma configuration
2 | var karmaConf = {
3 | basePath: '../',
4 | browserify: {
5 | debug: true // ,
6 | // transform: [
7 | // ['babelify', {presets: ['es2015']}]
8 | // ]
9 | },
10 | browsers: ['Chrome', 'Firefox'],
11 | // browsers: ['FirefoxNightly', 'Chromium_WebVR'],
12 | client: {
13 | captureConsole: true,
14 | mocha: {'ui': 'tdd'}
15 | },
16 | customLaunchers: {
17 | Chromium_WebVR: {
18 | base: 'Chromium',
19 | flags: ['--enable-webvr', '--enable-gamepad-extensions']
20 | }
21 | },
22 | envPreprocessor: [
23 | 'TEST_ENV'
24 | ],
25 | files: [
26 | // dependencies
27 | {pattern: 'tests/testDependencies.js', included: true},
28 | // module
29 | {pattern: 'index.js', included: true},
30 | // Define test files.
31 | {pattern: 'tests/**/*.test.js'}
32 | // Serve test assets.
33 | // {pattern: 'tests/assets/**/*', included: false, served: true}
34 | ],
35 | frameworks: ['mocha', 'sinon-chai', 'browserify'],
36 | preprocessors: {
37 | 'tests/testDependencies.js': ['browserify'],
38 | 'index.js': ['browserify'],
39 | 'tests/**/*.js': ['browserify']
40 | },
41 | reporters: ['mocha']
42 | }
43 |
44 | // Apply configuration
45 | module.exports = function (config) {
46 | config.set(karmaConf)
47 | }
48 |
--------------------------------------------------------------------------------
/tests/testDependencies.js:
--------------------------------------------------------------------------------
1 | window.debug = true
2 | require('aframe')
3 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | module: {
3 | rules: [{
4 | test: /\.js$/,
5 | exclude: function (modulePath) {
6 | return /node_modules/.test(modulePath) &&
7 | !/src.node_modules/.test(modulePath)
8 | },
9 | use: [
10 | {
11 | loader: 'babel-loader'
12 | }
13 | ]
14 | }]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------