mirror of
https://github.com/overte-org/overte.git
synced 2025-04-26 10:16:19 +02:00
Replace 3d Overlays by Local Entities This is for the system files. Another PR will follow for the developer scripts.
873 lines
36 KiB
JavaScript
873 lines
36 KiB
JavaScript
"use strict";
|
|
|
|
// equipEntity.js
|
|
//
|
|
// Created by Seth Alves, August 14th, 2017.
|
|
// Copyright 2017 High Fidelity, Inc.
|
|
// Copyright 2023, Overte e.V.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
|
|
|
|
/* global Script, Entities, MyAvatar, Controller, RIGHT_HAND, LEFT_HAND, Camera, print, getControllerJointIndex,
|
|
enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters,
|
|
makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic,
|
|
entityIsCloneable, cloneEntity, DISPATCHER_PROPERTIES, Uuid, isInEditMode, getGrabbableData,
|
|
entityIsEquippable, HMD
|
|
*/
|
|
|
|
Script.include("/~/system/libraries/Xform.js");
|
|
Script.include("/~/system/libraries/controllerDispatcherUtils.js");
|
|
Script.include("/~/system/libraries/controllers.js");
|
|
Script.include("/~/system/libraries/cloneEntityUtils.js");
|
|
Script.include("/~/system/libraries/utils.js");
|
|
|
|
|
|
var DEFAULT_SPHERE_MODEL_URL = Script.getExternalPath(Script.ExternalPaths.HF_Content, "/alan/dev/equip-Fresnel-3.fbx");
|
|
var EQUIP_SPHERE_SCALE_FACTOR = 0.65;
|
|
|
|
|
|
// Each overlayInfoSet describes a single equip hotspot.
|
|
// It is an object with the following keys:
|
|
// timestamp - last time this object was updated, used to delete stale hotspot overlays.
|
|
// entityID - entity assosicated with this hotspot
|
|
// localPosition - position relative to the entity
|
|
// hotspot - hotspot object
|
|
// overlays - array of overlay objects created by Overlay.addOverlay()
|
|
// currentSize - current animated scale value
|
|
// targetSize - the target of our scale animations
|
|
// type - "sphere" or "model".
|
|
function EquipHotspotBuddy() {
|
|
// holds map from {string} hotspot.key to {object} overlayInfoSet.
|
|
this.map = {};
|
|
|
|
// array of all hotspots that are highlighed.
|
|
this.highlightedHotspots = [];
|
|
}
|
|
EquipHotspotBuddy.prototype.clear = function() {
|
|
var keys = Object.keys(this.map);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var overlayInfoSet = this.map[keys[i]];
|
|
this.deleteOverlayInfoSet(overlayInfoSet);
|
|
}
|
|
this.map = {};
|
|
this.highlightedHotspots = [];
|
|
};
|
|
EquipHotspotBuddy.prototype.highlightHotspot = function(hotspot) {
|
|
this.highlightedHotspots.push(hotspot.key);
|
|
};
|
|
EquipHotspotBuddy.prototype.updateHotspot = function(hotspot, timestamp) {
|
|
var overlayInfoSet = this.map[hotspot.key];
|
|
if (!overlayInfoSet) {
|
|
// create a new overlayInfoSet
|
|
overlayInfoSet = {
|
|
timestamp: timestamp,
|
|
entityID: hotspot.entityID,
|
|
localPosition: hotspot.localPosition,
|
|
hotspot: hotspot,
|
|
currentSize: 0,
|
|
targetSize: 1,
|
|
overlays: []
|
|
};
|
|
|
|
var dimensions = hotspot.radius * 2 * EQUIP_SPHERE_SCALE_FACTOR;
|
|
|
|
if (hotspot.indicatorURL) {
|
|
dimensions = hotspot.indicatorScale;
|
|
}
|
|
|
|
// override default sphere with a user specified model, if it exists.
|
|
overlayInfoSet.overlays.push(Entities.addEntity({
|
|
"type": "Model",
|
|
"name": "hotspot overlay",
|
|
"modelURL": hotspot.indicatorURL ? hotspot.indicatorURL : DEFAULT_SPHERE_MODEL_URL,
|
|
"position": hotspot.worldPosition,
|
|
"rotation": {
|
|
"x": 0,
|
|
"y": 0,
|
|
"z": 0,
|
|
"w": 1
|
|
},
|
|
"dimensions": dimensions,
|
|
"ignorePickIntersection": true
|
|
}, "local"));
|
|
overlayInfoSet.type = "model";
|
|
this.map[hotspot.key] = overlayInfoSet;
|
|
} else {
|
|
overlayInfoSet.timestamp = timestamp;
|
|
}
|
|
};
|
|
EquipHotspotBuddy.prototype.updateHotspots = function(hotspots, timestamp) {
|
|
var _this = this;
|
|
hotspots.forEach(function(hotspot) {
|
|
_this.updateHotspot(hotspot, timestamp);
|
|
});
|
|
this.highlightedHotspots = [];
|
|
};
|
|
EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerData) {
|
|
|
|
var HIGHLIGHT_SIZE = 1.1;
|
|
var NORMAL_SIZE = 1.0;
|
|
|
|
var keys = Object.keys(this.map);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var overlayInfoSet = this.map[keys[i]];
|
|
|
|
// this overlayInfo is highlighted.
|
|
if (this.highlightedHotspots.indexOf(keys[i]) !== -1) {
|
|
overlayInfoSet.targetSize = HIGHLIGHT_SIZE;
|
|
} else {
|
|
overlayInfoSet.targetSize = NORMAL_SIZE;
|
|
}
|
|
|
|
// start to fade out this hotspot.
|
|
if (overlayInfoSet.timestamp !== timestamp) {
|
|
overlayInfoSet.targetSize = 0;
|
|
}
|
|
|
|
// animate the size.
|
|
var SIZE_TIMESCALE = 0.1;
|
|
var tau = deltaTime / SIZE_TIMESCALE;
|
|
if (tau > 1.0) {
|
|
tau = 1.0;
|
|
}
|
|
overlayInfoSet.currentSize += (overlayInfoSet.targetSize - overlayInfoSet.currentSize) * tau;
|
|
|
|
if (overlayInfoSet.timestamp !== timestamp && overlayInfoSet.currentSize <= 0.05) {
|
|
// this is an old overlay, that has finished fading out, delete it!
|
|
overlayInfoSet.overlays.forEach(Entities.deleteEntity);
|
|
delete this.map[keys[i]];
|
|
} else {
|
|
// update overlay position, rotation to follow the object it's attached to.
|
|
var props = controllerData.nearbyEntityPropertiesByID[overlayInfoSet.entityID];
|
|
if (props) {
|
|
var entityXform = new Xform(props.rotation, props.position);
|
|
var position = entityXform.xformPoint(overlayInfoSet.localPosition);
|
|
|
|
var dimensions;
|
|
if (overlayInfoSet.hotspot.indicatorURL) {
|
|
var ratio = overlayInfoSet.currentSize / overlayInfoSet.targetSize;
|
|
dimensions = {
|
|
x: overlayInfoSet.hotspot.dimensions.x * ratio,
|
|
y: overlayInfoSet.hotspot.dimensions.y * ratio,
|
|
z: overlayInfoSet.hotspot.dimensions.z * ratio
|
|
};
|
|
} else {
|
|
dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize;
|
|
}
|
|
|
|
overlayInfoSet.overlays.forEach(function(overlay) {
|
|
Entities.editEntity(overlay, {
|
|
"position": position,
|
|
"rotation": props.rotation,
|
|
"dimensions": dimensions
|
|
});
|
|
});
|
|
} else {
|
|
overlayInfoSet.overlays.forEach(Entities.deleteEntity);
|
|
delete this.map[keys[i]];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
(function() {
|
|
|
|
var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints";
|
|
|
|
var HAPTIC_PULSE_STRENGTH = 1.0;
|
|
var HAPTIC_PULSE_DURATION = 13.0;
|
|
var HAPTIC_TEXTURE_STRENGTH = 0.1;
|
|
var HAPTIC_TEXTURE_DURATION = 3.0;
|
|
var HAPTIC_TEXTURE_DISTANCE = 0.002;
|
|
var HAPTIC_DEQUIP_STRENGTH = 0.75;
|
|
var HAPTIC_DEQUIP_DURATION = 50.0;
|
|
|
|
var TRIGGER_SMOOTH_RATIO = 0.1; // Time averaging of trigger - 0.0 disables smoothing
|
|
var TRIGGER_OFF_VALUE = 0.1;
|
|
var TRIGGER_ON_VALUE = TRIGGER_OFF_VALUE + 0.05; // Squeezed just enough to activate search or near grab
|
|
var BUMPER_ON_VALUE = 0.5;
|
|
var ATTACHPOINT_MAX_DISTANCE = 3.0;
|
|
|
|
// var EMPTY_PARENT_ID = "{00000000-0000-0000-0000-000000000000}";
|
|
|
|
var UNEQUIP_KEY = "u";
|
|
|
|
function getWearableData(props) {
|
|
if (props.grab.equippable) {
|
|
return {
|
|
joints: {
|
|
LeftHand: [ props.grab.equippableLeftPosition, props.grab.equippableLeftRotation ],
|
|
RightHand: [ props.grab.equippableRightPosition, props.grab.equippableRightRotation ]
|
|
},
|
|
indicatorURL: props.grab.equippableIndicatorURL,
|
|
indicatorScale: props.grab.equippableIndicatorScale,
|
|
indicatorOffset: props.grab.equippableIndicatorOffset
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getAttachPointSettings() {
|
|
try {
|
|
var str = Settings.getValue(ATTACH_POINT_SETTINGS);
|
|
if (str === "false" || str === "") {
|
|
return {};
|
|
} else {
|
|
return JSON.parse(str);
|
|
}
|
|
} catch (err) {
|
|
print("Error parsing attachPointSettings: " + err);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function setAttachPointSettings(attachPointSettings) {
|
|
var str = JSON.stringify(attachPointSettings);
|
|
Settings.setValue(ATTACH_POINT_SETTINGS, str);
|
|
}
|
|
|
|
function getAttachPointForHotspotFromSettings(hotspot, hand) {
|
|
var skeletonModelURL = MyAvatar.skeletonModelURL;
|
|
var attachPointSettings = getAttachPointSettings();
|
|
var avatarSettingsData = attachPointSettings[skeletonModelURL];
|
|
if (avatarSettingsData) {
|
|
var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand";
|
|
var joints = avatarSettingsData[hotspot.key];
|
|
if (joints) {
|
|
// make sure they are reasonable
|
|
if (joints[jointName] && joints[jointName][0] &&
|
|
Vec3.length(joints[jointName][0]) > ATTACHPOINT_MAX_DISTANCE) {
|
|
print("equipEntity -- Warning: rejecting settings attachPoint " + Vec3.length(joints[jointName][0]));
|
|
return undefined;
|
|
}
|
|
return joints[jointName];
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function storeAttachPointForHotspotInSettings(hotspot, hand, offsetPosition, offsetRotation) {
|
|
var attachPointSettings = getAttachPointSettings();
|
|
var skeletonModelURL = MyAvatar.skeletonModelURL;
|
|
var avatarSettingsData = attachPointSettings[skeletonModelURL];
|
|
if (!avatarSettingsData) {
|
|
avatarSettingsData = {};
|
|
attachPointSettings[skeletonModelURL] = avatarSettingsData;
|
|
}
|
|
var jointName = (hand === RIGHT_HAND) ? "RightHand" : "LeftHand";
|
|
var joints = avatarSettingsData[hotspot.key];
|
|
if (!joints) {
|
|
joints = {};
|
|
avatarSettingsData[hotspot.key] = joints;
|
|
}
|
|
joints[jointName] = [offsetPosition, offsetRotation];
|
|
setAttachPointSettings(attachPointSettings);
|
|
}
|
|
|
|
function clearAttachPoints() {
|
|
setAttachPointSettings({});
|
|
}
|
|
|
|
function EquipEntity(hand) {
|
|
this.hand = hand;
|
|
this.targetEntityID = null;
|
|
this.prevHandIsUpsideDown = false;
|
|
this.triggerValue = 0;
|
|
this.messageGrabEntity = false;
|
|
this.grabEntityProps = null;
|
|
this.shouldSendStart = false;
|
|
this.equipedWithSecondary = false;
|
|
this.handHasBeenRightsideUp = false;
|
|
|
|
this.parameters = makeDispatcherModuleParameters(
|
|
115,
|
|
this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip"] : ["leftHand", "leftHandEquip"],
|
|
[],
|
|
100);
|
|
|
|
var equipHotspotBuddy = new EquipHotspotBuddy();
|
|
|
|
this.setMessageGrabData = function(entityProperties) {
|
|
if (entityProperties) {
|
|
this.messageGrabEntity = true;
|
|
this.grabEntityProps = entityProperties;
|
|
}
|
|
};
|
|
|
|
// returns a list of all equip-hotspots assosiated with this entity.
|
|
// @param {UUID} entityID
|
|
// @returns {Object[]} array of objects with the following fields.
|
|
// * key {string} a string that can be used to uniquely identify this hotspot
|
|
// * entityID {UUID}
|
|
// * localPosition {Vec3} position of the hotspot in object space.
|
|
// * worldPosition {vec3} position of the hotspot in world space.
|
|
// * radius {number} radius of equip hotspot
|
|
// * joints {Object} keys are joint names values are arrays of two elements:
|
|
// offset position {Vec3} and offset rotation {Quat}, both are in the coordinate system of the joint.
|
|
// * indicatorURL {string} url for model to use instead of default sphere.
|
|
// * indicatorScale {Vec3} scale factor for model
|
|
this.collectEquipHotspots = function(props) {
|
|
var result = [];
|
|
var entityID = props.id;
|
|
var entityXform = new Xform(props.rotation, props.position);
|
|
|
|
var wearableProps = getWearableData(props);
|
|
var sensorToScaleFactor = MyAvatar.sensorToWorldScale;
|
|
if (wearableProps && wearableProps.joints) {
|
|
result.push({
|
|
key: entityID.toString() + "0",
|
|
entityID: entityID,
|
|
localPosition: wearableProps.indicatorOffset,
|
|
worldPosition: entityXform.pos,
|
|
radius: ((wearableProps.indicatorScale.x +
|
|
wearableProps.indicatorScale.y +
|
|
wearableProps.indicatorScale.z) / 3) * sensorToScaleFactor,
|
|
dimensions: wearableProps.indicatorScale,
|
|
joints: wearableProps.joints,
|
|
indicatorURL: wearableProps.indicatorURL,
|
|
indicatorScale: wearableProps.indicatorScale,
|
|
});
|
|
}
|
|
return result;
|
|
};
|
|
|
|
this.hotspotIsEquippable = function(hotspot, controllerData) {
|
|
var props = controllerData.nearbyEntityPropertiesByID[hotspot.entityID];
|
|
|
|
var hasParent = true;
|
|
if (props.parentID === Uuid.NULL) {
|
|
hasParent = false;
|
|
}
|
|
|
|
if (hasParent || entityHasActions(hotspot.entityID)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
this.handToController = function() {
|
|
return (this.hand === RIGHT_HAND) ? Controller.Standard.RightHand : Controller.Standard.LeftHand;
|
|
};
|
|
|
|
this.updateSmoothedTrigger = function(controllerData) {
|
|
var triggerValue = controllerData.triggerValues[this.hand];
|
|
// smooth out trigger value
|
|
this.triggerValue = (this.triggerValue * TRIGGER_SMOOTH_RATIO) +
|
|
(triggerValue * (1.0 - TRIGGER_SMOOTH_RATIO));
|
|
};
|
|
|
|
this.triggerSmoothedGrab = function() {
|
|
return this.triggerClicked;
|
|
};
|
|
|
|
this.triggerSmoothedSqueezed = function() {
|
|
return this.triggerValue > TRIGGER_ON_VALUE;
|
|
};
|
|
|
|
this.triggerSmoothedReleased = function() {
|
|
return this.triggerValue < TRIGGER_OFF_VALUE;
|
|
};
|
|
|
|
this.secondaryReleased = function() {
|
|
return this.rawSecondaryValue < BUMPER_ON_VALUE;
|
|
};
|
|
|
|
this.secondarySmoothedSqueezed = function() {
|
|
return this.rawSecondaryValue > BUMPER_ON_VALUE;
|
|
};
|
|
|
|
this.chooseNearEquipHotspots = function(candidateEntityProps, controllerData) {
|
|
var _this = this;
|
|
var collectedHotspots = flatten(candidateEntityProps.map(function(props) {
|
|
return _this.collectEquipHotspots(props);
|
|
}));
|
|
var controllerLocation = controllerData.controllerLocations[_this.hand];
|
|
var worldControllerPosition = controllerLocation.position;
|
|
var equippableHotspots = collectedHotspots.filter(function(hotspot) {
|
|
var hotspotDistance = Vec3.distance(hotspot.worldPosition, worldControllerPosition);
|
|
return _this.hotspotIsEquippable(hotspot, controllerData) &&
|
|
hotspotDistance < hotspot.radius;
|
|
});
|
|
return equippableHotspots;
|
|
};
|
|
|
|
this.cloneHotspot = function(props, controllerData) {
|
|
if (entityIsCloneable(props)) {
|
|
var cloneID = cloneEntity(props);
|
|
return cloneID;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
this.chooseBestEquipHotspot = function(candidateEntityProps, controllerData) {
|
|
var equippableHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData);
|
|
if (equippableHotspots.length > 0) {
|
|
// sort by distance;
|
|
var controllerLocation = controllerData.controllerLocations[this.hand];
|
|
var worldControllerPosition = controllerLocation.position;
|
|
equippableHotspots.sort(function(a, b) {
|
|
var aDistance = Vec3.distance(a.worldPosition, worldControllerPosition);
|
|
var bDistance = Vec3.distance(b.worldPosition, worldControllerPosition);
|
|
return aDistance - bDistance;
|
|
});
|
|
return equippableHotspots[0];
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
this.dropGestureReset = function() {
|
|
this.prevHandIsUpsideDown = false;
|
|
};
|
|
|
|
this.dropGestureProcess = function (deltaTime) {
|
|
var worldHandRotation = getControllerWorldLocation(this.handToController(), true).orientation;
|
|
var localHandUpAxis = this.hand === RIGHT_HAND ? { x: 1, y: 0, z: 0 } : { x: -1, y: 0, z: 0 };
|
|
var worldHandUpAxis = Vec3.multiplyQbyV(worldHandRotation, localHandUpAxis);
|
|
var DOWN = { x: 0, y: -1, z: 0 };
|
|
|
|
var DROP_ANGLE = Math.PI / 3;
|
|
var HYSTERESIS_FACTOR = 1.1;
|
|
var ROTATION_ENTER_THRESHOLD = Math.cos(DROP_ANGLE);
|
|
var ROTATION_EXIT_THRESHOLD = Math.cos(DROP_ANGLE * HYSTERESIS_FACTOR);
|
|
var rotationThreshold = this.prevHandIsUpsideDown ? ROTATION_EXIT_THRESHOLD : ROTATION_ENTER_THRESHOLD;
|
|
|
|
var handIsUpsideDown = false;
|
|
if (Vec3.dot(worldHandUpAxis, DOWN) > rotationThreshold) {
|
|
handIsUpsideDown = true;
|
|
}
|
|
|
|
if (handIsUpsideDown !== this.prevHandIsUpsideDown) {
|
|
this.prevHandIsUpsideDown = handIsUpsideDown;
|
|
Controller.triggerHapticPulse(HAPTIC_DEQUIP_STRENGTH, HAPTIC_DEQUIP_DURATION, this.hand);
|
|
}
|
|
|
|
return handIsUpsideDown;
|
|
};
|
|
|
|
this.clearEquipHaptics = function() {
|
|
this.prevPotentialEquipHotspot = null;
|
|
};
|
|
|
|
this.updateEquipHaptics = function(potentialEquipHotspot, currentLocation) {
|
|
if (potentialEquipHotspot && !this.prevPotentialEquipHotspot ||
|
|
!potentialEquipHotspot && this.prevPotentialEquipHotspot) {
|
|
Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand);
|
|
this.lastHapticPulseLocation = currentLocation;
|
|
} else if (potentialEquipHotspot &&
|
|
Vec3.distance(this.lastHapticPulseLocation, currentLocation) > HAPTIC_TEXTURE_DISTANCE) {
|
|
Controller.triggerHapticPulse(HAPTIC_TEXTURE_STRENGTH, HAPTIC_TEXTURE_DURATION, this.hand);
|
|
this.lastHapticPulseLocation = currentLocation;
|
|
}
|
|
this.prevPotentialEquipHotspot = potentialEquipHotspot;
|
|
};
|
|
|
|
this.startEquipEntity = function (controllerData) {
|
|
var _this = this;
|
|
|
|
this.dropGestureReset();
|
|
this.clearEquipHaptics();
|
|
Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand);
|
|
|
|
var grabbedProperties = Entities.getEntityProperties(this.targetEntityID, DISPATCHER_PROPERTIES);
|
|
var grabData = getGrabbableData(grabbedProperties);
|
|
|
|
// if an object is "equipped" and has a predefined offset, use it.
|
|
if (this.grabbedHotspot) {
|
|
var offsets = getAttachPointForHotspotFromSettings(this.grabbedHotspot, this.hand);
|
|
if (offsets) {
|
|
this.offsetPosition = offsets[0];
|
|
this.offsetRotation = offsets[1];
|
|
} else {
|
|
var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
|
|
if (this.grabbedHotspot.joints[handJointName]) {
|
|
this.offsetPosition = this.grabbedHotspot.joints[handJointName][0];
|
|
this.offsetRotation = this.grabbedHotspot.joints[handJointName][1];
|
|
}
|
|
}
|
|
}
|
|
|
|
var handJointIndex;
|
|
if (HMD.mounted && HMD.isHandControllerAvailable() && grabData.grabFollowsController) {
|
|
handJointIndex = this.controllerJointIndex;
|
|
} else {
|
|
handJointIndex = MyAvatar.getJointIndex(this.hand === RIGHT_HAND ? "RightHand" : "LeftHand");
|
|
}
|
|
|
|
var reparentProps = {
|
|
parentID: MyAvatar.SELF_ID,
|
|
parentJointIndex: handJointIndex,
|
|
localVelocity: {x: 0, y: 0, z: 0},
|
|
localAngularVelocity: {x: 0, y: 0, z: 0},
|
|
localPosition: this.offsetPosition,
|
|
localRotation: this.offsetRotation
|
|
};
|
|
|
|
var isClone = false;
|
|
if (entityIsCloneable(grabbedProperties)) {
|
|
var cloneID = this.cloneHotspot(grabbedProperties, controllerData);
|
|
this.targetEntityID = cloneID;
|
|
controllerData.nearbyEntityPropertiesByID[this.targetEntityID] = grabbedProperties;
|
|
isClone = true;
|
|
} else if (grabbedProperties.locked) {
|
|
this.grabbedHotspot = null;
|
|
this.targetEntityID = null;
|
|
return;
|
|
}
|
|
|
|
|
|
// HACK -- when
|
|
// https://highfidelity.fogbugz.com/f/cases/21767/entity-edits-shortly-after-an-add-often-fail
|
|
// is resolved, this can just be an editEntity rather than a setTimeout.
|
|
this.editDelayTimeout = Script.setTimeout(function () {
|
|
_this.editDelayTimeout = null;
|
|
Entities.editEntity(_this.targetEntityID, reparentProps);
|
|
}, 100);
|
|
|
|
// we don't want to send startEquip message until the trigger is released. otherwise,
|
|
// guns etc will fire right as they are equipped.
|
|
this.shouldSendStart = true;
|
|
|
|
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
|
|
action: 'equip',
|
|
grabbedEntity: this.targetEntityID,
|
|
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
|
|
}));
|
|
|
|
var grabEquipCheck = function() {
|
|
var args = [_this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
|
Entities.callEntityMethod(_this.targetEntityID, "startEquip", args);
|
|
};
|
|
|
|
if (isClone) {
|
|
// 100 ms seems to be sufficient time to force the check even occur after the object has been initialized.
|
|
Script.setTimeout(grabEquipCheck, 100);
|
|
}
|
|
};
|
|
|
|
this.endEquipEntity = function () {
|
|
|
|
if (this.editDelayTimeout) {
|
|
Script.clearTimeout(this.editDelayTimeout);
|
|
this.editDelayTimeout = null;
|
|
}
|
|
|
|
this.storeAttachPointInSettings();
|
|
Entities.editEntity(this.targetEntityID, {
|
|
parentID: Uuid.NULL,
|
|
parentJointIndex: -1
|
|
});
|
|
|
|
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
|
Entities.callEntityMethod(this.targetEntityID, "releaseEquip", args);
|
|
|
|
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
|
|
action: 'release',
|
|
grabbedEntity: this.targetEntityID,
|
|
joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand"
|
|
}));
|
|
|
|
ensureDynamic(this.targetEntityID);
|
|
this.targetEntityID = null;
|
|
this.messageGrabEntity = false;
|
|
this.grabEntityProps = null;
|
|
};
|
|
|
|
this.updateInputs = function (controllerData) {
|
|
this.rawTriggerValue = controllerData.triggerValues[this.hand];
|
|
this.triggerClicked = controllerData.triggerClicks[this.hand];
|
|
this.rawSecondaryValue = controllerData.secondaryValues[this.hand];
|
|
this.updateSmoothedTrigger(controllerData);
|
|
};
|
|
|
|
this.checkNearbyHotspots = function (controllerData, deltaTime, timestamp) {
|
|
this.controllerJointIndex = getControllerJointIndex(this.hand);
|
|
|
|
if (this.triggerSmoothedReleased() && this.secondaryReleased()) {
|
|
this.waitForTriggerRelease = false;
|
|
}
|
|
|
|
var controllerLocation = getControllerWorldLocation(this.handToController(), true);
|
|
var worldHandPosition = controllerLocation.position;
|
|
var candidateEntityProps = controllerData.nearbyEntityProperties[this.hand];
|
|
|
|
|
|
var potentialEquipHotspot = null;
|
|
if (this.messageGrabEntity) {
|
|
var hotspots = this.collectEquipHotspots(this.grabEntityProps);
|
|
if (hotspots.length > -1) {
|
|
potentialEquipHotspot = hotspots[0];
|
|
}
|
|
} else {
|
|
potentialEquipHotspot = this.chooseBestEquipHotspot(candidateEntityProps, controllerData);
|
|
}
|
|
|
|
if (!this.waitForTriggerRelease) {
|
|
this.updateEquipHaptics(potentialEquipHotspot, worldHandPosition);
|
|
}
|
|
|
|
var nearEquipHotspots = this.chooseNearEquipHotspots(candidateEntityProps, controllerData);
|
|
equipHotspotBuddy.updateHotspots(nearEquipHotspots, timestamp);
|
|
if (potentialEquipHotspot) {
|
|
equipHotspotBuddy.highlightHotspot(potentialEquipHotspot);
|
|
}
|
|
|
|
equipHotspotBuddy.update(deltaTime, timestamp, controllerData);
|
|
|
|
// if the potentialHotspot is cloneable, clone it and return it
|
|
// if the potentialHotspot is not cloneable and locked return null
|
|
if (potentialEquipHotspot &&
|
|
(((this.triggerSmoothedSqueezed() || this.secondarySmoothedSqueezed()) && !this.waitForTriggerRelease) ||
|
|
this.messageGrabEntity)) {
|
|
this.grabbedHotspot = potentialEquipHotspot;
|
|
this.targetEntityID = this.grabbedHotspot.entityID;
|
|
this.startEquipEntity(controllerData);
|
|
this.equipedWithSecondary = this.secondarySmoothedSqueezed();
|
|
return makeRunningValues(true, [this.targetEntityID], []);
|
|
} else {
|
|
return makeRunningValues(false, [], []);
|
|
}
|
|
};
|
|
|
|
this.isTargetIDValid = function(controllerData) {
|
|
var entityProperties = controllerData.nearbyEntityPropertiesByID[this.targetEntityID];
|
|
return entityProperties && "type" in entityProperties;
|
|
};
|
|
|
|
this.isReady = function (controllerData, deltaTime) {
|
|
var timestamp = Date.now();
|
|
this.updateInputs(controllerData);
|
|
this.handHasBeenRightsideUp = false;
|
|
return this.checkNearbyHotspots(controllerData, deltaTime, timestamp);
|
|
};
|
|
|
|
this.run = function (controllerData, deltaTime) {
|
|
var timestamp = Date.now();
|
|
this.updateInputs(controllerData);
|
|
|
|
if (!this.messageGrabEntity && !this.isTargetIDValid(controllerData)) {
|
|
this.endEquipEntity();
|
|
return makeRunningValues(false, [], []);
|
|
}
|
|
|
|
if (!this.targetEntityID) {
|
|
return this.checkNearbyHotspots(controllerData, deltaTime, timestamp);
|
|
}
|
|
|
|
if (controllerData.secondaryValues[this.hand] && !this.equipedWithSecondary) {
|
|
// this.secondaryReleased() will always be true when not depressed
|
|
// so we cannot simply rely on that for release - ensure that the
|
|
// trigger was first "prepared" by being pushed in before the release
|
|
this.preparingHoldRelease = true;
|
|
}
|
|
|
|
if (this.preparingHoldRelease && !controllerData.secondaryValues[this.hand]) {
|
|
// we have an equipped object and the secondary trigger was released
|
|
// short-circuit the other checks and release it
|
|
this.preparingHoldRelease = false;
|
|
this.endEquipEntity();
|
|
return makeRunningValues(false, [], []);
|
|
}
|
|
|
|
var handIsUpsideDown = this.dropGestureProcess(deltaTime);
|
|
var dropDetected = false;
|
|
if (this.handHasBeenRightsideUp) {
|
|
dropDetected = handIsUpsideDown;
|
|
}
|
|
if (!handIsUpsideDown) {
|
|
this.handHasBeenRightsideUp = true;
|
|
}
|
|
|
|
if (this.triggerSmoothedReleased() || this.secondaryReleased()) {
|
|
if (this.shouldSendStart) {
|
|
// we don't want to send startEquip message until the trigger is released. otherwise,
|
|
// guns etc will fire right as they are equipped.
|
|
var startArgs = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
|
Entities.callEntityMethod(this.targetEntityID, "startEquip", startArgs);
|
|
this.shouldSendStart = false;
|
|
}
|
|
this.waitForTriggerRelease = false;
|
|
if (this.secondaryReleased() && this.equipedWithSecondary) {
|
|
this.equipedWithSecondary = false;
|
|
}
|
|
}
|
|
|
|
if (dropDetected && this.prevDropDetected !== dropDetected) {
|
|
this.waitForTriggerRelease = true;
|
|
}
|
|
|
|
// highlight the grabbed hotspot when the dropGesture is detected.
|
|
if (dropDetected && this.grabbedHotspot) {
|
|
equipHotspotBuddy.updateHotspot(this.grabbedHotspot, timestamp);
|
|
equipHotspotBuddy.highlightHotspot(this.grabbedHotspot);
|
|
}
|
|
|
|
if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) {
|
|
this.waitForTriggerRelease = true;
|
|
// store the offset attach points into preferences.
|
|
this.endEquipEntity();
|
|
return makeRunningValues(false, [], []);
|
|
}
|
|
this.prevDropDetected = dropDetected;
|
|
|
|
equipHotspotBuddy.update(deltaTime, timestamp, controllerData);
|
|
|
|
if (!this.shouldSendStart) {
|
|
var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID];
|
|
Entities.callEntityMethod(this.targetEntityID, "continueEquip", args);
|
|
}
|
|
|
|
return makeRunningValues(true, [this.targetEntityID], []);
|
|
};
|
|
|
|
this.storeAttachPointInSettings = function() {
|
|
if (this.grabbedHotspot && this.targetEntityID) {
|
|
var prefProps = Entities.getEntityProperties(this.targetEntityID, ["localPosition", "localRotation"]);
|
|
if (prefProps && prefProps.localPosition && prefProps.localRotation) {
|
|
storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand,
|
|
prefProps.localPosition, prefProps.localRotation);
|
|
}
|
|
}
|
|
};
|
|
|
|
this.cleanup = function () {
|
|
if (this.targetEntityID) {
|
|
this.endEquipEntity();
|
|
}
|
|
};
|
|
}
|
|
|
|
var handleMessage = function(channel, message, sender) {
|
|
var data;
|
|
if (sender === MyAvatar.sessionUUID) {
|
|
if (channel === 'Hifi-Hand-Grab') {
|
|
try {
|
|
data = JSON.parse(message);
|
|
var equipModule = (data.hand === "left") ? leftEquipEntity : rightEquipEntity;
|
|
var entityProperties = Entities.getEntityProperties(data.entityID, DISPATCHER_PROPERTIES);
|
|
entityProperties.id = data.entityID;
|
|
equipModule.setMessageGrabData(entityProperties);
|
|
} catch (e) {
|
|
print("WARNING: equipEntity.js -- error parsing Hifi-Hand-Grab message: " + message);
|
|
}
|
|
} else if (channel === 'Hifi-Hand-Drop') {
|
|
if (message === "left") {
|
|
leftEquipEntity.endEquipEntity();
|
|
} else if (message === "right") {
|
|
rightEquipEntity.endEquipEntity();
|
|
} else if (message === "both") {
|
|
leftEquipEntity.endEquipEntity();
|
|
rightEquipEntity.endEquipEntity();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var clearGrabActions = function(entityID) {
|
|
var actionIDs = Entities.getActionIDs(entityID);
|
|
var myGrabTag = "grab-" + MyAvatar.sessionUUID;
|
|
for (var actionIndex = 0; actionIndex < actionIDs.length; actionIndex++) {
|
|
var actionID = actionIDs[actionIndex];
|
|
var actionArguments = Entities.getActionArguments(entityID, actionID);
|
|
var tag = actionArguments.tag;
|
|
if (tag === myGrabTag) {
|
|
Entities.deleteAction(entityID, actionID);
|
|
}
|
|
}
|
|
};
|
|
|
|
var onMousePress = function(event) {
|
|
if (isInEditMode() || !event.isLeftButton) { // don't consider any left clicks on the entity while in edit
|
|
return;
|
|
}
|
|
var pickRay = Camera.computePickRay(event.x, event.y);
|
|
var intersection = Entities.findRayIntersection(pickRay, true);
|
|
if (intersection.intersects) {
|
|
var entityID = intersection.entityID;
|
|
var entityProperties = Entities.getEntityProperties(entityID, DISPATCHER_PROPERTIES);
|
|
entityProperties.id = entityID;
|
|
var hasEquipData = getWearableData(entityProperties);
|
|
if (hasEquipData && entityIsEquippable(entityProperties)) {
|
|
entityProperties.id = entityID;
|
|
var rightHandPosition = MyAvatar.getJointPosition("RightHand");
|
|
var leftHandPosition = MyAvatar.getJointPosition("LeftHand");
|
|
var distanceToRightHand = Vec3.distance(entityProperties.position, rightHandPosition);
|
|
var distanceToLeftHand = Vec3.distance(entityProperties.position, leftHandPosition);
|
|
var leftHandAvailable = leftEquipEntity.targetEntityID === null;
|
|
var rightHandAvailable = rightEquipEntity.targetEntityID === null;
|
|
if (rightHandAvailable && (distanceToRightHand < distanceToLeftHand || !leftHandAvailable)) {
|
|
// clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags)
|
|
clearGrabActions(entityID);
|
|
rightEquipEntity.setMessageGrabData(entityProperties);
|
|
} else if (leftHandAvailable && (distanceToLeftHand < distanceToRightHand || !rightHandAvailable)) {
|
|
// clear any existing grab actions on the entity now (their later removal could affect bootstrapping flags)
|
|
clearGrabActions(entityID);
|
|
leftEquipEntity.setMessageGrabData(entityProperties);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var onKeyPress = function(event) {
|
|
if (event.text.toLowerCase() === UNEQUIP_KEY) {
|
|
if (rightEquipEntity.targetEntityID) {
|
|
rightEquipEntity.endEquipEntity();
|
|
}
|
|
if (leftEquipEntity.targetEntityID) {
|
|
leftEquipEntity.endEquipEntity();
|
|
}
|
|
}
|
|
};
|
|
|
|
var deleteEntity = function(entityID) {
|
|
if (rightEquipEntity.targetEntityID === entityID) {
|
|
rightEquipEntity.endEquipEntity();
|
|
}
|
|
if (leftEquipEntity.targetEntityID === entityID) {
|
|
leftEquipEntity.endEquipEntity();
|
|
}
|
|
};
|
|
|
|
var clearEntities = function() {
|
|
if (rightEquipEntity.targetEntityID) {
|
|
rightEquipEntity.endEquipEntity();
|
|
}
|
|
if (leftEquipEntity.targetEntityID) {
|
|
leftEquipEntity.endEquipEntity();
|
|
}
|
|
};
|
|
|
|
Messages.subscribe('Hifi-Hand-Grab');
|
|
Messages.subscribe('Hifi-Hand-Drop');
|
|
Messages.messageReceived.connect(handleMessage);
|
|
Controller.mousePressEvent.connect(onMousePress);
|
|
Controller.keyPressEvent.connect(onKeyPress);
|
|
Entities.deletingEntity.connect(deleteEntity);
|
|
Entities.clearingEntities.connect(clearEntities);
|
|
|
|
var leftEquipEntity = new EquipEntity(LEFT_HAND);
|
|
var rightEquipEntity = new EquipEntity(RIGHT_HAND);
|
|
|
|
enableDispatcherModule("LeftEquipEntity", leftEquipEntity);
|
|
enableDispatcherModule("RightEquipEntity", rightEquipEntity);
|
|
|
|
function cleanup() {
|
|
leftEquipEntity.cleanup();
|
|
rightEquipEntity.cleanup();
|
|
disableDispatcherModule("LeftEquipEntity");
|
|
disableDispatcherModule("RightEquipEntity");
|
|
clearAttachPoints();
|
|
Messages.messageReceived.disconnect(handleMessage);
|
|
Controller.mousePressEvent.disconnect(onMousePress);
|
|
Controller.keyPressEvent.disconnect(onKeyPress);
|
|
Entities.deletingEntity.disconnect(deleteEntity);
|
|
Entities.clearingEntities.disconnect(clearEntities);
|
|
}
|
|
Script.scriptEnding.connect(cleanup);
|
|
}());
|