├── .gitignore
├── package.json
├── server.js
├── LICENSE
├── README.md
├── lib
└── fast-sync.js
└── dist
└── fast-sync-component.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | demos/
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fast-sync",
3 | "version": "1.2.0",
4 | "description": "A-Frame component for Syncing over Web Sockets",
5 | "main": "./lib/fast-sync.js",
6 | "dependencies": {
7 | "eslint": "^4.1.1",
8 | "express": "^4.15.3",
9 | "uws": "^8.14.0"
10 | },
11 | "devDependencies": {},
12 | "scripts": {
13 | "start": "node server.js"
14 | },
15 | "author": "Ada Rose Edwards",
16 | "license": "MIT",
17 | "prettier": {
18 | "useTabs": true,
19 | "singleQuote": true,
20 | "bracketSpacing": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-env es6 */
2 | /* eslint no-console: 0 */
3 | 'use strict';
4 |
5 | const server = require('http').createServer();
6 | const express = require('express');
7 | const app = express();
8 | const port = process.env.PORT || 3000;
9 | const fastSync = require('./');
10 |
11 | // Options from https://github.com/websockets/ws/blob/master/doc/ws.md
12 | // Set up the WebSocket Server;
13 | const wss = fastSync(server, {
14 | path: '/fast-sync/',
15 | debug: true
16 | });
17 |
18 | // Make the client side script available on /fast-sync/
19 | app.use(wss.dist);
20 |
21 | app.use(express.static('demos', {
22 | maxAge: 3600 * 1000 * 24
23 | }));
24 |
25 | server.on('request', app);
26 |
27 | server.listen(port, function () {
28 | console.log('Listening on ' + server.address().port)
29 | });
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Samsung Internet Dev Rel
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 fast-sync
2 |
3 | Used to efficiently sync the position and rotation of 100s of A-Frame objects at 30fps, written to be as fast and light as possible.
4 |
5 | # Usage
6 |
7 | Requires a Node server,
8 |
9 | ```
10 | npm install --save fast-sync
11 | ```
12 |
13 | ## In your server:
14 |
15 | There is a complete example in [the demo](https://glitch.com/edit/#!/fast-sync?path=server.js:1:0)
16 |
17 | ```
18 | const server = require('http').createServer();
19 |
20 | // Options from https://github.com/websockets/ws/blob/master/doc/ws.md
21 | // Set up the WebSocket Server;
22 | const wss = fastSync(server, {
23 | path: '/fast-sync/',
24 | debug: true
25 | });
26 | ```
27 |
28 | ## On the client
29 |
30 | Include the dist file in your client:
31 |
32 | ```
33 |
34 |
35 |
36 |
37 | ```
38 | ## Configure the aframe system:
39 |
40 | By default it uses the room 'demo' and the url as the url of the page + '/fast-sync/'
41 |
42 | ```
43 |
44 | ...
45 |
46 |
47 | ```
48 |
49 | ## Simple syncing
50 |
51 | When it is initiated it will clone itself onto any remote users with the following components copied:
52 |
53 | material, color, shadow, id, class, geometry, scale
54 |
55 | As well as any components defined in components.
56 |
57 | ```
58 |
59 |
60 |
61 | ```
62 |
63 | 30 times a second it will sync it's position with the server.
64 |
--------------------------------------------------------------------------------
/lib/fast-sync.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Refactor as a node module.
3 | */
4 |
5 | /* eslint-env es6 */
6 | /* eslint no-console: 0 */
7 | 'use strict';
8 |
9 | const WebSocketServer = require('uws').Server;
10 | const SYNC_INTERVAL = 1000 / 30;
11 | let wsServers = new Map();
12 | const extend = require('util')._extend;
13 | const express = require('express');
14 | const path = require('path');
15 |
16 | // 0th entry is always filled
17 | let ids = [true];
18 |
19 | function wsHandleErr(e) {
20 | if (e) {
21 | console.log('Fast-Sync error:' + Date.now() + ' ' + e.message);
22 | }
23 | }
24 |
25 | function getWebSocketServer(server, options) {
26 | const rooms = {};
27 | if (wsServers.has(server)) {
28 | return wsServers.get(server);
29 | }
30 | const wss = new WebSocketServer(extend({ server: server }, options));
31 | wsServers.set(server, wss);
32 | wss.on('connection', function connection(ws) {
33 | let id = ids.indexOf(false);
34 | if (id === -1) {
35 | id = ids.push(true) - 1;
36 | }
37 | ws.id = id;
38 | ws._size = 0;
39 |
40 | ws.on('close', function close() {
41 | const roomies = [];
42 | wss.clients.forEach(
43 | function(ws) {
44 | if (ws._room === this._room) roomies.push(ws);
45 | }.bind(this)
46 | );
47 |
48 | const roomiesIds = roomies.map(ws => ws.id);
49 |
50 | roomies.forEach(function(ws) {
51 | ws.send(JSON.stringify(['UPDATE_USERS', roomiesIds]), wsHandleErr);
52 | });
53 |
54 | if (!!options.debug)
55 | console.log('user with id', id, 'left room', ws._room);
56 | if (rooms[ws._room]) rooms[ws._room].removeWs(ws);
57 | });
58 |
59 | ws.on('message', function incoming(message) {
60 | // Rebroadcast any string messages
61 | if (typeof message === 'string') {
62 | if (message === '__ping__') {
63 | return ws.send('__pong__', wsHandleErr);
64 | }
65 | let data;
66 | try {
67 | data = JSON.parse(message);
68 | } catch (e) {
69 | if (!!options.debug) console.log('INVALID JSON:' + message);
70 | return;
71 | }
72 | if (data[0] === 'HANDSHAKE') {
73 | if (!!options.debug)
74 | console.log('user with id', id, 'joined room', data[1]);
75 |
76 | ws._room = data[1];
77 | const room = rooms[ws._room] || new Room(options.debug);
78 | rooms[ws._room] = room;
79 | if (!room.hasWs()) {
80 | room.addWs(ws);
81 | }
82 |
83 | const roomies = [];
84 | wss.clients.forEach(function(ws) {
85 | if (ws._room === data[1]) roomies.push(ws);
86 | });
87 |
88 | const roomiesIds = roomies.map(ws => ws.id);
89 | roomiesIds.push(id);
90 | roomies.forEach(function(ws) {
91 | ws.send(JSON.stringify(['UPDATE_USERS', roomiesIds]), wsHandleErr);
92 | });
93 |
94 | return;
95 | }
96 |
97 | // By default rebroadcast
98 |
99 | // send to specific user
100 | if (data.length === 3) {
101 | message = JSON.stringify([data[0], id, data[2]]);
102 | wss.clients.forEach(function(otherWs) {
103 | otherWs.send(message);
104 | });
105 | return;
106 | }
107 |
108 | // send to everyone
109 | if (data.length === 2) {
110 | message = JSON.stringify([data[0], id, data[1]]);
111 | wss.clients.forEach(function(otherWs) {
112 | otherWs.send(message);
113 | });
114 | return;
115 | }
116 | } else {
117 | if (!ws._room) return;
118 |
119 | const room = rooms[ws._room];
120 |
121 | // if the size of the data from this ws grows then update it
122 | if (ws._size < message.byteLength) {
123 | ws._size = message.byteLength;
124 | room.updateSize();
125 | }
126 |
127 | room.set(ws, Buffer.from(message));
128 | }
129 | });
130 |
131 | ws.isAlive = true;
132 | ws.on('pong', heartbeat);
133 |
134 | ws.send(JSON.stringify(['HANDSHAKE', id]), wsHandleErr);
135 | });
136 |
137 | setInterval(function ping() {
138 | wss.clients.forEach(function each(ws) {
139 | if (ws.isAlive === false) {
140 | return ws.terminate();
141 | }
142 |
143 | ws.isAlive = false;
144 | ws.ping('', false, true);
145 | });
146 | }, 5000);
147 |
148 | setInterval(function() {
149 | const roomKeys = Object.keys(rooms);
150 | for (const roomKey of roomKeys) {
151 | const room = rooms[roomKey];
152 | if (room._clean) continue;
153 | for (const ws of room.webSockets) {
154 | ws.send(new Buffer(room._buffer.buffer, room._start), function() {
155 | room.clean();
156 | });
157 | }
158 | }
159 | }, SYNC_INTERVAL);
160 |
161 | wss.dist = express();
162 |
163 | wss.dist.use(
164 | options.path,
165 | express.static(path.resolve(path.join(__dirname, '../', 'dist')))
166 | );
167 |
168 | return wss;
169 | }
170 |
171 | /*
172 | * Used to maintain a Buffer for the room which grows as needed
173 | */
174 | class Room {
175 | constructor(debug) {
176 | this.webSockets = [];
177 | this._buffer = Buffer.alloc(1);
178 | this.clean();
179 | this.debug = !!debug;
180 | }
181 |
182 | hasWs(ws) {
183 | return this.webSockets.indexOf(ws) !== -1;
184 | }
185 |
186 | addWs(ws) {
187 | if (this.webSockets.indexOf(ws) === -1) {
188 | this.webSockets.push(ws);
189 | this.updateSize();
190 | }
191 | }
192 |
193 | removeWs(ws) {
194 | if (this.webSockets.indexOf(ws) !== -1) {
195 | // Pull out the old websockets and filter to remove the one that left
196 | // Then put them back
197 | const old = this.webSockets.splice(0).filter(testWs => testWs !== ws);
198 | this.webSockets.push(...old);
199 | this.updateSize();
200 | }
201 | }
202 |
203 | updateSize() {
204 | this._size = this.webSockets.reduce((a, b) => a + b._size, 0);
205 | this._buffer = Buffer.alloc(this._size);
206 | if (this.debug) console.log('Buffer Update', this._size);
207 | this.clean();
208 | }
209 |
210 | clean() {
211 | this._buffer.fill(0);
212 | this._clean = true;
213 | this._start = Infinity;
214 | }
215 |
216 | set(ws, data) {
217 | let offset = 0;
218 | for (let i = 0; i < this.webSockets.length; i++) {
219 | const testWs = this.webSockets[i];
220 | if (ws === testWs) break;
221 | offset += testWs._size;
222 | }
223 | if (this._start > offset) this._start = offset;
224 | this._clean = false;
225 | data.copy(this._buffer, offset);
226 | }
227 | }
228 |
229 | function heartbeat() {
230 | this.isAlive = true;
231 | }
232 |
233 | module.exports = getWebSocketServer;
234 |
--------------------------------------------------------------------------------
/dist/fast-sync-component.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | /* global AFRAME, Promise, Uint32Array, Map, Set, THREE, Float32Array */
3 | /* eslint no-var: 0 */
4 |
5 | var localObjectTracker = [];
6 | var connectedUsersIds = new Set();
7 | var isLocal =
8 | location.hostname === 'localhost' || location.hostname === '127.0.0.1';
9 | var radToDeg = 180 / Math.PI;
10 |
11 | // Connection opened
12 | function getNewWS(url, callbacks) {
13 | return new Promise(function(resolve, reject) {
14 | var interval = -1;
15 | var ws;
16 | try {
17 | ws = new WebSocket(url);
18 | } catch (e) {
19 | return reject(e);
20 | }
21 | ws.binaryType = 'arraybuffer';
22 |
23 | ws.isAlive = true;
24 |
25 | ws.addEventListener('message', function m(e) {
26 | if (typeof e.data === 'string') {
27 | if (e.data === '__pong__') {
28 | ws.isAlive = true;
29 | return;
30 | }
31 | var data = JSON.parse(e.data);
32 | if (ws.id === undefined && data[0] === 'HANDSHAKE') {
33 | ws.id = data[1];
34 | return resolve(ws);
35 | }
36 | if (data[0] === 'UPDATE_USERS') {
37 | var newUsers = new Set(data[1]);
38 | newUsers.delete(ws.id);
39 | newUsers.forEach(function(id) {
40 | if (!connectedUsersIds.has(id)) {
41 | connectedUsersIds.add(id);
42 | callbacks.userJoinCallback(ws, id);
43 | }
44 | });
45 | Array.from(connectedUsersIds).forEach(function(id) {
46 | if (!newUsers.has(id)) {
47 | callbacks.userLeaveCallback(id);
48 | connectedUsersIds.delete(ws, id);
49 | }
50 | });
51 | return;
52 | }
53 | if (data[0] === 'UPDATE_REMOTE_EL') {
54 | if (data[1] === ws.id) return;
55 | callbacks.createForeignEl(ws, data[1], data[2]);
56 | return;
57 | }
58 | if (data[0] === 'REMOVE_REMOTE_EL') {
59 | if (data[1] === ws.id) return;
60 | callbacks.removeForeignEl(ws, data[1], data[2]);
61 | return;
62 | }
63 | if (data[0] === 'UPDATE_HTML') {
64 | if (data[1] === ws.id) return;
65 | callbacks.onUpdateHTML(ws, data[1], data[2]);
66 | return;
67 | }
68 | if (data[0] === 'STEAL_EL') {
69 | callbacks.stealEl(ws, data[1], data[2]);
70 | return;
71 | }
72 | } else {
73 | var temp = new Uint32Array(e.data);
74 | callbacks.messageCallback(ws, temp);
75 | }
76 | });
77 |
78 | ws.addEventListener('close', terminate);
79 |
80 | ws.addEventListener('open', function firstOpen() {
81 | /* eslint-disable no-console */
82 | console.log('Connected to the server...');
83 |
84 | interval = setInterval(function ping() {
85 | if (ws.isAlive === false) {
86 | console.log('Timeout...');
87 | terminate();
88 | }
89 | ws.isAlive = false;
90 | ws.send('__ping__');
91 | }, 3000);
92 |
93 | ws.removeEventListener('open', firstOpen);
94 | /* eslint-enable no-console */
95 | });
96 |
97 | function terminate() {
98 | ws.close();
99 | clearInterval(interval);
100 | }
101 | });
102 | }
103 |
104 | function checkForSyncId(el) {
105 | if (el.components['fast-sync'].syncId === undefined) return false;
106 | return true;
107 | }
108 |
109 | function checkSyncDataChanged(el) {
110 | var syncDataObj = el.components['fast-sync'].getSyncData();
111 | var oldData = el._cachedSyncData;
112 | var dirty = false;
113 | var oldValue;
114 | oldValue = oldData[0];
115 | oldData[0] = syncDataObj.rotation.x;
116 | if (oldData[0] !== oldValue) dirty = true;
117 |
118 | oldValue = oldData[1];
119 | oldData[1] = syncDataObj.rotation.y;
120 | if (oldData[1] !== oldValue) dirty = true;
121 |
122 | oldValue = oldData[2];
123 | oldData[2] = syncDataObj.rotation.z;
124 | if (oldData[2] !== oldValue) dirty = true;
125 |
126 | oldValue = oldData[3];
127 | oldData[3] = syncDataObj.position.x;
128 | if (oldData[3] !== oldValue) dirty = true;
129 |
130 | oldValue = oldData[4];
131 | oldData[4] = syncDataObj.position.y;
132 | if (oldData[4] !== oldValue) dirty = true;
133 |
134 | oldValue = oldData[5];
135 | oldData[5] = syncDataObj.position.z;
136 | if (oldData[5] !== oldValue) dirty = true;
137 |
138 | return dirty;
139 | }
140 |
141 | AFRAME.registerSystem('fast-sync-controller', {
142 | schema: {
143 | room: {
144 | default: 'demo'
145 | },
146 | url: {
147 | default: (isLocal ? 'ws://' : 'wss://') + location.host + '/fast-sync/'
148 | }
149 | },
150 | init: function() {
151 | this.objects = new Map();
152 | this.foreignObjects = new Map();
153 | this._wsPromise = getNewWS(this.data.url, {
154 | messageCallback: this.onbinary.bind(this),
155 | createForeignEl: this.onupdate.bind(this),
156 | removeForeignEl: this.onremove.bind(this),
157 | userJoinCallback: this.onuserjoin.bind(this),
158 | onUpdateHTML: this.onUpdateHTML.bind(this),
159 | stealEl: this.onstealel.bind(this),
160 | userLeaveCallback: function(id) {
161 | /* eslint-disable no-console */
162 |
163 | // Find and clean up any elements belonging to that user.
164 | var els = Array.from(
165 | this.el.querySelectorAll(
166 | '[fast-sync-listener^="original-creator: ' + id + ';"]'
167 | )
168 | );
169 | els.forEach(function(el) {
170 | el.parentNode.removeChild(el);
171 | });
172 |
173 | this.foreignObjects.delete(id);
174 |
175 | this.el.emit('userleave', {
176 | id: id,
177 | count: this.foreignObjects.size
178 | });
179 |
180 | console.log('User left', id);
181 | /* eslint-enable no-console */
182 | }.bind(this)
183 | });
184 | this.getWs().then(ws => {
185 | ws.send(JSON.stringify(['HANDSHAKE', this.data.room]));
186 | this._ws = ws;
187 | });
188 | this.tick = AFRAME.utils.throttleTick(this.throttledTick, 1000 / 30, this);
189 | },
190 | throttledTick: function() {
191 | if (!this._ws) return;
192 | var toSerial = Array.from(this.objects.values()).filter(checkForSyncId);
193 | var filtered = this.forceSync
194 | ? toSerial
195 | : toSerial.filter(checkSyncDataChanged);
196 | this.forceSync = false;
197 | var count = toSerial.length;
198 | if (!filtered.length) {
199 | return;
200 | }
201 | var bindata = new Uint32Array(2 + 7 * count);
202 | var index = 2;
203 | bindata[0] = this._ws.id;
204 | bindata[1] = count;
205 |
206 | toSerial.forEach(function(el) {
207 | var accessFloatAsInt = new Uint32Array(el._cachedSyncData.buffer);
208 | bindata[0 + index] = el.components['fast-sync'].syncId;
209 | bindata[1 + index] = accessFloatAsInt[0];
210 | bindata[2 + index] = accessFloatAsInt[1];
211 | bindata[3 + index] = accessFloatAsInt[2];
212 | bindata[4 + index] = accessFloatAsInt[3];
213 | bindata[5 + index] = accessFloatAsInt[4];
214 | bindata[6 + index] = accessFloatAsInt[5];
215 | index += 7;
216 | });
217 |
218 | this._ws.send(bindata);
219 | },
220 | onuserjoin: function(ws, id) {
221 | /* eslint-disable */
222 | console.log('User joined: ' + ws.id, id, location.pathname);
223 | /*eslint-enable */
224 | // Update newly joined user
225 | Array.from(this.objects.values()).forEach(function(el) {
226 | if (ws.id !== id)
227 | el.components['fast-sync']
228 | .getSyncTemplate(ws.id)
229 | .then(function(template) {
230 | ws.send(JSON.stringify(['UPDATE_REMOTE_EL', id, template]));
231 | });
232 | });
233 |
234 | this.el.emit('userjoin', {
235 | id: id,
236 | count: this.foreignObjects.size
237 | });
238 |
239 | // Make sure the new user gets the position and rotation information too
240 | this.forceSync = true;
241 | },
242 | onbinary: function(ws, message) {
243 | var index = 0;
244 | while (index < message.length) {
245 | var id = message[index];
246 |
247 | // Skip long sections of zeros
248 | if (id === 0) while (id === 0) id = message[++index];
249 |
250 | var count = message[index + 1];
251 |
252 | // skip self
253 | if (id === ws.id) {
254 | index += 2 + count * 7;
255 | continue;
256 | }
257 |
258 | if (count > 1024) {
259 | // Throw away the data.
260 | throw Error('Something probably went wrong');
261 | }
262 |
263 | // iterate over all the data
264 | while (count--) {
265 | var syncId = message[index + 2];
266 | if (this.foreignObjects.has(id + ',' + syncId)) {
267 | // sync rotation and position
268 | var el = this.foreignObjects.get(id + ',' + syncId);
269 | var accessIntAsFloat = new Float32Array(
270 | message.buffer,
271 | (index + 3) * 4,
272 | 6
273 | );
274 | el.setAttribute('rotation', {
275 | x: accessIntAsFloat[0],
276 | y: accessIntAsFloat[1],
277 | z: accessIntAsFloat[2]
278 | });
279 | el.setAttribute('position', {
280 | x: accessIntAsFloat[3],
281 | y: accessIntAsFloat[4],
282 | z: accessIntAsFloat[5]
283 | });
284 | }
285 | index += 7;
286 | }
287 | index += 2;
288 | }
289 | },
290 | onupdate: function(ws, id, details) {
291 | if (id === ws.id) return;
292 | var fOId = id + ',' + details.syncId;
293 | var el;
294 | if (details.was) {
295 | var wasFOId = details.was.originalCreator + ',' + details.was.syncId;
296 | var el = this.foreignObjects.get(wasFOId);
297 | if (!el)
298 | throw Error('No element with that Id in Foreign Objects', wasFOId);
299 | this.foreignObjects.delete(wasFOId);
300 | this.foreignObjects.set(fOId, el);
301 | }
302 | if (details.html) {
303 | var oldEl = this.foreignObjects.get(fOId);
304 | if (oldEl) oldEl.parentNode.removeChild(oldEl);
305 | this.sceneEl.insertAdjacentHTML('beforeend', details.html);
306 | var el = this.sceneEl.lastElementChild;
307 | this.foreignObjects.set(fOId, el);
308 | }
309 |
310 | Element.prototype.setAttribute.call(
311 | el,
312 | 'fast-sync-listener',
313 | 'original-creator: ' + id + '; sync-id: ' + details.syncId + ';'
314 | );
315 | el._fastSyncConfig = details.config;
316 | el.transferables = details.transferables;
317 | },
318 | onremove: function(ws, id, details) {
319 | var el = this.foreignObjects.get(id + ',' + details.syncId);
320 | el.parentNode.remove(el);
321 | },
322 | onUpdateHTML: function(ws, id, details) {
323 | var el = this.foreignObjects.get(id + ',' + details.syncId);
324 |
325 | if (el) {
326 | el.innerHTML = details.htmlString;
327 | } else {
328 | // If el is not there yet then wait a second to see if it appears.
329 | setTimeout(() => {
330 | var el = this.foreignObjects.get(id + ',' + details.syncId);
331 | el.innerHTML = details.htmlString;
332 | }, 1000);
333 | }
334 | },
335 | onstealel: function(ws, id, o) {
336 | var details = o.idData;
337 | var options = o.options;
338 | var fOId = details.originalCreator + ',' + details.syncId;
339 | if (id === ws.id) {
340 | this.foreignObjects
341 | .get(fOId)
342 | .components['fast-sync-listener']._stealComplete();
343 | this.foreignObjects.delete(fOId);
344 | } else {
345 | if (details.originalCreator === ws.id) {
346 | var el = this.objects.get(details.syncId);
347 |
348 | // My item has been stolen, give it up
349 | el.components['fast-sync'].teardown();
350 | el.removeAttribute('fast-sync');
351 |
352 | if (options.transfer) {
353 | options.transfer.forEach(function(attr) {
354 | el.removeAttribute(attr);
355 | });
356 | }
357 |
358 | // Assign it as a foreign object so it can be found updated later
359 | this.foreignObjects.set(fOId, el);
360 |
361 | el.emit('stolen');
362 | } else {
363 | // Someone's object has been stolen, transfer ownership from old user to new user
364 | }
365 | }
366 | },
367 | register: function(el) {
368 | var id;
369 | return this.getWs()
370 | .then(ws => {
371 | var idIndex = localObjectTracker.includes(false)
372 | ? localObjectTracker.indexOf(false)
373 | : localObjectTracker.length;
374 | localObjectTracker[idIndex] = el;
375 | var actualId = 1024 * ws.id + idIndex;
376 | this.objects.set(actualId, el);
377 | id = actualId;
378 | return actualId;
379 | })
380 | .then(function() {
381 | return id;
382 | });
383 | },
384 | updateEl: function(el) {
385 | this.getWs(ws => {
386 | return el.components['fast-sync']
387 | .getSyncTemplate(ws.id)
388 | .then(function(template) {
389 | ws.send(JSON.stringify(['UPDATE_REMOTE_EL', template]));
390 | });
391 | });
392 | },
393 | stealEl: function(data, options) {
394 | this.getWs().then(ws => {
395 | ws.send(
396 | JSON.stringify([
397 | 'STEAL_EL',
398 | {
399 | idData: data,
400 | options: options
401 | }
402 | ])
403 | );
404 | });
405 | },
406 |
407 | syncHTML: function(syncId, string) {
408 | this.getWs().then(ws => {
409 | ws.send(
410 | JSON.stringify([
411 | 'UPDATE_HTML',
412 | {
413 | syncId: syncId,
414 | htmlString: string
415 | }
416 | ])
417 | );
418 | });
419 | },
420 |
421 | removeEl: function(syncId) {
422 | this.objects.delete(syncId);
423 | this.getWs().then(ws => {
424 | ws.send(
425 | JSON.stringify([
426 | 'REMOVE_REMOTE_EL',
427 | {
428 | syncId: syncId
429 | }
430 | ])
431 | );
432 | });
433 | },
434 | teardownEl: function(syncId) {
435 | this.objects.delete(syncId);
436 | },
437 |
438 | getWs: function getWs(callback) {
439 | // If it's not blocking on the websocket then just return the promise
440 | if (!typeof callback !== 'function') {
441 | return this._wsPromise;
442 | }
443 |
444 | // If it is blocking on the websocket then wait for that promise to resolve and update _wsPromise
445 | this._wsPromise = this._wsPromise.then(
446 | function(ws) {
447 | var maybePromise = callback && callback.bind(this)(ws);
448 | return maybePromise.constructor === Promise
449 | ? maybePromise.then(function() {
450 | return ws;
451 | })
452 | : ws;
453 | }.bind(this)
454 | );
455 | return this._wsPromise;
456 | },
457 |
458 | getId: function() {
459 | return this.getWs().then(function(ws) {
460 | return ws.id;
461 | });
462 | }
463 | });
464 |
465 | AFRAME.registerComponent('fast-sync-listener', {
466 | schema: {
467 | originalCreator: {
468 | type: 'number'
469 | },
470 | syncId: {
471 | type: 'number'
472 | }
473 | },
474 | steal: function(options) {
475 | options = options || {};
476 | if (this.stealPromise) return this.stealPromise;
477 | this.el.sceneEl.systems['fast-sync-controller'].stealEl(
478 | this.data,
479 | options || {}
480 | );
481 | this.stealPromise = new Promise(
482 | function(resolve) {
483 | this.stealResolve = resolve;
484 | }.bind(this)
485 | ).then(
486 | function() {
487 | this.el.removeAttribute('fast-sync-listener');
488 | this.el.setAttribute('fast-sync', this.el._fastSyncConfig);
489 | this.el._fastSyncWas = this.data;
490 | if (options.transfer)
491 | options.transfer.forEach(
492 | function(attr) {
493 | this.el.setAttribute(attr, this.el.transferables[attr]);
494 | }.bind(this)
495 | );
496 | }.bind(this)
497 | );
498 | return this.stealPromise;
499 | },
500 | _stealComplete: function() {
501 | if (this.stealResolve) this.stealResolve();
502 | }
503 | });
504 |
505 | // configuration of the observer:
506 | var config = {
507 | attributes: true,
508 | childList: true,
509 | characterData: true,
510 | subtree: true
511 | };
512 |
513 | AFRAME.registerComponent('fast-sync', {
514 | schema: {
515 | // Instead of copying self, copy another element
516 | copy: {
517 | type: 'selector'
518 | },
519 |
520 | components: {
521 | default: ''
522 | },
523 |
524 | transferables: {
525 | default: ''
526 | },
527 |
528 | world: {
529 | default: false
530 | },
531 |
532 | syncContents: {
533 | default: false
534 | }
535 | },
536 | init: function() {
537 | this.el._cachedSyncData = new Float32Array(6);
538 | this._registerPromise = this.el.sceneEl.systems['fast-sync-controller']
539 | .register(this.el)
540 | .then(
541 | function(syncId) {
542 | this.syncId = syncId;
543 | return syncId;
544 | }.bind(this)
545 | );
546 |
547 | this.syncHTML = '';
548 |
549 | // create an observer instance
550 | this.observer = new MutationObserver(() => {
551 | this.syncHTML = this.el.innerHTML;
552 | });
553 | },
554 | update: function() {
555 | this.observer.disconnect();
556 | if (this.data.syncContents) {
557 | this.observer.observe(this.el, config);
558 | this.syncHTML = this.el.innerHTML;
559 | }
560 | this.el.sceneEl.systems['fast-sync-controller'].updateEl(this.el);
561 | },
562 | tick: function() {
563 | if (this.syncHTML) {
564 | var syncHTML = this.syncHTML;
565 | this.syncHTML = '';
566 |
567 | this._registerPromise.then(
568 | function(syncId) {
569 | this.el.sceneEl.systems['fast-sync-controller'].syncHTML(
570 | syncId,
571 | syncHTML
572 | );
573 | }.bind(this)
574 | );
575 | }
576 | },
577 | getSyncData: (function() {
578 | var converter;
579 | return function() {
580 | if (this.data.world) {
581 | var worldRot = this.el.object3D.getWorldRotation();
582 | worldRot.x *= radToDeg;
583 | worldRot.y *= radToDeg;
584 | worldRot.z *= radToDeg;
585 | return {
586 | position: this.el.object3D.getWorldPosition(),
587 | rotation: worldRot
588 | };
589 | }
590 |
591 | var el = this.el;
592 | var pos = el.components.position.data;
593 | var rot;
594 | if (el.components.quaternion) {
595 | var data = el.components.quaternion.data;
596 | converter = converter || new THREE.Euler();
597 | converter.setFromQuaternion(data, 'YXZ');
598 | converter.x *= radToDeg;
599 | converter.y *= radToDeg;
600 | converter.z *= radToDeg;
601 | rot = converter;
602 | } else {
603 | rot = el.components.rotation.data;
604 | }
605 |
606 | return {
607 | rotation: rot,
608 | position: pos
609 | };
610 | };
611 | })(),
612 | getSyncTemplate: function() {
613 | return this._registerPromise.then(
614 | function(syncId) {
615 | var config = {
616 | components: this.data.components,
617 | transferables: this.data.transferables,
618 | world: this.data.world
619 | };
620 | var components = [
621 | 'material',
622 | 'color',
623 | 'shadow',
624 | 'id',
625 | 'class',
626 | 'geometry',
627 | 'scale'
628 | ].concat(
629 | this.data.components.split(',').map(function(s) {
630 | return s.toLowerCase().trim();
631 | })
632 | );
633 |
634 | var transferables = {};
635 | if (this.data.transferables)
636 | this.data.transferables.split(',').forEach(
637 | function(s) {
638 | var attr = s.toLowerCase().trim();
639 | transferables[attr] = Element.prototype.getAttribute.call(
640 | this.el,
641 | attr
642 | );
643 | }.bind(this)
644 | );
645 |
646 | if (this.el._fastSyncWas) {
647 | var was = this.el._fastSyncWas;
648 | delete this.el._fastSyncWas;
649 | return {
650 | was: was,
651 | syncId: syncId,
652 | config: config,
653 | transferables: transferables
654 | };
655 | }
656 |
657 | var newEl;
658 |
659 | if (this.data.copy !== null) {
660 | newEl = this.data.copy.cloneNode();
661 | } else {
662 | newEl = this.el.cloneNode();
663 | }
664 |
665 | Array.from(newEl.attributes).forEach(function(a) {
666 | if (components.includes(a.name.toLowerCase())) return;
667 | newEl.removeAttribute(a.name);
668 | });
669 |
670 | return {
671 | html: newEl.outerHTML,
672 | syncId: syncId,
673 | config: config,
674 | transferables: transferables
675 | };
676 | }.bind(this)
677 | );
678 | },
679 | remove: function() {
680 | if (this._tornDown === true) return;
681 | this._registerPromise.then(
682 | function(syncId) {
683 | this.el.sceneEl.systems['fast-sync-controller'].removeEl(syncId);
684 | }.bind(this)
685 | );
686 | },
687 | teardown: function() {
688 | this._tornDown = true;
689 | this.observer.disconnect();
690 | this._registerPromise.then(
691 | function(syncId) {
692 | this.el.sceneEl.systems['fast-sync-controller'].teardownEl(syncId);
693 | }.bind(this)
694 | );
695 | }
696 | });
697 |
--------------------------------------------------------------------------------