diff --git a/src/InteractablePlane.js b/src/InteractablePlane.js index d4ab734..9d7affc 100644 --- a/src/InteractablePlane.js +++ b/src/InteractablePlane.js @@ -12,6 +12,12 @@ window.InteractablePlane = function(planeMesh, controller, options){ this.options.damping !== undefined || (this.options.damping = 0.12); // this can be configured through this.highlightMesh this.options.hoverBounds !== undefined || (this.options.hoverBounds = [0, 0.32]); // react to hover within 3cm. + // For use with a plane constrained to the z. This configures the threshold with which a hand can push in the z + // By moving with a sloped hand. The number is the slope of the hand (m = rise/run = x/z or y/z). e.g., 1 = 1/1 = 45° + // Turn it up (like to 100) for a more responsive, but jumpy, feel + // Turn it down (like to 5) for something more controllable. + this.options.minSlideAngle !== undefined || (this.options.minSlideAngle = 10); // react to hover within 3cm. + this.mesh = planeMesh; if (!(controller instanceof Leap.Controller)) { @@ -41,10 +47,6 @@ window.InteractablePlane = function(planeMesh, controller, options){ // Todo - it would be great to have a "bouncy constraint" option, which would act like the scroll limits on OSX this.movementConstraints = {}; - // If this is ever increased above one, that initial finger can not be counted when averaging position - // otherwise, it causes jumpyness. - this.fingersRequiredForMove = 1; - this.tempVec3 = new THREE.Vector3; this.density = 1; @@ -121,7 +123,6 @@ window.InteractablePlane.prototype = { this.touch(function(){ if (!this.interactable) return; - this.highlight(true); }.bind(this)); @@ -167,35 +168,82 @@ window.InteractablePlane.prototype = { // Returns the position of the mesh intersected // If position is passed in, sets it. getPosition: function(position){ - var newPosition = position || new THREE.Vector3, intersectionCount = 0; + var newPosition = (position || new THREE.Vector3).set(0,0,0); + var n = new THREE.Vector3; + + // todo: factor back in XY movement and officially scrap this.intersections + // this may need a proper place (perhaps with getters and setters on moveX and moveY) + this.moveProximity.options.xyRetain = false; + + var sumZ = 0; + + var i = 0, ns = [], z = 0, p1, p2, intersectionPoint; + + for ( var intersectionKey in this.moveProximity.intersectionPoints ) { + if( !this.moveProximity.intersectionPoints.hasOwnProperty(intersectionKey) ) continue; + + p1 = this.moveProximity.intersectingLines[intersectionKey][0]; // line beginning + p2 = this.moveProximity.intersectingLines[intersectionKey][1]; // line end + intersectionPoint = this.moveProximity.intersectionPoints[intersectionKey]; + + n.subVectors(p2, p1); - for ( var intersectionKey in this.intersections ){ - if( this.intersections.hasOwnProperty(intersectionKey) ){ + ns.push( n.clone() ); - intersectionCount++; + var delta = this.moveProximity.positionChange(intersectionKey); + if ( !delta || ( Math.abs(delta.x) < 1e-8 && Math.abs(delta.y) < 1e-8 && Math.abs(delta.z) < 1e-8 ) ) continue; + // ^^ small (e-9) values seem to get introduced sometimes, perhaps from the intersectionOffset. - newPosition.add( - this.moveProximity.intersectionPoints[intersectionKey].clone().sub( - this.intersections[intersectionKey] - ) - ) + // todo - "play" factor? Allow a smidgen of xy and maybe some rotation at strong z angles, indicating bend + // y = mx + b + // m = rise/run = n.x / n.z + // z = n.z / n.x * delta.x + z = 0; + if (n.x !==0 && Math.abs(n.z / n.x) < this.options.minSlideAngle ) z += n.z / n.x * delta.x * -1; + if (n.y !==0 && Math.abs(n.z / n.y) < this.options.minSlideAngle ) z += n.z / n.y * delta.y * -1; + + // Don't move farther than bone end + // note: there's got to be a way to clean this up a little + if ( z > 0 ) { + if (p2.z > p1.z) { + z = Math.min(z, p2.z - intersectionPoint.z) + 1e-8; + } else { + z = Math.min(z, p1.z - intersectionPoint.z) + 1e-8; + } + } else { + if (p2.z > p1.z) { + z = Math.max(z, p1.z - intersectionPoint.z) - 1e-8; + } else { + z = Math.max(z, p2.z - intersectionPoint.z) - 1e-8; + } } + sumZ += z; + + i++; } - // todo - experiment with spring physics - if ( intersectionCount < this.fingersRequiredForMove) { + // average the inputs + if (i > 0) z = sumZ / i; - newPosition.copy(this.mesh.position); + newPosition.copy(this.mesh.position); - } else { - newPosition.divideScalar(intersectionCount); + // what happens when this combines with movement constraints? + + i = 0; + for ( var intersectionKey in this.moveProximity.intersectionPoints ) { + if( !this.moveProximity.intersectionPoints.hasOwnProperty(intersectionKey) ) continue; + + var intersectionOffset = ns[i].multiplyScalar(z / ns[i].z); + this.moveProximity.intersectionPoints[intersectionKey].add(intersectionOffset); + i++; } + newPosition.z += z; - return newPosition; + return newPosition }, // Adds a spring @@ -402,18 +450,22 @@ window.InteractablePlane.prototype = { // for every 2 index, we want to add (4 - 2). That will equal the boneMesh index. // not sure if there is a clever formula for the following array: - var indexToBoneMeshIndex = [0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3, 0,1,2,3]; + var indexToBoneMeshIndex = [ + [0,5],[0,3],[0,1], + [1,7],[1,5],[1,3],[1,1], + [2,7],[2,5],[2,3],[2,1], + [3,7],[3,5],[3,3],[3,1], + [4,7],[4,5],[4,3],[4,1] + ]; var setBoneMeshColor = function(hand, index, color){ // In `index / 2`, `2` is the number of joints per hand we're looking at. - var meshes = hand.fingers[ Math.floor(index / 4) ].data('boneMeshes'); + if (!hand.data('handMesh')) return; + var meshes = hand.data('handMesh').fingerMeshes; + var x = indexToBoneMeshIndex[index]; - if (!meshes) return; - - meshes[ - indexToBoneMeshIndex[index] - ].material.color.setHex(color) + meshes[x[0]][x[1]].material.color.setHex(color) }; @@ -422,7 +474,7 @@ window.InteractablePlane.prototype = { // todo - rename to something that's not a mozilla method var proximity = this.moveProximity = this.controller.watch( this.mesh, - this.interactiveEndBones + this.interactiveBones ); // this ties InteractablePlane to boneHand plugin - probably should have callbacks pushed out to scene. @@ -434,16 +486,10 @@ window.InteractablePlane.prototype = { // This doesn't allow intersections to count if I'm already pinching // So if they want to move after a pinch, they have to take hand out of picture and re-place. if (hand.data('resizing')) return; - setBoneMeshColor(hand, index, 0xffffff); + setBoneMeshColor(hand, index, 0x00ff00); this.intersections[key] = intersectionPoint.clone().sub(this.mesh.position); - if (!this.touched) { - this.touched = true; -// console.log('touch', this.mesh.name); - this.emit('touch', this); - } - }.bind(this) ); proximity.out( function(hand, intersectionPoint, key, index){ @@ -461,13 +507,6 @@ window.InteractablePlane.prototype = { } - // not sure why, but sometimes getting multiple 0 proximity release events - if (proximity.intersectionCount() == 0 && this.touched) { - this.touched = false; -// console.log('release', this.mesh.name, proximity.intersectionCount()); - this.emit('release', this); - } - }.bind(this) ); }, @@ -566,12 +605,29 @@ window.InteractablePlane.prototype = { } - // note - include moveZ here when implemented. if ( moveX || moveY || moveZ ) this.emit( 'travel', this, this.mesh ); + this.emitTouchEvents(); // This happens after getPosition has a chance to mutate intersectionPoints. if (this.options.hoverBounds) this.emitHoverEvents(); }, + emitTouchEvents: function(){ + + var intersectionCount = this.moveProximity.intersectionCount(); + + if (intersectionCount > 0 && !this.touched) { + this.touched = true; + this.emit('touch', this); + } + + // not sure why, but sometimes getting multiple 0 proximity release events + if (intersectionCount === 0 && this.touched) { + this.touched = false; + this.emit('release', this); + } + + }, + // Takes the previousOverlap calculated earlier in this frame. // If any within range, emits an event. // note - could also emit an event on that fingertip? @@ -579,6 +635,7 @@ window.InteractablePlane.prototype = { var overlap, isHovered; + // "Not optimized: ForInStatement is not fast case" for (var key in this.previousOverlap){ overlap = this.previousOverlap[key]; @@ -691,7 +748,8 @@ window.InteractablePlane.prototype = { // Order matters for our own use in this class // returns a collection of lines to be tested against // could be optimized to reuse vectors between frames - interactiveEndBones: function(hand){ + interactiveBones: function(hand){ + var out = [], finger; for (var i = 0; i < 5; i++){ @@ -700,13 +758,17 @@ window.InteractablePlane.prototype = { if (i > 0){ // no thumb proximal out.push( [ - (new THREE.Vector3).fromArray(finger.proximal.nextJoint), - (new THREE.Vector3).fromArray(finger.proximal.prevJoint) + (new THREE.Vector3).fromArray(finger.metacarpal.nextJoint), + (new THREE.Vector3).fromArray(finger.metacarpal.prevJoint) ] ); } out.push( + [ + (new THREE.Vector3).fromArray(finger.proximal.nextJoint), + (new THREE.Vector3).fromArray(finger.proximal.prevJoint) + ], [ (new THREE.Vector3).fromArray(finger.medial.nextJoint), (new THREE.Vector3).fromArray(finger.medial.prevJoint) diff --git a/src/leap.proximity.js b/src/leap.proximity.js index 18491e6..255a723 100644 --- a/src/leap.proximity.js +++ b/src/leap.proximity.js @@ -123,22 +123,21 @@ Leap.plugin('proximity', function(scope){ // mode: var Proximity = function(mesh, handPoints, options){ - setTimeout( // pop out of angular scope. - function(){ - testIntersectionPointBetweenLines() - }, - 0 - ); - options || (options = {}); this.options = options; + // This is to be used when the object is mobile on the XY plane, meaning that one wouldn't usually expect this to be let go + // Todo - this could be refactored with something smarter - more constraint-aware. + this.options.xyRetain !== undefined || (this.options.xyRetain = true); + this.mesh = mesh; this.handPoints = handPoints; // These are both keyed by the string: hand.id + handPointIndex this.states = {}; this.intersectionPoints = {}; // checkLines: one for each handPoint. Position in world space. + this.lastIntersectionPoints = {}; + this.intersectingLines = {}; // Similar to above, but also includes point on the plane, but not on the plane segment. // This is used for responding to between-frame motion @@ -173,6 +172,7 @@ Leap.plugin('proximity', function(scope){ return this }, + // todo this is kind of a dumb method and should be architected out check: function(hand){ // Handles Spheres. Planes. Boxes? other shapes? custom shapes? @@ -193,21 +193,24 @@ Leap.plugin('proximity', function(scope){ }, - // Todo - this loop could be split in to smaller methods for JIT compiler optimization. + // todo - circles support checkLines: function(hand, lines){ - var mesh = this.mesh, state, intersectionPoint, key; + var mesh = this.mesh, state, intersectionPoint, key, line; var worldPosition = (new THREE.Vector3).setFromMatrixPosition( this.mesh.matrixWorld ); - // j because this is inside a loop for every hand - for (var j = 0; j < lines.length; j++){ + this.intersectingLines = {}; - key = hand.id + '-' + j; + for (var i = 0; i < lines.length; i++){ + + line = lines[i]; + key = hand.id + '-' + i; - intersectionPoint = mesh.intersectedByLine(lines[j][0], lines[j][1], worldPosition); + intersectionPoint = mesh.intersectedByLine(line[0], line[1], worldPosition); - var lastIntersectionPoint = this.possibleIntersectionPoints[key]; + var possibleIntersectionPoint = this.possibleIntersectionPoints[key]; // a somewhat terrible name indicating a point on the plane outside of the plane segment. + // handle incoming fast bone // 1: store lastIntersectionPoint at all times // 2: only return values for good intersectionpoints from mesh.intersectedByLine // 3: use it to tune intersectionpoint. @@ -217,7 +220,10 @@ Leap.plugin('proximity', function(scope){ // In that case, the foremost line should push the image, but what happens here and in InteractablePlane#getPosition // is the lines are averaged and then move the image // InteractablePlane should be aware of this adjustment (perhaps doing so itself) - if ( this.states[key] === 'out' && intersectionPoint && lastIntersectionPoint ){ + // This is somewhat edgy (no pun intended): + // we overwrite lastIntersection point to be on the edge. although there was never actually a frame emitted + // with it as an intersection point. + if ( this.options.xyRetain && this.states[key] !== 'in' && intersectionPoint && possibleIntersectionPoint ){ // check all four edges, // take the one that actually has a cross @@ -229,12 +235,14 @@ Leap.plugin('proximity', function(scope){ var minLenSq = Infinity; var closestEdgeIntersectionPoint = null; - for (var i = 0; i < 4; i++){ + for (var j = 0; j < 4; j++){ + // todo - this doesn't work with circles.. + // maybe: who cares? var point = intersectionPointBetweenLines( - corners[i], - corners[(i+1) % 4], - lastIntersectionPoint, + corners[j], + corners[(j+1) % 4], + possibleIntersectionPoint, intersectionPoint ); @@ -245,7 +253,7 @@ Leap.plugin('proximity', function(scope){ //console.assert(!isNaN(point.y)); //console.assert(!isNaN(point.z)); - var lengthSq = (new THREE.Vector3).subVectors(point, lastIntersectionPoint).lengthSq(); + var lengthSq = (new THREE.Vector3).subVectors(point, possibleIntersectionPoint).lengthSq(); // console.log('edge #:', i, 'line #:', j, "distance:", Math.sqrt(lengthSq) ); @@ -256,21 +264,17 @@ Leap.plugin('proximity', function(scope){ } - if (closestEdgeIntersectionPoint) { - - //console.log('edge intersection', closestEdgeIntersectionPoint, "between", intersectionPoint, "and", lastIntersectionPoint); - - intersectionPoint = closestEdgeIntersectionPoint; - - } - } + // handle outgoing fast bone // if there already was a valid intersection point, // And the new one is valid in z but off in x and y, // don't emit an out event. // This allows high-speed motions out. - if ( !intersectionPoint && this.intersectionPoints[key] && mesh.intersectionPoint ) { + // when there's one bone being dragged out + // and there's no complimenting z motion, this shouldn't fire pretty much.. + // we provide the xyRetain option as a quick fix to disable this feature + if ( this.options.xyRetain && !intersectionPoint && this.intersectionPoints[key] && mesh.intersectionPoint ) { //console.log('found newly lost intersection point'); intersectionPoint = mesh.intersectionPoint @@ -279,7 +283,18 @@ Leap.plugin('proximity', function(scope){ if (intersectionPoint){ + if (closestEdgeIntersectionPoint) { + + //console.log('edge intersection', closestEdgeIntersectionPoint, "between", intersectionPoint, "and", lastIntersectionPoint); + + // actually becomes this.lastIntersectionPoints[key]: + this.intersectionPoints[key] = closestEdgeIntersectionPoint; + + } + + this.lastIntersectionPoints[key] = this.intersectionPoints[key]; this.intersectionPoints[key] = intersectionPoint; + this.intersectingLines[key] = line; } else if (this.intersectionPoints[key]) { @@ -300,7 +315,7 @@ Leap.plugin('proximity', function(scope){ state = intersectionPoint ? 'in' : 'out'; if ( (state == 'in' && this.states[key] !== 'in') || (state == 'out' && this.states[key] === 'in')){ // this logic prevents initial `out` events. - this.emit(state, hand, intersectionPoint, key, j); // todo - could include intersection displacement vector here (!) + this.emit(state, hand, intersectionPoint, key, i); this.states[key] = state; } @@ -308,6 +323,17 @@ Leap.plugin('proximity', function(scope){ }, + // Gets the change in position between this frame and last for the given line key + positionChange: function(key){ + if ( !this.lastIntersectionPoints[key] || !this.intersectionPoints[key] ) return; + + return (new THREE.Vector3).subVectors( + this.intersectionPoints[key], + this.lastIntersectionPoints[key] + ) + + }, + checkPoints: function(hand, handPoints){ var mesh = this.mesh, length, state, handPoint, meshWorldPosition = new THREE.Vector3, @@ -358,6 +384,7 @@ Leap.plugin('proximity', function(scope){ delete this.states[key]; delete this.intersectionPoints[key]; + delete this.lastIntersectionPoints[key]; delete this.lengths[key]; delete this.distances[key]; this.emit('out', hand, null, key, parseInt(key.split('-')[1],10) ); diff --git a/test/slideZ.html b/test/slideZ.html new file mode 100644 index 0000000..173ab9f --- /dev/null +++ b/test/slideZ.html @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + +View Source + +

+ Insert exactly one hand to run loop. Type `moveLineX(0.001)` in to console to experiment. +

+

+ +

+ + + + + + \ No newline at end of file