node-red-contrib-leap-motion
Version:
Node-Red nodes for leap motion
504 lines (480 loc) • 18.7 kB
JavaScript
var Hand = require("./hand")
, Pointable = require("./pointable")
, createGesture = require("./gesture").createGesture
, glMatrix = require("gl-matrix")
, mat3 = glMatrix.mat3
, vec3 = glMatrix.vec3
, InteractionBox = require("./interaction_box")
, Finger = require('./finger')
, _ = require("underscore");
/**
* Constructs a Frame object.
*
* Frame instances created with this constructor are invalid.
* Get valid Frame objects by calling the
* [Controller.frame]{@link Leap.Controller#frame}() function.
*<C-D-Space>
* @class Frame
* @memberof Leap
* @classdesc
* The Frame class represents a set of hand and finger tracking data detected
* in a single frame.
*
* The Leap detects hands, fingers and tools within the tracking area, reporting
* their positions, orientations and motions in frames at the Leap frame rate.
*
* Access Frame objects using the [Controller.frame]{@link Leap.Controller#frame}() function.
*/
var Frame = module.exports = function(data) {
/**
* Reports whether this Frame instance is valid.
*
* A valid Frame is one generated by the Controller object that contains
* tracking data for all detected entities. An invalid Frame contains no
* actual tracking data, but you can call its functions without risk of a
* undefined object exception. The invalid Frame mechanism makes it more
* convenient to track individual data across the frame history. For example,
* you can invoke:
*
* ```javascript
* var finger = controller.frame(n).finger(fingerID);
* ```
*
* for an arbitrary Frame history value, "n", without first checking whether
* frame(n) returned a null object. (You should still check that the
* returned Finger instance is valid.)
*
* @member valid
* @memberof Leap.Frame.prototype
* @type {Boolean}
*/
this.valid = true;
/**
* A unique ID for this Frame. Consecutive frames processed by the Leap
* have consecutive increasing values.
* @member id
* @memberof Leap.Frame.prototype
* @type {String}
*/
this.id = data.id;
/**
* The frame capture time in microseconds elapsed since the Leap started.
* @member timestamp
* @memberof Leap.Frame.prototype
* @type {number}
*/
this.timestamp = data.timestamp;
/**
* The list of Hand objects detected in this frame, given in arbitrary order.
* The list can be empty if no hands are detected.
*
* @member hands[]
* @memberof Leap.Frame.prototype
* @type {Leap.Hand}
*/
this.hands = [];
this.handsMap = {};
/**
* The list of Pointable objects (fingers and tools) detected in this frame,
* given in arbitrary order. The list can be empty if no fingers or tools are
* detected.
*
* @member pointables[]
* @memberof Leap.Frame.prototype
* @type {Leap.Pointable}
*/
this.pointables = [];
/**
* The list of Tool objects detected in this frame, given in arbitrary order.
* The list can be empty if no tools are detected.
*
* @member tools[]
* @memberof Leap.Frame.prototype
* @type {Leap.Pointable}
*/
this.tools = [];
/**
* The list of Finger objects detected in this frame, given in arbitrary order.
* The list can be empty if no fingers are detected.
* @member fingers[]
* @memberof Leap.Frame.prototype
* @type {Leap.Pointable}
*/
this.fingers = [];
/**
* The InteractionBox associated with the current frame.
*
* @member interactionBox
* @memberof Leap.Frame.prototype
* @type {Leap.InteractionBox}
*/
if (data.interactionBox) {
this.interactionBox = new InteractionBox(data.interactionBox);
}
this.gestures = [];
this.pointablesMap = {};
this._translation = data.t;
this._rotation = _.flatten(data.r);
this._scaleFactor = data.s;
this.data = data;
this.type = 'frame'; // used by event emitting
this.currentFrameRate = data.currentFrameRate;
if (data.gestures) {
/**
* The list of Gesture objects detected in this frame, given in arbitrary order.
* The list can be empty if no gestures are detected.
*
* Circle and swipe gestures are updated every frame. Tap gestures
* only appear in the list for a single frame.
* @member gestures[]
* @memberof Leap.Frame.prototype
* @type {Leap.Gesture}
*/
for (var gestureIdx = 0, gestureCount = data.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
this.gestures.push(createGesture(data.gestures[gestureIdx]));
}
}
this.postprocessData(data);
};
Frame.prototype.postprocessData = function(data){
if (!data) {
data = this.data;
}
for (var handIdx = 0, handCount = data.hands.length; handIdx != handCount; handIdx++) {
var hand = new Hand(data.hands[handIdx]);
hand.frame = this;
this.hands.push(hand);
this.handsMap[hand.id] = hand;
}
data.pointables = _.sortBy(data.pointables, function(pointable) { return pointable.id });
for (var pointableIdx = 0, pointableCount = data.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
var pointableData = data.pointables[pointableIdx];
var pointable = pointableData.dipPosition ? new Finger(pointableData) : new Pointable(pointableData);
pointable.frame = this;
this.addPointable(pointable);
}
};
/**
* Adds data from a pointable element into the pointablesMap;
* also adds the pointable to the frame.handsMap hand to which it belongs,
* and to the hand's tools or hand's fingers map.
*
* @param pointable {Object} a Pointable
*/
Frame.prototype.addPointable = function (pointable) {
this.pointables.push(pointable);
this.pointablesMap[pointable.id] = pointable;
(pointable.tool ? this.tools : this.fingers).push(pointable);
if (pointable.handId !== undefined && this.handsMap.hasOwnProperty(pointable.handId)) {
var hand = this.handsMap[pointable.handId];
hand.pointables.push(pointable);
(pointable.tool ? hand.tools : hand.fingers).push(pointable);
switch (pointable.type){
case 0:
hand.thumb = pointable;
break;
case 1:
hand.indexFinger = pointable;
break;
case 2:
hand.middleFinger = pointable;
break;
case 3:
hand.ringFinger = pointable;
break;
case 4:
hand.pinky = pointable;
break;
}
}
};
/**
* The tool with the specified ID in this frame.
*
* Use the Frame tool() function to retrieve a tool from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Pointable object, but if no tool
* with the specified ID is present, an invalid Pointable object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a tool is lost and subsequently
* regained, the new Pointable object representing that tool may have a
* different ID than that representing the tool in an earlier frame.
*
* @method tool
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a Tool object from a previous frame.
* @returns {Leap.Pointable} The tool with the
* matching ID if one exists in this frame; otherwise, an invalid Pointable object
* is returned.
*/
Frame.prototype.tool = function(id) {
var pointable = this.pointable(id);
return pointable.tool ? pointable : Pointable.Invalid;
};
/**
* The Pointable object with the specified ID in this frame.
*
* Use the Frame pointable() function to retrieve the Pointable object from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Pointable object, but if no finger or tool
* with the specified ID is present, an invalid Pointable object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a finger or tool is lost and subsequently
* regained, the new Pointable object representing that finger or tool may have
* a different ID than that representing the finger or tool in an earlier frame.
*
* @method pointable
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a Pointable object from a previous frame.
* @returns {Leap.Pointable} The Pointable object with
* the matching ID if one exists in this frame;
* otherwise, an invalid Pointable object is returned.
*/
Frame.prototype.pointable = function(id) {
return this.pointablesMap[id] || Pointable.Invalid;
};
/**
* The finger with the specified ID in this frame.
*
* Use the Frame finger() function to retrieve the finger from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Finger object, but if no finger
* with the specified ID is present, an invalid Pointable object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a finger is lost and subsequently
* regained, the new Pointable object representing that physical finger may have
* a different ID than that representing the finger in an earlier frame.
*
* @method finger
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a finger from a previous frame.
* @returns {Leap.Pointable} The finger with the
* matching ID if one exists in this frame; otherwise, an invalid Pointable
* object is returned.
*/
Frame.prototype.finger = function(id) {
var pointable = this.pointable(id);
return !pointable.tool ? pointable : Pointable.Invalid;
};
/**
* The Hand object with the specified ID in this frame.
*
* Use the Frame hand() function to retrieve the Hand object from
* this frame using an ID value obtained from a previous frame.
* This function always returns a Hand object, but if no hand
* with the specified ID is present, an invalid Hand object is returned.
*
* Note that ID values persist across frames, but only until tracking of a
* particular object is lost. If tracking of a hand is lost and subsequently
* regained, the new Hand object representing that physical hand may have
* a different ID than that representing the physical hand in an earlier frame.
*
* @method hand
* @memberof Leap.Frame.prototype
* @param {String} id The ID value of a Hand object from a previous frame.
* @returns {Leap.Hand} The Hand object with the matching
* ID if one exists in this frame; otherwise, an invalid Hand object is returned.
*/
Frame.prototype.hand = function(id) {
return this.handsMap[id] || Hand.Invalid;
};
/**
* The angle of rotation around the rotation axis derived from the overall
* rotational motion between the current frame and the specified frame.
*
* The returned angle is expressed in radians measured clockwise around
* the rotation axis (using the right-hand rule) between the start and end frames.
* The value is always between 0 and pi radians (0 and 180 degrees).
*
* The Leap derives frame rotation from the relative change in position and
* orientation of all objects detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, then the
* angle of rotation is zero.
*
* @method rotationAngle
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
* @param {number[]} [axis] The axis to measure rotation around.
* @returns {number} A positive value containing the heuristically determined
* rotational change between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.rotationAngle = function(sinceFrame, axis) {
if (!this.valid || !sinceFrame.valid) return 0.0;
var rot = this.rotationMatrix(sinceFrame);
var cs = (rot[0] + rot[4] + rot[8] - 1.0)*0.5;
var angle = Math.acos(cs);
angle = isNaN(angle) ? 0.0 : angle;
if (axis !== undefined) {
var rotAxis = this.rotationAxis(sinceFrame);
angle *= vec3.dot(rotAxis, vec3.normalize(vec3.create(), axis));
}
return angle;
};
/**
* The axis of rotation derived from the overall rotational motion between
* the current frame and the specified frame.
*
* The returned direction vector is normalized.
*
* The Leap derives frame rotation from the relative change in position and
* orientation of all objects detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, or if no
* rotation is detected between the two frames, a zero vector is returned.
*
* @method rotationAxis
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
* @returns {number[]} A normalized direction vector representing the axis of the heuristically determined
* rotational change between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.rotationAxis = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return vec3.create();
return vec3.normalize(vec3.create(), [
this._rotation[7] - sinceFrame._rotation[5],
this._rotation[2] - sinceFrame._rotation[6],
this._rotation[3] - sinceFrame._rotation[1]
]);
}
/**
* The transform matrix expressing the rotation derived from the overall
* rotational motion between the current frame and the specified frame.
*
* The Leap derives frame rotation from the relative change in position and
* orientation of all objects detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, then
* this method returns an identity matrix.
*
* @method rotationMatrix
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative rotation.
* @returns {number[]} A transformation matrix containing the heuristically determined
* rotational change between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.rotationMatrix = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return mat3.create();
var transpose = mat3.transpose(mat3.create(), this._rotation)
return mat3.multiply(mat3.create(), sinceFrame._rotation, transpose);
}
/**
* The scale factor derived from the overall motion between the current frame and the specified frame.
*
* The scale factor is always positive. A value of 1.0 indicates no scaling took place.
* Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion.
*
* The Leap derives scaling from the relative inward or outward motion of all
* objects detected in the field of view (independent of translation and rotation).
*
* If either this frame or sinceFrame is an invalid Frame object, then this method returns 1.0.
*
* @method scaleFactor
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative scaling.
* @returns {number} A positive value representing the heuristically determined
* scaling change ratio between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.scaleFactor = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return 1.0;
return Math.exp(this._scaleFactor - sinceFrame._scaleFactor);
}
/**
* The change of position derived from the overall linear motion between the
* current frame and the specified frame.
*
* The returned translation vector provides the magnitude and direction of the
* movement in millimeters.
*
* The Leap derives frame translation from the linear motion of all objects
* detected in the field of view.
*
* If either this frame or sinceFrame is an invalid Frame object, then this
* method returns a zero vector.
*
* @method translation
* @memberof Leap.Frame.prototype
* @param {Leap.Frame} sinceFrame The starting frame for computing the relative translation.
* @returns {number[]} A vector representing the heuristically determined change in
* position of all objects between the current frame and that specified in the sinceFrame parameter.
*/
Frame.prototype.translation = function(sinceFrame) {
if (!this.valid || !sinceFrame.valid) return vec3.create();
return vec3.subtract(vec3.create(), this._translation, sinceFrame._translation);
}
/**
* A string containing a brief, human readable description of the Frame object.
*
* @method toString
* @memberof Leap.Frame.prototype
* @returns {String} A brief description of this frame.
*/
Frame.prototype.toString = function() {
var str = "Frame [ id:"+this.id+" | timestamp:"+this.timestamp+" | Hand count:("+this.hands.length+") | Pointable count:("+this.pointables.length+")";
if (this.gestures) str += " | Gesture count:("+this.gestures.length+")";
str += " ]";
return str;
}
/**
* Returns a JSON-formatted string containing the hands, pointables and gestures
* in this frame.
*
* @method dump
* @memberof Leap.Frame.prototype
* @returns {String} A JSON-formatted string.
*/
Frame.prototype.dump = function() {
var out = '';
out += "Frame Info:<br/>";
out += this.toString();
out += "<br/><br/>Hands:<br/>"
for (var handIdx = 0, handCount = this.hands.length; handIdx != handCount; handIdx++) {
out += " "+ this.hands[handIdx].toString() + "<br/>";
}
out += "<br/><br/>Pointables:<br/>";
for (var pointableIdx = 0, pointableCount = this.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
out += " "+ this.pointables[pointableIdx].toString() + "<br/>";
}
if (this.gestures) {
out += "<br/><br/>Gestures:<br/>";
for (var gestureIdx = 0, gestureCount = this.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
out += " "+ this.gestures[gestureIdx].toString() + "<br/>";
}
}
out += "<br/><br/>Raw JSON:<br/>";
out += JSON.stringify(this.data);
return out;
}
/**
* An invalid Frame object.
*
* You can use this invalid Frame in comparisons testing
* whether a given Frame instance is valid or invalid. (You can also check the
* [Frame.valid]{@link Leap.Frame#valid} property.)
*
* @static
* @type {Leap.Frame}
* @name Invalid
* @memberof Leap.Frame
*/
Frame.Invalid = {
valid: false,
hands: [],
fingers: [],
tools: [],
gestures: [],
pointables: [],
pointable: function() { return Pointable.Invalid },
finger: function() { return Pointable.Invalid },
hand: function() { return Hand.Invalid },
toString: function() { return "invalid frame" },
dump: function() { return this.toString() },
rotationAngle: function() { return 0.0; },
rotationMatrix: function() { return mat3.create(); },
rotationAxis: function() { return vec3.create(); },
scaleFactor: function() { return 1.0; },
translation: function() { return vec3.create(); }
};