UNPKG

14.7 kBJavaScriptView Raw
1var Frame = require('./frame')
2 , Hand = require('./hand')
3 , Pointable = require('./pointable')
4 , CircularBuffer = require("./circular_buffer")
5 , Pipeline = require("./pipeline")
6 , EventEmitter = require('events').EventEmitter
7 , gestureListener = require('./gesture').gestureListener
8 , _ = require('underscore');
9
10/**
11 * Constructs a Controller object.
12 *
13 * When creating a Controller object, you may optionally pass in options
14 * to set the host , set the port, enable gestures, or select the frame event type.
15 *
16 * ```javascript
17 * var controller = new Leap.Controller({
18 * host: '127.0.0.1',
19 * port: 6437,
20 * enableGestures: true,
21 * frameEventName: 'animationFrame'
22 * });
23 * ```
24 *
25 * @class Controller
26 * @memberof Leap
27 * @classdesc
28 * The Controller class is your main interface to the Leap Motion Controller.
29 *
30 * Create an instance of this Controller class to access frames of tracking data
31 * and configuration information. Frame data can be polled at any time using the
32 * [Controller.frame]{@link Leap.Controller#frame}() function. Call frame() or frame(0) to get the most recent
33 * frame. Set the history parameter to a positive integer to access previous frames.
34 * A controller stores up to 60 frames in its frame history.
35 *
36 * Polling is an appropriate strategy for applications which already have an
37 * intrinsic update loop, such as a game.
38 */
39
40
41var Controller = module.exports = function(opts) {
42 var inNode = (typeof(process) !== 'undefined' && process.versions && process.versions.node),
43 controller = this;
44
45 opts = _.defaults(opts || {}, {
46 inNode: inNode
47 });
48
49 this.inNode = opts.inNode;
50
51 opts = _.defaults(opts || {}, {
52 frameEventName: this.useAnimationLoop() ? 'animationFrame' : 'deviceFrame',
53 suppressAnimationLoop: !this.useAnimationLoop(),
54 loopWhileDisconnected: false,
55 useAllPlugins: false
56 });
57
58 this.animationFrameRequested = false;
59 this.onAnimationFrame = function() {
60 controller.emit('animationFrame', controller.lastConnectionFrame);
61 if (controller.loopWhileDisconnected && (controller.connection.focusedState || controller.connection.opts.background) ){
62 window.requestAnimationFrame(controller.onAnimationFrame);
63 }else{
64 controller.animationFrameRequested = false;
65 }
66 }
67 this.suppressAnimationLoop = opts.suppressAnimationLoop;
68 this.loopWhileDisconnected = opts.loopWhileDisconnected;
69 this.frameEventName = opts.frameEventName;
70 this.useAllPlugins = opts.useAllPlugins;
71 this.history = new CircularBuffer(200);
72 this.lastFrame = Frame.Invalid;
73 this.lastValidFrame = Frame.Invalid;
74 this.lastConnectionFrame = Frame.Invalid;
75 this.accumulatedGestures = [];
76 if (opts.connectionType === undefined) {
77 this.connectionType = (this.inBrowser() ? require('./connection/browser') : require('./connection/node'));
78 } else {
79 this.connectionType = opts.connectionType;
80 }
81 this.connection = new this.connectionType(opts);
82 this.plugins = {};
83 this._pluginPipelineSteps = {};
84 this._pluginExtendedMethods = {};
85 if (opts.useAllPlugins) this.useRegisteredPlugins();
86 this.setupConnectionEvents();
87}
88
89Controller.prototype.gesture = function(type, cb) {
90 var creator = gestureListener(this, type);
91 if (cb !== undefined) {
92 creator.stop(cb);
93 }
94 return creator;
95}
96
97/*
98 * @returns the controller
99 */
100Controller.prototype.setBackground = function(state) {
101 this.connection.setBackground(state);
102 return this;
103}
104
105Controller.prototype.inBrowser = function() {
106 return !this.inNode;
107}
108
109Controller.prototype.useAnimationLoop = function() {
110 return this.inBrowser() && !this.inBackgroundPage();
111}
112
113Controller.prototype.inBackgroundPage = function(){
114 // http://developer.chrome.com/extensions/extension#method-getBackgroundPage
115 return (typeof(chrome) !== "undefined") &&
116 chrome.extension &&
117 chrome.extension.getBackgroundPage &&
118 (chrome.extension.getBackgroundPage() === window)
119}
120
121/*
122 * @returns the controller
123 */
124Controller.prototype.connect = function() {
125 this.connection.connect();
126 return this;
127}
128
129Controller.prototype.runAnimationLoop = function(){
130 if (!this.suppressAnimationLoop && !this.animationFrameRequested) {
131 this.animationFrameRequested = true;
132 window.requestAnimationFrame(this.onAnimationFrame);
133 }
134}
135
136/*
137 * @returns the controller
138 */
139Controller.prototype.disconnect = function() {
140 this.connection.disconnect();
141 return this;
142}
143
144/**
145 * Returns a frame of tracking data from the Leap.
146 *
147 * Use the optional history parameter to specify which frame to retrieve.
148 * Call frame() or frame(0) to access the most recent frame; call frame(1) to
149 * access the previous frame, and so on. If you use a history value greater
150 * than the number of stored frames, then the controller returns an invalid frame.
151 *
152 * @method frame
153 * @memberof Leap.Controller.prototype
154 * @param {number} history The age of the frame to return, counting backwards from
155 * the most recent frame (0) into the past and up to the maximum age (59).
156 * @returns {Leap.Frame} The specified frame; or, if no history
157 * parameter is specified, the newest frame. If a frame is not available at
158 * the specified history position, an invalid Frame is returned.
159 */
160Controller.prototype.frame = function(num) {
161 return this.history.get(num) || Frame.Invalid;
162}
163
164Controller.prototype.loop = function(callback) {
165 switch (callback.length) {
166 case 1:
167 this.on(this.frameEventName, callback);
168 break;
169 case 2:
170 var controller = this;
171 var scheduler = null;
172 var immediateRunnerCallback = function(frame) {
173 callback(frame, function() {
174 if (controller.lastFrame != frame) {
175 immediateRunnerCallback(controller.lastFrame);
176 } else {
177 controller.once(controller.frameEventName, immediateRunnerCallback);
178 }
179 });
180 }
181 this.once(this.frameEventName, immediateRunnerCallback);
182 break;
183 }
184 return this.connect();
185}
186
187Controller.prototype.addStep = function(step) {
188 if (!this.pipeline) this.pipeline = new Pipeline(this);
189 this.pipeline.addStep(step);
190}
191
192// this is run on every deviceFrame
193Controller.prototype.processFrame = function(frame) {
194 if (frame.gestures) {
195 this.accumulatedGestures = this.accumulatedGestures.concat(frame.gestures);
196 }
197 // lastConnectionFrame is used by the animation loop
198 this.lastConnectionFrame = frame;
199 this.runAnimationLoop();
200 this.emit('deviceFrame', frame);
201}
202
203// on a this.deviceEventName (usually 'animationFrame' in browsers), this emits a 'frame'
204Controller.prototype.processFinishedFrame = function(frame) {
205 this.lastFrame = frame;
206 if (frame.valid) {
207 this.lastValidFrame = frame;
208 }
209 frame.controller = this;
210 frame.historyIdx = this.history.push(frame);
211 if (frame.gestures) {
212 frame.gestures = this.accumulatedGestures;
213 this.accumulatedGestures = [];
214 for (var gestureIdx = 0; gestureIdx != frame.gestures.length; gestureIdx++) {
215 this.emit("gesture", frame.gestures[gestureIdx], frame);
216 }
217 }
218 if (this.pipeline) {
219 frame = this.pipeline.run(frame);
220 if (!frame) frame = Frame.Invalid;
221 }
222 this.emit('frame', frame);
223}
224
225Controller.prototype.setupConnectionEvents = function() {
226 var controller = this;
227 this.connection.on('frame', function(frame) {
228 controller.processFrame(frame);
229 });
230 this.on(this.frameEventName, function(frame) {
231 controller.processFinishedFrame(frame);
232 });
233
234 // Delegate connection events
235 this.connection.on('disconnect', function() { controller.emit('disconnect'); });
236 this.connection.on('ready', function() { controller.emit('ready'); });
237 this.connection.on('connect', function() { controller.emit('connect'); });
238 this.connection.on('focus', function() { controller.emit('focus'); controller.runAnimationLoop(); });
239 this.connection.on('blur', function() { controller.emit('blur') });
240 this.connection.on('protocol', function(protocol) { controller.emit('protocol', protocol); });
241 this.connection.on('deviceConnect', function(evt) { controller.emit(evt.state ? 'deviceConnected' : 'deviceDisconnected'); });
242}
243
244
245Controller._pluginFactories = {};
246
247/*
248 * Registers a plugin, making is accessible to controller.use later on.
249 *
250 * @member plugin
251 * @memberof Leap.Controller.prototype
252 * @param {String} name The name of the plugin (usually camelCase).
253 * @param {function} factory A factory method which will return an instance of a plugin.
254 * The factory receives an optional hash of options, passed in via controller.use.
255 *
256 * Valid keys for the object include frame, hand, finger, tool, and pointable. The value
257 * of each key can be either a function or an object. If given a function, that function
258 * will be called once for every instance of the object, with that instance injected as an
259 * argument. This allows decoration of objects with additional data:
260 *
261 * ```javascript
262 * Leap.Controller.plugin('testPlugin', function(options){
263 * return {
264 * frame: function(frame){
265 * frame.foo = 'bar';
266 * }
267 * }
268 * });
269 * ```
270 *
271 * When hand is used, the callback is called for every hand in `frame.hands`. Note that
272 * hand objects are recreated with every new frame, so that data saved on the hand will not
273 * persist.
274 *
275 * ```javascript
276 * Leap.Controller.plugin('testPlugin', function(){
277 * return {
278 * hand: function(hand){
279 * console.log('testPlugin running on hand ' + hand.id);
280 * }
281 * }
282 * });
283 * ```
284 *
285 * A factory can return an object to add custom functionality to Frames, Hands, or Pointables.
286 * The methods are added directly to the object's prototype. Finger and Tool cannot be used here, Pointable
287 * must be used instead.
288 * This is encouraged for calculations which may not be necessary on every frame.
289 * Memoization is also encouraged, for cases where the method may be called many times per frame by the application.
290 *
291 * ```javascript
292 * // This plugin allows hand.usefulData() to be called later.
293 * Leap.Controller.plugin('testPlugin', function(){
294 * return {
295 * hand: {
296 * usefulData: function(){
297 * console.log('usefulData on hand', this.id);
298 * // memoize the results on to the hand, preventing repeat work:
299 * this.x || this.x = someExpensiveCalculation();
300 * return this.x;
301 * }
302 * }
303 * }
304 * });
305 *
306 * Note that the factory pattern allows encapsulation for every plugin instance.
307 *
308 * ```javascript
309 * Leap.Controller.plugin('testPlugin', function(options){
310 * options || options = {}
311 * options.center || options.center = [0,0,0]
312 *
313 * privatePrintingMethod = function(){
314 * console.log('privatePrintingMethod - options', options);
315 * }
316 *
317 * return {
318 * pointable: {
319 * publicPrintingMethod: function(){
320 * privatePrintingMethod();
321 * }
322 * }
323 * }
324 * });
325 *
326 */
327Controller.plugin = function(pluginName, factory) {
328 if (this._pluginFactories[pluginName]) {
329 throw "Plugin \"" + pluginName + "\" already registered";
330 }
331 return this._pluginFactories[pluginName] = factory;
332};
333
334/*
335 * Returns a list of registered plugins.
336 * @returns {Array} Plugin Factories.
337 */
338Controller.plugins = function() {
339 return _.keys(this._pluginFactories);
340};
341
342/*
343 * Begin using a registered plugin. The plugin's functionality will be added to all frames
344 * returned by the controller (and/or added to the objects within the frame).
345 * - The order of plugin execution inside the loop will match the order in which use is called by the application.
346 * - The plugin be run for both deviceFrames and animationFrames.
347 *
348 * If called a second time, the options will be merged with those of the already instantiated plugin.
349 *
350 * @method use
351 * @memberOf Leap.Controller.prototype
352 * @param pluginName
353 * @param {Hash} Options to be passed to the plugin's factory.
354 * @returns the controller
355 */
356Controller.prototype.use = function(pluginName, options) {
357 var functionOrHash, pluginFactory, key, pluginInstance, klass;
358
359 pluginFactory = (typeof pluginName == 'function') ? pluginName : Controller._pluginFactories[pluginName];
360
361 if (!pluginFactory) {
362 throw 'Leap Plugin ' + pluginName + ' not found.';
363 }
364
365 options || (options = {});
366
367 if (this.plugins[pluginName]){
368 _.extend(this.plugins[pluginName], options)
369 return this;
370 }
371
372 this.plugins[pluginName] = options;
373
374 pluginInstance = pluginFactory.call(this, options);
375
376 for (key in pluginInstance) {
377 functionOrHash = pluginInstance[key];
378
379 if (typeof functionOrHash === 'function') {
380 if (!this.pipeline) this.pipeline = new Pipeline(this);
381 if (!this._pluginPipelineSteps[pluginName]) this._pluginPipelineSteps[pluginName] = [];
382
383 this._pluginPipelineSteps[pluginName].push( this.pipeline.addWrappedStep(key, functionOrHash) );
384 } else {
385 if (!this._pluginExtendedMethods[pluginName]) this._pluginExtendedMethods[pluginName] = [];
386
387 switch (key) {
388 case 'frame':
389 klass = Frame
390 break;
391 case 'hand':
392 klass = Hand
393 break;
394 case 'pointable':
395 klass = Pointable
396 break;
397 default:
398 throw pluginName + ' specifies invalid object type "' + key + '" for prototypical extension'
399 }
400
401 _.extend(klass.prototype, functionOrHash);
402 _.extend(klass.Invalid, functionOrHash);
403 this._pluginExtendedMethods[pluginName].push([klass, functionOrHash])
404 }
405 }
406 return this;
407};
408
409/*
410 * Stop using a used plugin. This will remove any of the plugin's pipeline methods (those called on every frame)
411 * and remove any methods which extend frame-object prototypes.
412 *
413 * @method stopUsing
414 * @memberOf Leap.Controller.prototype
415 * @param pluginName
416 * @returns the controller
417 */
418Controller.prototype.stopUsing = function (pluginName) {
419 var steps = this._pluginPipelineSteps[pluginName],
420 extMethodHashes = this._pluginExtendedMethods[pluginName],
421 i = 0, klass, extMethodHash;
422
423 if (!this.plugins[pluginName]) return;
424
425 if (steps) {
426 for (i = 0; i < steps.length; i++) {
427 this.pipeline.removeStep(steps[i]);
428 }
429 }
430
431 if (extMethodHashes){
432 for (i = 0; i < extMethodHashes.length; i++){
433 klass = extMethodHashes[i][0]
434 extMethodHash = extMethodHashes[i][1]
435 for (var methodName in extMethodHash) {
436 delete klass.prototype[methodName]
437 delete klass.Invalid[methodName]
438 }
439 }
440 }
441
442 delete this.plugins[pluginName]
443
444 return this;
445}
446
447Controller.prototype.useRegisteredPlugins = function(){
448 for (var plugin in Controller._pluginFactories){
449 this.use(plugin);
450 }
451}
452
453
454_.extend(Controller.prototype, EventEmitter.prototype);