Merge branch 'master' of https://github.com/highfidelity/hifi into orange

This commit is contained in:
samcake 2015-09-18 13:59:56 -07:00
commit a804767d8f

View file

@ -10,72 +10,69 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
Script.include("../libraries/utils.js");
var RADIUS_FACTOR = 4;
var RIGHT_HAND_CLICK = Controller.findAction("RIGHT_HAND_CLICK");
var rightTriggerAction = RIGHT_HAND_CLICK;
/////////////////////////////////////////////////////////////////
//
// these tune time-averaging and "on" value for analog trigger
//
var GRAB_USER_DATA_KEY = "grabKey";
var TRIGGER_SMOOTH_RATIO = 0.7;
var TRIGGER_ON_VALUE = 0.2;
var LEFT_HAND_CLICK = Controller.findAction("LEFT_HAND_CLICK");
var leftTriggerAction = LEFT_HAND_CLICK;
/////////////////////////////////////////////////////////////////
//
// distant manipulation
//
var LIFETIME = 10;
var EXTRA_TIME = 5;
var POINTER_CHECK_TIME = 5000;
var ZERO_VEC = {
x: 0,
y: 0,
z: 0
}
var DISTANCE_HOLDING_RADIUS_FACTOR = 4; // multiplied by distance between hand and object
var DISTANCE_HOLDING_ACTION_TIMEFRAME = 0.1; // how quickly objects move to their new position
var DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR = 2.0; // object rotates this much more than hand did
var NO_INTERSECT_COLOR = {red: 10, green: 10, blue: 255}; // line color when pick misses
var INTERSECT_COLOR = {red: 250, green: 10, blue: 10}; // line color when pick hits
var LINE_ENTITY_DIMENSIONS = {x: 1000, y: 1000, z: 1000};
var LINE_LENGTH = 500;
var THICK_LINE_WIDTH = 7;
var THIN_LINE_WIDTH = 2;
var NO_INTERSECT_COLOR = {
red: 10,
green: 10,
blue: 255
};
var INTERSECT_COLOR = {
red: 250,
green: 10,
blue: 10
};
var GRAB_RADIUS = 0.3;
/////////////////////////////////////////////////////////////////
//
// close grabbing
//
var GRAB_COLOR = {
red: 250,
green: 10,
blue: 250
};
var SHOW_LINE_THRESHOLD = 0.2;
var DISTANCE_HOLD_THRESHOLD = 0.8;
var GRAB_RADIUS = 0.3; // if the ray misses but an object is this close, it will still be selected
var CLOSE_GRABBING_ACTION_TIMEFRAME = 0.05; // how quickly objects move to their new position
var CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO = 0.9; // adjust time-averaging of held object's velocity
var CLOSE_PICK_MAX_DISTANCE = 0.6; // max length of pick-ray for close grabbing to be selected
var right4Action = 18;
var left4Action = 17;
var RIGHT = 1;
var LEFT = 0;
var rightController = new controller(RIGHT, rightTriggerAction, right4Action, "right");
var leftController = new controller(LEFT, leftTriggerAction, left4Action, "left");
/////////////////////////////////////////////////////////////////
//
// other constants
//
var RIGHT_HAND = 1;
var LEFT_HAND = 0;
var ZERO_VEC = {x: 0, y: 0, z: 0};
var NULL_ACTION_ID = "{00000000-0000-0000-000000000000}";
var MSEC_PER_SEC = 1000.0;
// these control how long an abandoned pointer line will hang around
var startTime = Date.now();
var LIFETIME = 10;
// states for the state machine
var STATE_SEARCHING = 0;
var STATE_DISTANCE_HOLDING = 1;
var STATE_CLOSE_GRABBING = 2;
var STATE_CONTINUE_CLOSE_GRABBING = 3;
var STATE_RELEASE = 4;
//Need to wait before calling these methods for some reason...
Script.setTimeout(function() {
rightController.checkPointer();
leftController.checkPointer();
}, 100)
function controller(side, triggerAction, pullAction, hand) {
function controller(hand, triggerAction) {
this.hand = hand;
if (hand === "right") {
if (this.hand === RIGHT_HAND) {
this.getHandPosition = MyAvatar.getRightPalmPosition;
this.getHandRotation = MyAvatar.getRightPalmRotation;
} else {
@ -83,309 +80,292 @@ function controller(side, triggerAction, pullAction, hand) {
this.getHandRotation = MyAvatar.getLeftPalmRotation;
}
this.triggerAction = triggerAction;
this.pullAction = pullAction;
this.actionID = null;
this.distanceHolding = false;
this.closeGrabbing = false;
this.triggerValue = 0;
this.prevTriggerValue = 0;
this.palm = 2 * side;
this.tip = 2 * side + 1;
this.pointer = null;
}
this.palm = 2 * hand;
// this.tip = 2 * hand + 1; // unused, but I'm leaving this here for fear it will be needed
this.actionID = null; // action this script created...
this.grabbedEntity = null; // on this entity.
this.grabbedVelocity = ZERO_VEC; // rolling average of held object's velocity
this.state = 0; // 0 = searching, 1 = distanceHolding, 2 = closeGrabbing
this.pointer = null; // entity-id of line object
this.triggerValue = 0; // rolling average of trigger value
controller.prototype.updateLine = function() {
if (this.pointer != null) {
if (Entities.getEntityProperties(this.pointer).id != this.pointer) {
this.pointer = null;
this.update = function() {
switch(this.state) {
case STATE_SEARCHING:
this.search();
break;
case STATE_DISTANCE_HOLDING:
this.distanceHolding();
break;
case STATE_CLOSE_GRABBING:
this.closeGrabbing();
break;
case STATE_CONTINUE_CLOSE_GRABBING:
this.continueCloseGrabbing();
break;
case STATE_RELEASE:
this.release();
break;
}
}
if (this.pointer == null) {
this.lineCreationTime = Date.now();
this.pointer = Entities.addEntity({
type: "Line",
name: "pointer",
color: NO_INTERSECT_COLOR,
dimensions: {
x: 1000,
y: 1000,
z: 1000
},
visible: true,
lifetime: LIFETIME
});
}
var handPosition = this.getHandPosition();
var direction = Quat.getUp(this.getHandRotation());
//only check if we havent already grabbed an object
if (this.distanceHolding) {
Entities.editEntity(this.pointer, {
position: handPosition,
linePoints: [ ZERO_VEC, Vec3.subtract(Entities.getEntityProperties(this.grabbedEntity).position, handPosition) ],
lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME
});
return;
}
Entities.editEntity(this.pointer, {
position: handPosition,
linePoints: [ ZERO_VEC, Vec3.multiply(direction, LINE_LENGTH) ],
lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME
});
if (this.checkForIntersections(handPosition, direction)) {
Entities.editEntity(this.pointer, {
color: INTERSECT_COLOR,
});
} else {
Entities.editEntity(this.pointer, {
color: NO_INTERSECT_COLOR,
});
}
}
controller.prototype.checkPointer = function() {
var self = this;
Script.setTimeout(function() {
var props = Entities.getEntityProperties(self.pointer);
Entities.editEntity(self.pointer, {
lifetime: (Date.now() - startTime) / 1000.0 + LIFETIME
});
self.checkPointer();
}, POINTER_CHECK_TIME);
}
controller.prototype.checkForIntersections = function(origin, direction) {
var pickRay = {
origin: origin,
direction: direction
};
var intersection = Entities.findRayIntersection(pickRay, true);
if (intersection.intersects && intersection.properties.collisionsWillMove === 1) {
var handPosition = Controller.getSpatialControlPosition(this.palm);
this.distanceToEntity = Vec3.distance(handPosition, intersection.properties.position);
var intersectionDistance = Vec3.distance(handPosition, intersection.intersection);
if (intersectionDistance < 0.6) {
//We are grabbing an entity, so let it know we've grabbed it
this.grabbedEntity = intersection.entityID;
this.activateEntity(this.grabbedEntity);
this.hidePointer();
this.shouldDisplayLine = false;
this.grabEntity();
return true;
this.lineOn = function(closePoint, farPoint, color) {
// draw a line
if (this.pointer == null) {
this.pointer = Entities.addEntity({
type: "Line",
name: "pointer",
dimensions: LINE_ENTITY_DIMENSIONS,
visible: true,
position: closePoint,
linePoints: [ ZERO_VEC, farPoint ],
color: color,
lifetime: LIFETIME
});
} else {
Entities.editEntity(this.pointer, {
linePoints: [
ZERO_VEC,
Vec3.multiply(direction, this.distanceToEntity)
]
position: closePoint,
linePoints: [ ZERO_VEC, farPoint ],
color: color,
lifetime: (Date.now() - startTime) / MSEC_PER_SEC + LIFETIME
});
this.grabbedEntity = intersection.entityID;
return true;
}
}
return false;
}
controller.prototype.attemptMove = function() {
if (this.grabbedEntity || this.distanceHolding) {
var handPosition = Controller.getSpatialControlPosition(this.palm);
this.lineOff = function() {
if (this.pointer != null) {
Entities.deleteEntity(this.pointer);
}
this.pointer = null;
}
this.triggerSmoothedSqueezed = function() {
var triggerValue = Controller.getActionValue(this.triggerAction);
// smooth out trigger value
this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) +
(triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO));
return this.triggerValue > TRIGGER_ON_VALUE;
}
this.triggerSqueezed = function() {
var triggerValue = Controller.getActionValue(this.triggerAction);
return triggerValue > TRIGGER_ON_VALUE;
}
this.search = function() {
if (!this.triggerSmoothedSqueezed()) {
this.state = STATE_RELEASE;
return;
}
// the trigger is being pressed, do a ray test
var handPosition = this.getHandPosition();
var pickRay = {origin: handPosition, direction: Quat.getUp(this.getHandRotation())};
var intersection = Entities.findRayIntersection(pickRay, true);
if (intersection.intersects &&
intersection.properties.collisionsWillMove === 1 &&
intersection.properties.locked === 0) {
// the ray is intersecting something we can move.
var handControllerPosition = Controller.getSpatialControlPosition(this.palm);
var intersectionDistance = Vec3.distance(handControllerPosition, intersection.intersection);
this.grabbedEntity = intersection.entityID;
if (intersectionDistance < CLOSE_PICK_MAX_DISTANCE) {
// the hand is very close to the intersected object. go into close-grabbing mode.
this.state = STATE_CLOSE_GRABBING;
} else {
// the hand is far from the intersected object. go into distance-holding mode
this.state = STATE_DISTANCE_HOLDING;
this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR);
}
} else {
// forward ray test failed, try sphere test.
var nearbyEntities = Entities.findEntities(handPosition, GRAB_RADIUS);
var minDistance = GRAB_RADIUS;
var grabbedEntity = null;
for (var i = 0; i < nearbyEntities.length; i++) {
var props = Entities.getEntityProperties(nearbyEntities[i]);
var distance = Vec3.distance(props.position, handPosition);
if (distance < minDistance && props.name !== "pointer" &&
props.collisionsWillMove === 1 &&
props.locked === 0) {
this.grabbedEntity = nearbyEntities[i];
minDistance = distance;
}
}
if (this.grabbedEntity === null) {
this.lineOn(pickRay.origin, Vec3.multiply(pickRay.direction, LINE_LENGTH), NO_INTERSECT_COLOR);
} else {
this.state = STATE_CLOSE_GRABBING;
}
}
}
this.distanceHolding = function() {
if (!this.triggerSmoothedSqueezed()) {
this.state = STATE_RELEASE;
return;
}
var handPosition = this.getHandPosition();
var handControllerPosition = Controller.getSpatialControlPosition(this.palm);
var handRotation = Quat.multiply(MyAvatar.orientation, Controller.getSpatialControlRawRotation(this.palm));
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity);
this.lineOn(handPosition, Vec3.subtract(grabbedProperties.position, handPosition), INTERSECT_COLOR);
this.distanceHolding = true;
if (this.actionID === null) {
this.currentObjectPosition = Entities.getEntityProperties(this.grabbedEntity).position;
this.currentObjectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation;
this.handPreviousPosition = handPosition;
// first time here since trigger pulled -- add the action and initialize some variables
this.currentObjectPosition = grabbedProperties.position;
this.currentObjectRotation = grabbedProperties.rotation;
this.handPreviousPosition = handControllerPosition;
this.handPreviousRotation = handRotation;
this.actionID = Entities.addAction("spring", this.grabbedEntity, {
targetPosition: this.currentObjectPosition,
linearTimeScale: .1,
linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME,
targetRotation: this.currentObjectRotation,
angularTimeScale: .1
angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME
});
if (this.actionID == NULL_ACTION_ID) {
this.actionID = null;
}
} else {
var radius = Math.max(Vec3.distance(this.currentObjectPosition, handPosition) * RADIUS_FACTOR, 1.0);
// the action was set up on a previous call. update the targets.
var radius = Math.max(Vec3.distance(this.currentObjectPosition,
handControllerPosition) * DISTANCE_HOLDING_RADIUS_FACTOR,
DISTANCE_HOLDING_RADIUS_FACTOR);
var handMoved = Vec3.subtract(handPosition, this.handPreviousPosition);
this.handPreviousPosition = handPosition;
var handMoved = Vec3.subtract(handControllerPosition, this.handPreviousPosition);
this.handPreviousPosition = handControllerPosition;
var superHandMoved = Vec3.multiply(handMoved, radius);
this.currentObjectPosition = Vec3.sum(this.currentObjectPosition, superHandMoved);
// ---------------- this tracks hand rotation
// var handChange = Quat.multiply(handRotation, Quat.inverse(this.handPreviousRotation));
// this.handPreviousRotation = handRotation;
// this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
// ----------------
// ---------------- this doubles hand rotation
var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation, 2.0),
// this doubles hand rotation
var handChange = Quat.multiply(Quat.slerp(this.handPreviousRotation, handRotation,
DISTANCE_HOLDING_ROTATION_EXAGGERATION_FACTOR),
Quat.inverse(this.handPreviousRotation));
this.handPreviousRotation = handRotation;
this.currentObjectRotation = Quat.multiply(handChange, this.currentObjectRotation);
// ----------------
Entities.updateAction(this.grabbedEntity, this.actionID, {
targetPosition: this.currentObjectPosition, linearTimeScale: .1,
targetRotation: this.currentObjectRotation, angularTimeScale: .1
targetPosition: this.currentObjectPosition, linearTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME,
targetRotation: this.currentObjectRotation, angularTimeScale: DISTANCE_HOLDING_ACTION_TIMEFRAME
});
}
}
}
controller.prototype.showPointer = function() {
Entities.editEntity(this.pointer, {
visible: true
});
}
controller.prototype.hidePointer = function() {
Entities.editEntity(this.pointer, {
visible: false
});
}
controller.prototype.letGo = function() {
if (this.grabbedEntity && this.actionID) {
this.deactivateEntity(this.grabbedEntity);
Entities.deleteAction(this.grabbedEntity, this.actionID);
}
this.grabbedEntity = null;
this.actionID = null;
this.distanceHolding = false;
this.closeGrabbing = false;
}
this.closeGrabbing = function() {
if (!this.triggerSmoothedSqueezed()) {
this.state = STATE_RELEASE;
return;
}
controller.prototype.update = function() {
this.triggerValue = Controller.getActionValue(this.triggerAction);
if (this.triggerValue > SHOW_LINE_THRESHOLD && this.prevTriggerValue < SHOW_LINE_THRESHOLD) {
//First check if an object is within close range and then run the close grabbing logic
if (this.checkForInRangeObject()) {
this.grabEntity();
this.lineOff();
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity);
var handRotation = this.getHandRotation();
var handPosition = this.getHandPosition();
var objectRotation = grabbedProperties.rotation;
var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation);
this.currentObjectPosition = grabbedProperties.position;
this.currentObjectTime = Date.now();
var offset = Vec3.subtract(this.currentObjectPosition, handPosition);
var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset);
this.actionID = Entities.addAction("hold", this.grabbedEntity, {
hand: this.hand == RIGHT_HAND ? "right" : "left",
timeScale: CLOSE_GRABBING_ACTION_TIMEFRAME,
relativePosition: offsetPosition,
relativeRotation: offsetRotation
});
if (this.actionID == NULL_ACTION_ID) {
this.actionID = null;
} else {
this.showPointer();
this.shouldDisplayLine = true;
}
} else if (this.triggerValue < SHOW_LINE_THRESHOLD && this.prevTriggerValue > SHOW_LINE_THRESHOLD) {
this.hidePointer();
this.letGo();
this.shouldDisplayLine = false;
}
if (this.shouldDisplayLine) {
this.updateLine();
}
if (this.triggerValue > DISTANCE_HOLD_THRESHOLD && !this.closeGrabbing) {
this.attemptMove();
}
this.prevTriggerValue = this.triggerValue;
}
controller.prototype.grabEntity = function() {
var handRotation = this.getHandRotation();
var handPosition = this.getHandPosition();
this.closeGrabbing = true;
//check if our entity has instructions on how to be grabbed, otherwise, just use default relative position and rotation
var userData = getEntityUserData(this.grabbedEntity);
var objectRotation = Entities.getEntityProperties(this.grabbedEntity).rotation;
var offsetRotation = Quat.multiply(Quat.inverse(handRotation), objectRotation);
var objectPosition = Entities.getEntityProperties(this.grabbedEntity).position;
var offset = Vec3.subtract(objectPosition, handPosition);
var offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, offsetRotation)), offset);
var relativePosition = offsetPosition;
var relativeRotation = offsetRotation;
if (userData.grabFrame) {
if (userData.grabFrame.relativePosition) {
relativePosition = userData.grabFrame.relativePosition;
}
if (userData.grabFrame.relativeRotation) {
relativeRotation = userData.grabFrame.relativeRotation;
this.state = STATE_CONTINUE_CLOSE_GRABBING;
}
}
this.actionID = Entities.addAction("hold", this.grabbedEntity, {
hand: this.hand,
timeScale: 0.05,
relativePosition: relativePosition,
relativeRotation: relativeRotation
});
}
controller.prototype.checkForInRangeObject = function() {
var handPosition = Controller.getSpatialControlPosition(this.palm);
var entities = Entities.findEntities(handPosition, GRAB_RADIUS);
var minDistance = GRAB_RADIUS;
var grabbedEntity = null;
//Get nearby entities and assign nearest
for (var i = 0; i < entities.length; i++) {
var props = Entities.getEntityProperties(entities[i]);
var distance = Vec3.distance(props.position, handPosition);
if (distance < minDistance && props.name !== "pointer" && props.collisionsWillMove === 1) {
grabbedEntity = entities[i];
minDistance = distance;
this.continueCloseGrabbing = function() {
if (!this.triggerSmoothedSqueezed()) {
this.state = STATE_RELEASE;
return;
}
}
if (grabbedEntity === null) {
return false;
} else {
//We are grabbing an entity, so let it know we've grabbed it
this.grabbedEntity = grabbedEntity;
this.activateEntity(this.grabbedEntity);
return true;
// keep track of the measured velocity of the held object
var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity);
var now = Date.now();
var deltaPosition = Vec3.subtract(grabbedProperties.position, this.currentObjectPosition); // meters
var deltaTime = (now - this.currentObjectTime) / MSEC_PER_SEC; // convert to seconds
if (deltaTime > 0.0) {
var grabbedVelocity = Vec3.multiply(deltaPosition, 1.0 / deltaTime);
// don't update grabbedVelocity if the trigger is off. the smoothing of the trigger
// value would otherwise give the held object time to slow down.
if (this.triggerSqueezed()) {
this.grabbedVelocity =
Vec3.sum(Vec3.multiply(this.grabbedVelocity,
(1.0 - CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO)),
Vec3.multiply(grabbedVelocity, CLOSE_GRABBING_VELOCITY_SMOOTH_RATIO));
}
}
this.currentObjectPosition = grabbedProperties.position;
this.currentObjectTime = now;
}
this.release = function() {
this.lineOff();
if (this.grabbedEntity != null && this.actionID != null) {
Entities.deleteAction(this.grabbedEntity, this.actionID);
}
// the action will tend to quickly bring an object's velocity to zero. now that
// the action is gone, set the objects velocity to something the holder might expect.
Entities.editEntity(this.grabbedEntity, {velocity: this.grabbedVelocity});
this.grabbedVelocity = ZERO_VEC;
this.grabbedEntity = null;
this.actionID = null;
this.state = STATE_SEARCHING;
}
this.cleanup = function() {
release();
}
}
controller.prototype.activateEntity = function(entity) {
var data = {
activated: true,
avatarId: MyAvatar.sessionUUID
};
setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data);
}
controller.prototype.deactivateEntity = function(entity) {
var data = {
activated: false,
avatarId: null
};
setEntityCustomData(GRAB_USER_DATA_KEY, this.grabbedEntity, data);
}
var rightController = new controller(RIGHT_HAND, Controller.findAction("RIGHT_HAND_CLICK"));
var leftController = new controller(LEFT_HAND, Controller.findAction("LEFT_HAND_CLICK"));
controller.prototype.cleanup = function() {
Entities.deleteEntity(this.pointer);
if (this.grabbedEntity) {
Entities.deleteAction(this.grabbedEntity, this.actionID);
}
}
function update() {
rightController.update();
leftController.update();
}
function cleanup() {
rightController.cleanup();
leftController.cleanup();
}
Script.scriptEnding.connect(cleanup);
Script.update.connect(update)