diff --git a/interface/resources/avatar/animations/sitting.fbx b/interface/resources/avatar/animations/sitting.fbx new file mode 100644 index 0000000000..dfb51afb66 Binary files /dev/null and b/interface/resources/avatar/animations/sitting.fbx differ diff --git a/interface/resources/avatar/animations/sitting_idle.fbx b/interface/resources/avatar/animations/sitting_idle.fbx new file mode 100644 index 0000000000..ee03d942cd Binary files /dev/null and b/interface/resources/avatar/animations/sitting_idle.fbx differ diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 842939d938..969268c549 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -260,6 +260,11 @@ QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { return AvatarData::toByteArrayStateful(dataDetail); } +void MyAvatar::resetSensorsAndBody() { + qApp->getActiveDisplayPlugin()->resetSensors(); + reset(true, false, true); +} + void MyAvatar::centerBody() { if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "centerBody"); @@ -2483,6 +2488,45 @@ glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { } } +bool MyAvatar::pinJoint(int index, const glm::vec3& position, const glm::quat& orientation) { + auto hipsIndex = getJointIndex("Hips"); + if (index != hipsIndex) { + qWarning() << "Pinning is only supported for the hips joint at the moment."; + return false; + } + + setPosition(position); + setOrientation(orientation); + + _rig->setMaxHipsOffsetLength(0.05f); + + auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index); + if (it == _pinnedJoints.end()) { + _pinnedJoints.push_back(index); + } + + return true; +} + +bool MyAvatar::clearPinOnJoint(int index) { + auto it = std::find(_pinnedJoints.begin(), _pinnedJoints.end(), index); + if (it != _pinnedJoints.end()) { + _pinnedJoints.erase(it); + + auto hipsIndex = getJointIndex("Hips"); + if (index == hipsIndex) { + _rig->setMaxHipsOffsetLength(FLT_MAX); + } + + return true; + } + return false; +} + +float MyAvatar::getIKErrorOnLastSolve() const { + return _rig->getIKErrorOnLastSolve(); +} + // thread-safe void MyAvatar::addHoldAction(AvatarActionHold* holdAction) { std::lock_guard guard(_holdActionsMutex); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 4f86256a2f..3cc665b533 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -99,6 +99,7 @@ public: void reset(bool andRecenter = false, bool andReload = true, bool andHead = true); + Q_INVOKABLE void resetSensorsAndBody(); Q_INVOKABLE void centerBody(); // thread-safe Q_INVOKABLE void clearIKJointLimitHistory(); // thread-safe @@ -216,6 +217,11 @@ public: virtual void clearJointData(int index) override; virtual void clearJointsData() override; + Q_INVOKABLE bool pinJoint(int index, const glm::vec3& position, const glm::quat& orientation); + Q_INVOKABLE bool clearPinOnJoint(int index); + + Q_INVOKABLE float getIKErrorOnLastSolve() const; + Q_INVOKABLE void useFullAvatarURL(const QUrl& fullAvatarURL, const QString& modelName = QString()); Q_INVOKABLE QUrl getFullAvatarURLFromPreferences() const { return _fullAvatarURLFromPreferences; } Q_INVOKABLE QString getFullAvatarModelName() const { return _fullAvatarModelName; } @@ -527,6 +533,8 @@ private: bool didTeleport(); bool getIsAway() const { return _isAway; } void setAway(bool value); + + std::vector _pinnedJoints; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index adeede17ad..173af3fdf6 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -189,6 +189,7 @@ void AnimInverseKinematics::solveWithCyclicCoordinateDescent(const std::vectorisLowerSpine()) { + if (constraint && constraint->isLowerSpine() && tipIndex != _headIndex) { // for these types of targets we only allow twist at the lower-spine // (this prevents the hand targets from bending the spine too much and thereby driving the hips too far) glm::vec3 twistAxis = absolutePoses[pivotIndex].trans() - absolutePoses[pivotsParentIndex].trans(); @@ -300,8 +301,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe const float MIN_ADJUSTMENT_ANGLE = 1.0e-4f; if (angle > MIN_ADJUSTMENT_ANGLE) { // reduce angle by a fraction (for stability) - const float FRACTION = 0.5f; - angle *= FRACTION; + const float STABILITY_FRACTION = 0.5f; + angle *= STABILITY_FRACTION; deltaRotation = glm::angleAxis(angle, axis); // The swing will re-orient the tip but there will tend to be be a non-zero delta between the tip's @@ -323,7 +324,8 @@ int AnimInverseKinematics::solveTargetWithCCD(const IKTarget& target, AnimPoseVe glm::vec3 axis = glm::normalize(deltaRotation * leverArm); swingTwistDecomposition(missingRotation, axis, swingPart, twistPart); float dotSign = copysignf(1.0f, twistPart.w); - deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, FRACTION)) * deltaRotation; + const float LIMIT_LEAK_FRACTION = 0.1f; + deltaRotation = glm::normalize(glm::lerp(glm::quat(), dotSign * twistPart, LIMIT_LEAK_FRACTION)) * deltaRotation; } } } @@ -486,7 +488,13 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars // measure new _hipsOffset for next frame // by looking for discrepancies between where a targeted endEffector is // and where it wants to be (after IK solutions are done) - glm::vec3 newHipsOffset = Vectors::ZERO; + + // use weighted average between HMD and other targets + float HMD_WEIGHT = 10.0f; + float OTHER_WEIGHT = 1.0f; + float totalWeight = 0.0f; + + glm::vec3 additionalHipsOffset = Vectors::ZERO; for (auto& target: targets) { int targetIndex = target.getIndex(); if (targetIndex == _headIndex && _headIndex != -1) { @@ -497,32 +505,61 @@ const AnimPoseVec& AnimInverseKinematics::overlay(const AnimVariantMap& animVars glm::vec3 under = _skeleton->getAbsolutePose(_headIndex, underPoses).trans(); glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); const float HEAD_OFFSET_SLAVE_FACTOR = 0.65f; - newHipsOffset += HEAD_OFFSET_SLAVE_FACTOR * (actual - under); + additionalHipsOffset += (OTHER_WEIGHT * HEAD_OFFSET_SLAVE_FACTOR) * (under- actual); + totalWeight += OTHER_WEIGHT; } else if (target.getType() == IKTarget::Type::HmdHead) { - // we want to shift the hips to bring the head to its designated position glm::vec3 actual = _skeleton->getAbsolutePose(_headIndex, _relativePoses).trans(); - _hipsOffset += target.getTranslation() - actual; - // and ignore all other targets - newHipsOffset = _hipsOffset; - break; + glm::vec3 thisOffset = target.getTranslation() - actual; + glm::vec3 futureHipsOffset = _hipsOffset + thisOffset; + if (glm::length(glm::vec2(futureHipsOffset.x, futureHipsOffset.z)) < _maxHipsOffsetLength) { + // it is imperative to shift the hips and bring the head to its designated position + // so we slam newHipsOffset here and ignore all other targets + additionalHipsOffset = futureHipsOffset - _hipsOffset; + totalWeight = 0.0f; + break; + } else { + additionalHipsOffset += HMD_WEIGHT * (target.getTranslation() - actual); + totalWeight += HMD_WEIGHT; + } } } else if (target.getType() == IKTarget::Type::RotationAndPosition) { glm::vec3 actualPosition = _skeleton->getAbsolutePose(targetIndex, _relativePoses).trans(); glm::vec3 targetPosition = target.getTranslation(); - newHipsOffset += targetPosition - actualPosition; + additionalHipsOffset += OTHER_WEIGHT * (targetPosition - actualPosition); + totalWeight += OTHER_WEIGHT; } } + if (totalWeight > 1.0f) { + additionalHipsOffset /= totalWeight; + } + + // Add downward pressure on the hips + additionalHipsOffset *= 0.95f; + additionalHipsOffset -= 1.0f; // smooth transitions by relaxing _hipsOffset toward the new value - const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.15f; + const float HIPS_OFFSET_SLAVE_TIMESCALE = 0.10f; float tau = dt < HIPS_OFFSET_SLAVE_TIMESCALE ? dt / HIPS_OFFSET_SLAVE_TIMESCALE : 1.0f; - _hipsOffset += (newHipsOffset - _hipsOffset) * tau; + _hipsOffset += additionalHipsOffset * tau; + + // clamp the hips offset + float hipsOffsetLength = glm::length(_hipsOffset); + if (hipsOffsetLength > _maxHipsOffsetLength) { + _hipsOffset *= _maxHipsOffsetLength / hipsOffsetLength; + } + } } } return _relativePoses; } +void AnimInverseKinematics::setMaxHipsOffsetLength(float maxLength) { + // manually adjust scale here + const float METERS_TO_CENTIMETERS = 100.0f; + _maxHipsOffsetLength = METERS_TO_CENTIMETERS * maxLength; +} + void AnimInverseKinematics::clearIKJointLimitHistory() { for (auto& pair : _constraints) { pair.second->clearHistory(); @@ -740,7 +777,7 @@ void AnimInverseKinematics::initConstraints() { stConstraint->setTwistLimits(-MAX_SPINE_TWIST, MAX_SPINE_TWIST); std::vector minDots; - const float MAX_SPINE_SWING = PI / 14.0f; + const float MAX_SPINE_SWING = PI / 10.0f; minDots.push_back(cosf(MAX_SPINE_SWING)); stConstraint->setSwingLimits(minDots); if (0 == baseName.compare("Spine1", Qt::CaseSensitive) @@ -776,11 +813,11 @@ void AnimInverseKinematics::initConstraints() { } else if (0 == baseName.compare("Head", Qt::CaseSensitive)) { SwingTwistConstraint* stConstraint = new SwingTwistConstraint(); stConstraint->setReferenceRotation(_defaultRelativePoses[i].rot()); - const float MAX_HEAD_TWIST = PI / 9.0f; + const float MAX_HEAD_TWIST = PI / 6.0f; stConstraint->setTwistLimits(-MAX_HEAD_TWIST, MAX_HEAD_TWIST); std::vector minDots; - const float MAX_HEAD_SWING = PI / 10.0f; + const float MAX_HEAD_SWING = PI / 6.0f; minDots.push_back(cosf(MAX_HEAD_SWING)); stConstraint->setSwingLimits(minDots); diff --git a/libraries/animation/src/AnimInverseKinematics.h b/libraries/animation/src/AnimInverseKinematics.h index c9560c7383..892a5616b2 100644 --- a/libraries/animation/src/AnimInverseKinematics.h +++ b/libraries/animation/src/AnimInverseKinematics.h @@ -39,6 +39,10 @@ public: void clearIKJointLimitHistory(); + void setMaxHipsOffsetLength(float maxLength); + + float getMaxErrorOnLastSolve() { return _maxErrorOnLastSolve; } + protected: void computeTargets(const AnimVariantMap& animVars, std::vector& targets, const AnimPoseVec& underPoses); void solveWithCyclicCoordinateDescent(const std::vector& targets); @@ -83,6 +87,7 @@ protected: // experimental data for moving hips during IK glm::vec3 _hipsOffset { Vectors::ZERO }; + float _maxHipsOffsetLength{ FLT_MAX }; int _headIndex { -1 }; int _hipsIndex { -1 }; int _hipsParentIndex { -1 }; @@ -90,6 +95,8 @@ protected: // _maxTargetIndex is tracked to help optimize the recalculation of absolute poses // during the the cyclic coordinate descent algorithm int _maxTargetIndex { 0 }; + + float _maxErrorOnLastSolve { FLT_MAX }; }; #endif // hifi_AnimInverseKinematics_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 07462e9878..c47da7c0b0 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -319,6 +319,39 @@ void Rig::clearIKJointLimitHistory() { } } +void Rig::setMaxHipsOffsetLength(float maxLength) { + _maxHipsOffsetLength = maxLength; + + if (_animNode) { + _animNode->traverse([&](AnimNode::Pointer node) { + auto ikNode = std::dynamic_pointer_cast(node); + if (ikNode) { + ikNode->setMaxHipsOffsetLength(_maxHipsOffsetLength); + } + return true; + }); + } +} + +float Rig::getMaxHipsOffsetLength() const { + return _maxHipsOffsetLength; +} + +float Rig::getIKErrorOnLastSolve() const { + float result = 0.0f; + + if (_animNode) { + _animNode->traverse([&](AnimNode::Pointer node) { + auto ikNode = std::dynamic_pointer_cast(node); + if (ikNode) { + result = ikNode->getMaxErrorOnLastSolve(); + } + return true; + }); + } + return result; +} + int Rig::getJointParentIndex(int childIndex) const { if (_animSkeleton && isIndexValid(childIndex)) { return _animSkeleton->getParentIndex(childIndex); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 78a669b249..f1c87d0d3e 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -104,6 +104,10 @@ public: void clearJointAnimationPriority(int index); void clearIKJointLimitHistory(); + void setMaxHipsOffsetLength(float maxLength); + float getMaxHipsOffsetLength() const; + + float getIKErrorOnLastSolve() const; int getJointParentIndex(int childIndex) const; @@ -318,6 +322,8 @@ protected: bool _enabledAnimations { true }; mutable uint32_t _jointNameWarningCount { 0 }; + float _maxHipsOffsetLength { 1.0f }; + float _maxErrorOnLastSolve { 0.0f }; private: QMap _stateHandlers; diff --git a/scripts/system/assets/models/teleport-seat.fbx b/scripts/system/assets/models/teleport-seat.fbx new file mode 100644 index 0000000000..cd7a9abc7e Binary files /dev/null and b/scripts/system/assets/models/teleport-seat.fbx differ diff --git a/scripts/system/controllers/teleport.js b/scripts/system/controllers/teleport.js index d3284352bf..c058f046db 100644 --- a/scripts/system/controllers/teleport.js +++ b/scripts/system/controllers/teleport.js @@ -17,12 +17,20 @@ var NUMBER_OF_STEPS = 6; var TARGET_MODEL_URL = Script.resolvePath("../assets/models/teleport-destination.fbx"); var TOO_CLOSE_MODEL_URL = Script.resolvePath("../assets/models/teleport-cancel.fbx"); +var SEAT_MODEL_URL = Script.resolvePath("../assets/models/teleport-seat.fbx"); + var TARGET_MODEL_DIMENSIONS = { x: 1.15, y: 0.5, z: 1.15 }; +var COLORS_TELEPORT_SEAT = { + red: 255, + green: 0, + blue: 170 +} + var COLORS_TELEPORT_CAN_TELEPORT = { red: 97, green: 247, @@ -35,29 +43,30 @@ var COLORS_TELEPORT_CANNOT_TELEPORT = { blue: 141 }; -var COLORS_TELEPORT_TOO_CLOSE = { +var COLORS_TELEPORT_CANCEL = { red: 255, green: 184, blue: 73 }; var TELEPORT_CANCEL_RANGE = 1; -var USE_COOL_IN = true; var COOL_IN_DURATION = 500; +const handInfo = { + right: { + controllerInput: Controller.Standard.RightHand + }, + left: { + controllerInput: Controller.Standard.LeftHand + } +}; + function ThumbPad(hand) { this.hand = hand; var _thisPad = this; this.buttonPress = function(value) { _thisPad.buttonValue = value; - if (value === 0) { - if (activationTimeout !== null) { - Script.clearTimeout(activationTimeout); - activationTimeout = null; - } - - } }; } @@ -67,7 +76,6 @@ function Trigger(hand) { this.buttonPress = function(value) { _this.buttonValue = value; - }; this.down = function() { @@ -78,347 +86,224 @@ function Trigger(hand) { var coolInTimeout = null; +var TELEPORTER_STATES = { + IDLE: 'idle', + COOL_IN: 'cool_in', + TARGETTING_INVALID: 'targetting_invalid', +} + +var TARGET = { + NONE: 'none', // Not currently targetting anything + INVISIBLE: 'invisible', // The current target is an invvsible surface + INVALID: 'invalid', // The current target is invalid (wall, ceiling, etc.) + SURFACE: 'surface', // The current target is a valid surface + SEAT: 'seat', // The current target is a seat +} + function Teleporter() { var _this = this; - this.intersection = null; - this.rightOverlayLine = null; - this.leftOverlayLine = null; - this.targetOverlay = null; - this.cancelOverlay = null; - this.updateConnected = null; - this.smoothArrivalInterval = null; - this.teleportHand = null; - this.tooClose = false; - this.inCoolIn = false; + this.active = false; + this.state = TELEPORTER_STATES.IDLE; + this.currentTarget = TARGET.INVALID; - this.initialize = function() { - this.createMappings(); + this.overlayLines = { + left: null, + right: null, }; + this.updateConnected = null; + this.activeHand = null; - this.createMappings = function() { - teleporter.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); - teleporter.teleportMappingInternal = Controller.newMapping(teleporter.telporterMappingInternalName); + this.telporterMappingInternalName = 'Hifi-Teleporter-Internal-Dev-' + Math.random(); + this.teleportMappingInternal = Controller.newMapping(this.telporterMappingInternalName); - Controller.enableMapping(teleporter.telporterMappingInternalName); + // Setup overlays + this.cancelOverlay = Overlays.addOverlay("model", { + url: TOO_CLOSE_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: false + }); + this.targetOverlay = Overlays.addOverlay("model", { + url: TARGET_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: false + }); + this.seatOverlay = Overlays.addOverlay("model", { + url: SEAT_MODEL_URL, + dimensions: TARGET_MODEL_DIMENSIONS, + visible: false + }); + + this.enableMappings = function() { + Controller.enableMapping(this.telporterMappingInternalName); }; this.disableMappings = function() { Controller.disableMapping(teleporter.telporterMappingInternalName); }; - this.enterTeleportMode = function(hand) { + this.cleanup = function() { + this.disableMappings(); + Overlays.deleteOverlay(this.targetOverlay); + this.targetOverlay = null; + + Overlays.deleteOverlay(this.cancelOverlay); + this.cancelOverlay = null; + + Overlays.deleteOverlay(this.seatOverlay); + this.seatOverlay = null; + + this.deleteOverlayBeams(); + if (this.updateConnected === true) { + Script.update.disconnect(this, this.update); + } + }; + + this.enterTeleportMode = function(hand) { if (inTeleportMode === true) { return; } - if (isDisabled === 'both') { + if (isDisabled === 'both' || isDisabled === hand) { return; } inTeleportMode = true; - this.inCoolIn = true; + if (coolInTimeout !== null) { Script.clearTimeout(coolInTimeout); - } + + this.state = TELEPORTER_STATES.COOL_IN; coolInTimeout = Script.setTimeout(function() { - _this.inCoolIn = false; + if (_this.state === TELEPORTER_STATES.COOL_IN) { + _this.state = TELEPORTER_STATES.TARGETTING; + } }, COOL_IN_DURATION) - if (this.smoothArrivalInterval !== null) { - Script.clearInterval(this.smoothArrivalInterval); - } - if (activationTimeout !== null) { - Script.clearInterval(activationTimeout); - } - - this.teleportHand = hand; - this.initialize(); - Script.update.connect(this.update); + this.activeHand = hand; + this.enableMappings(); + Script.update.connect(this, this.update); this.updateConnected = true; - - - }; - this.createTargetOverlay = function(visible) { - if (visible == undefined) { - visible = true; - } - - if (_this.targetOverlay !== null) { - return; - } - var targetOverlayProps = { - url: TARGET_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - visible: visible - }; - - _this.targetOverlay = Overlays.addOverlay("model", targetOverlayProps); - - }; - - this.createCancelOverlay = function(visible) { - if (visible == undefined) { - visible = true; - } - - if (_this.cancelOverlay !== null) { - return; - } - - var cancelOverlayProps = { - url: TOO_CLOSE_MODEL_URL, - dimensions: TARGET_MODEL_DIMENSIONS, - visible: visible - }; - - _this.cancelOverlay = Overlays.addOverlay("model", cancelOverlayProps); - }; - - this.deleteCancelOverlay = function() { - if (this.cancelOverlay === null) { - return; - } - - Overlays.deleteOverlay(this.cancelOverlay); - this.cancelOverlay = null; - } - - this.hideCancelOverlay = function() { - if (this.cancelOverlay === null) { - return; - } - - this.intersection = null; - Overlays.editOverlay(this.cancelOverlay, { visible: false }); - } - - this.showCancelOverlay = function() { - if (this.cancelOverlay === null) { - return this.createCancelOverlay(); - } - Overlays.editOverlay(this.cancelOverlay, { visible: true }); - } - - - this.deleteTargetOverlay = function() { - if (this.targetOverlay === null) { - return; - } - - Overlays.deleteOverlay(this.targetOverlay); - this.intersection = null; - this.targetOverlay = null; - } - - this.hideTargetOverlay = function() { - if (this.targetOverlay === null) { - return; - } - - this.intersection = null; - Overlays.editOverlay(this.targetOverlay, { visible: false }); - } - - this.showTargetOverlay = function() { - if (this.targetOverlay === null) { - return this.createTargetOverlay(); - } - Overlays.editOverlay(this.targetOverlay, { visible: true }); - } - - this.turnOffOverlayBeams = function() { - this.rightOverlayOff(); - this.leftOverlayOff(); - } - this.exitTeleportMode = function(value) { - if (activationTimeout !== null) { - Script.clearTimeout(activationTimeout); - activationTimeout = null; - } if (this.updateConnected === true) { - Script.update.disconnect(this.update); + Script.update.disconnect(this, this.update); } this.disableMappings(); - this.turnOffOverlayBeams(); + this.deleteOverlayBeams(); + this.hideTargetOverlay(); + this.hideCancelOverlay(); this.updateConnected = null; - this.inCoolIn = false; + this.state = TELEPORTER_STATES.IDLE; inTeleportMode = false; }; - this.update = function() { - if (isDisabled === 'both') { - return; - } - - if (teleporter.teleportHand === 'left') { - if (isDisabled === 'left') { - return; + this.deleteOverlayBeams = function() { + for (key in this.overlayLines) { + if (this.overlayLines[key] !== null) { + Overlays.deleteOverlay(this.overlayLines[key]); + this.overlayLines[key] = null; } - teleporter.leftRay(); - if ((leftPad.buttonValue === 0) && inTeleportMode === true) { - if (_this.inCoolIn === true) { - _this.exitTeleportMode(); - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - } else { - _this.teleport(); - } - return; - } - - } else { - if (isDisabled === 'right') { - return; - } - teleporter.rightRay(); - if ((rightPad.buttonValue === 0) && inTeleportMode === true) { - if (_this.inCoolIn === true) { - _this.exitTeleportMode(); - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - } else { - _this.teleport(); - } - return; - } - } - - }; - - this.rightRay = function() { - var pose = Controller.getPoseValue(Controller.Standard.RightHand); - var rightPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); - var rightRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : - Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { - x: 1, - y: 0, - z: 0 - })); - - var rightPickRay = { - origin: rightPosition, - direction: Quat.getUp(rightRotation), - }; - - this.rightPickRay = rightPickRay; - - var location = Vec3.sum(rightPickRay.origin, Vec3.multiply(rightPickRay.direction, 50)); - - - var rightIntersection = Entities.findRayIntersection(teleporter.rightPickRay, true, [], [this.targetEntity], true, true); - - if (rightIntersection.intersects) { - if (this.tooClose === true) { - this.hideTargetOverlay(); - - this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(rightIntersection); - } else { - this.createCancelOverlay(); - } - } else { - if (this.inCoolIn === true) { - this.hideTargetOverlay(); - this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(rightIntersection); - } else { - this.createCancelOverlay(); - } - } else { - this.hideCancelOverlay(); - - this.rightLineOn(rightPickRay.origin, rightIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); - if (this.targetOverlay !== null) { - this.updateTargetOverlay(rightIntersection); - } else { - this.createTargetOverlay(); - } - } - - - } - - } else { - - this.hideTargetOverlay(); - this.rightLineOn(rightPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT); } } + this.update = function() { + if (_this.state === TELEPORTER_STATES.IDLE) { + return; + } - this.leftRay = function() { - var pose = Controller.getPoseValue(Controller.Standard.LeftHand); - var leftPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); - var leftRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : + // Get current hand pose information so that we can get the direction of the teleport beam + var pose = Controller.getPoseValue(handInfo[_this.activeHand].controllerInput); + var handPosition = pose.valid ? Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, pose.translation), MyAvatar.position) : MyAvatar.getHeadPosition(); + var handRotation = pose.valid ? Quat.multiply(MyAvatar.orientation, pose.rotation) : Quat.multiply(MyAvatar.headOrientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 })); - var leftPickRay = { - origin: leftPosition, - direction: Quat.getUp(leftRotation), + var pickRay = { + origin: handPosition, + direction: Quat.getUp(handRotation), }; - this.leftPickRay = leftPickRay; + // We do up to 2 ray picks to find a teleport location. + // There are 2 types of teleport locations we are interested in: + // 1. A visible floor. This can be any entity surface that points within some degree of "up" + // 2. A seat. The seat can be visible or invisible. + // + // * In the first pass we pick against visible and invisible entities so that we can find invisible seats. + // We might hit an invisible entity that is not a seat, so we need to do a second pass. + // * In the second pass we pick against visible entities only. + // + var intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], false, true); - var location = Vec3.sum(MyAvatar.position, Vec3.multiply(leftPickRay.direction, 50)); - - - var leftIntersection = Entities.findRayIntersection(teleporter.leftPickRay, true, [], [this.targetEntity], true, true); - - if (leftIntersection.intersects) { - - if (this.tooClose === true) { - this.hideTargetOverlay(); - this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(leftIntersection); - } else { - this.createCancelOverlay(); - } - } else { - if (this.inCoolIn === true) { - this.hideTargetOverlay(); - this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_TOO_CLOSE); - if (this.cancelOverlay !== null) { - this.updateCancelOverlay(leftIntersection); - } else { - this.createCancelOverlay(); - } - } else { - this.hideCancelOverlay(); - this.leftLineOn(leftPickRay.origin, leftIntersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); - - if (this.targetOverlay !== null) { - this.updateTargetOverlay(leftIntersection); - } else { - this.createTargetOverlay(); - } - } - - - } - - - } else { + var teleportLocationType = getTeleportTargetType(intersection); + if (teleportLocationType === TARGET.INVISIBLE) { + intersection = Entities.findRayIntersection(pickRay, true, [], [this.targetEntity], true, true); + teleportLocationType = getTeleportTargetType(intersection); + } + if (teleportLocationType === TARGET.NONE) { this.hideTargetOverlay(); - this.leftLineOn(leftPickRay.origin, location, COLORS_TELEPORT_CANNOT_TELEPORT); + this.hideCancelOverlay(); + this.hideSeatOverlay(); + + var farPosition = Vec3.sum(pickRay.origin, Vec3.multiply(pickRay.direction, 50)); + this.updateLineOverlay(_this.activeHand, pickRay.origin, farPosition, COLORS_TELEPORT_CANNOT_TELEPORT); + } else if (teleportLocationType === TARGET.INVALID || teleportLocationType === TARGET.INVISIBLE) { + this.hideTargetOverlay(); + this.hideSeatOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL); + this.updateDestinationOverlay(this.cancelOverlay, intersection); + } else if (teleportLocationType === TARGET.SURFACE) { + if (this.state === TELEPORTER_STATES.COOL_IN) { + this.hideTargetOverlay(); + this.hideSeatOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CANCEL); + this.updateDestinationOverlay(this.cancelOverlay, intersection); + } else { + this.hideCancelOverlay(); + this.hideSeatOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_CAN_TELEPORT); + this.updateDestinationOverlay(this.targetOverlay, intersection); + } + } else if (teleportLocationType === TARGET.SEAT) { + this.hideCancelOverlay(); + this.hideTargetOverlay(); + + this.updateLineOverlay(_this.activeHand, pickRay.origin, intersection.intersection, COLORS_TELEPORT_SEAT); + this.updateDestinationOverlay(this.seatOverlay, intersection); + } + + + if (((_this.activeHand == 'left' ? leftPad : rightPad).buttonValue === 0) && inTeleportMode === true) { + this.exitTeleportMode(); + this.hideCancelOverlay(); + this.hideTargetOverlay(); + this.hideSeatOverlay(); + + if (teleportLocationType === TARGET.NONE || teleportLocationType === TARGET.INVALID || this.state === TELEPORTER_STATES.COOL_IN) { + // Do nothing + } else if (teleportLocationType === TARGET.SEAT) { + Entities.callEntityMethod(intersection.entityID, 'sit'); + } else if (teleportLocationType === TARGET.SURFACE) { + var offset = getAvatarFootOffset(); + intersection.intersection.y += offset; + MyAvatar.position = intersection.intersection; + HMD.centerUI(); + } } }; - this.rightLineOn = function(closePoint, farPoint, color) { - if (this.rightOverlayLine === null) { + this.updateLineOverlay = function(hand, closePoint, farPoint, color) { + if (this.overlayLines[hand] === null) { var lineProperties = { start: closePoint, end: farPoint, @@ -431,10 +316,10 @@ function Teleporter() { glow: 1.0 }; - this.rightOverlayLine = Overlays.addOverlay("line3d", lineProperties); + this.overlayLines[hand] = Overlays.addOverlay("line3d", lineProperties); } else { - var success = Overlays.editOverlay(this.rightOverlayLine, { + var success = Overlays.editOverlay(this.overlayLines[hand], { start: closePoint, end: farPoint, color: color @@ -442,47 +327,19 @@ function Teleporter() { } }; - this.leftLineOn = function(closePoint, farPoint, color) { - if (this.leftOverlayLine === null) { - var lineProperties = { - ignoreRayIntersection: true, - start: closePoint, - end: farPoint, - color: color, - visible: true, - alpha: 1, - solid: true, - glow: 1.0, - drawInFront: true - }; - - this.leftOverlayLine = Overlays.addOverlay("line3d", lineProperties); - - } else { - var success = Overlays.editOverlay(this.leftOverlayLine, { - start: closePoint, - end: farPoint, - color: color - }); - } - }; - this.rightOverlayOff = function() { - if (this.rightOverlayLine !== null) { - Overlays.deleteOverlay(this.rightOverlayLine); - this.rightOverlayLine = null; - } + this.hideCancelOverlay = function() { + Overlays.editOverlay(this.cancelOverlay, { visible: false }); }; - this.leftOverlayOff = function() { - if (this.leftOverlayLine !== null) { - Overlays.deleteOverlay(this.leftOverlayLine); - this.leftOverlayLine = null; - } + this.hideTargetOverlay = function() { + Overlays.editOverlay(this.targetOverlay, { visible: false }); }; - this.updateTargetOverlay = function(intersection) { - _this.intersection = intersection; + this.hideSeatOverlay = function() { + Overlays.editOverlay(this.seatOverlay, { visible: false }); + }; + this.updateDestinationOverlay = function(overlayID, intersection) { var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP); var euler = Quat.safeEulerAngles(rotation); var position = { @@ -491,115 +348,15 @@ function Teleporter() { z: intersection.intersection.z }; - this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal); var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); - Overlays.editOverlay(this.targetOverlay, { + Overlays.editOverlay(overlayID, { visible: true, position: position, rotation: towardUs }); }; - - this.updateCancelOverlay = function(intersection) { - _this.intersection = intersection; - - var rotation = Quat.lookAt(intersection.intersection, MyAvatar.position, Vec3.UP); - var euler = Quat.safeEulerAngles(rotation); - var position = { - x: intersection.intersection.x, - y: intersection.intersection.y + TARGET_MODEL_DIMENSIONS.y / 2, - z: intersection.intersection.z - }; - - this.tooClose = isValidTeleportLocation(position, intersection.surfaceNormal); - var towardUs = Quat.fromPitchYawRollDegrees(0, euler.y, 0); - - Overlays.editOverlay(this.cancelOverlay, { - visible: true, - position: position, - rotation: towardUs - }); - }; - - this.triggerHaptics = function() { - var hand = this.teleportHand === 'left' ? 0 : 1; - var haptic = Controller.triggerShortHapticPulse(0.2, hand); - }; - - this.teleport = function(value) { - - if (value === undefined) { - this.exitTeleportMode(); - } - - if (this.intersection !== null) { - if (this.tooClose === true) { - this.exitTeleportMode(); - this.hideCancelOverlay(); - return; - } - var offset = getAvatarFootOffset(); - this.intersection.intersection.y += offset; - this.exitTeleportMode(); - // Disable smooth arrival, possibly temporarily - //this.smoothArrival(); - MyAvatar.position = _this.intersection.intersection; - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - HMD.centerUI(); - } - }; - - this.findMidpoint = function(start, end) { - var xy = Vec3.sum(start, end); - var midpoint = Vec3.multiply(0.5, xy); - return midpoint - }; - - this.getArrivalPoints = function(startPoint, endPoint) { - var arrivalPoints = []; - var i; - var lastPoint; - - for (i = 0; i < NUMBER_OF_STEPS; i++) { - if (i === 0) { - lastPoint = startPoint; - } - var newPoint = _this.findMidpoint(lastPoint, endPoint); - lastPoint = newPoint; - arrivalPoints.push(newPoint); - } - - arrivalPoints.push(endPoint); - - return arrivalPoints; - }; - - this.smoothArrival = function() { - - _this.arrivalPoints = _this.getArrivalPoints(MyAvatar.position, _this.intersection.intersection); - _this.smoothArrivalInterval = Script.setInterval(function() { - if (_this.arrivalPoints.length === 0) { - Script.clearInterval(_this.smoothArrivalInterval); - HMD.centerUI(); - return; - } - var landingPoint = _this.arrivalPoints.shift(); - MyAvatar.position = landingPoint; - - if (_this.arrivalPoints.length === 1 || _this.arrivalPoints.length === 0) { - _this.hideTargetOverlay(); - _this.hideCancelOverlay(); - } - - }, SMOOTH_ARRIVAL_SPACING); - } - - this.createTargetOverlay(false); - this.createCancelOverlay(false); - } //related to repositioning the avatar after you teleport @@ -611,20 +368,16 @@ function getAvatarFootOffset() { var jointName = d.joint; if (jointName === "RightUpLeg") { upperLeg = d.translation.y; - } - if (jointName === "RightLeg") { + } else if (jointName === "RightLeg") { lowerLeg = d.translation.y; - } - if (jointName === "RightFoot") { + } else if (jointName === "RightFoot") { foot = d.translation.y; - } - if (jointName === "RightToeBase") { + } else if (jointName === "RightToeBase") { toe = d.translation.y; - } - if (jointName === "RightToe_End") { + } else if (jointName === "RightToe_End") { toeTop = d.translation.y; } - }) + }); var offset = upperLeg + lowerLeg + foot + toe + toeTop; offset = offset / 100; @@ -655,7 +408,6 @@ var rightTrigger = new Trigger('right'); var mappingName, teleportMapping; -var activationTimeout = null; var TELEPORT_DELAY = 0; function isMoving() { @@ -668,17 +420,44 @@ function isMoving() { } }; +function parseJSON(json) { + try { + return JSON.parse(json); + } catch (e) { + return undefined; + } +} // When determininig whether you can teleport to a location, the normal of the // point that is being intersected with is looked at. If this normal is more // than MAX_ANGLE_FROM_UP_TO_TELEPORT degrees from <0, 1, 0> (straight up), then // you can't teleport there. -var MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; -function isValidTeleportLocation(position, surfaceNormal) { +const MAX_ANGLE_FROM_UP_TO_TELEPORT = 70; +function getTeleportTargetType(intersection) { + if (!intersection.intersects) { + return TARGET.NONE; + } + + var props = Entities.getEntityProperties(intersection.entityID, ['userData', 'visible']); + var data = parseJSON(props.userData); + if (data !== undefined && data.seat !== undefined) { + return TARGET.SEAT; + } + + if (!props.visible) { + return TARGET.INVISIBLE; + } + + var surfaceNormal = intersection.surfaceNormal; var adj = Math.sqrt(surfaceNormal.x * surfaceNormal.x + surfaceNormal.z * surfaceNormal.z); var angleUp = Math.atan2(surfaceNormal.y, adj) * (180 / Math.PI); - return angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || - angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || - Vec3.distance(MyAvatar.position, position) <= TELEPORT_CANCEL_RANGE; + + if (angleUp < (90 - MAX_ANGLE_FROM_UP_TO_TELEPORT) || + angleUp > (90 + MAX_ANGLE_FROM_UP_TO_TELEPORT) || + Vec3.distance(MyAvatar.position, intersection.intersection) <= TELEPORT_CANCEL_RANGE) { + return TARGET.INVALID; + } else { + return TARGET.SURFACE; + } }; function registerMappings() { @@ -695,20 +474,13 @@ function registerMappings() { if (isDisabled === 'left' || isDisabled === 'both') { return; } - if (activationTimeout !== null) { - return - } if (leftTrigger.down()) { return; } if (isMoving() === true) { return; } - activationTimeout = Script.setTimeout(function() { - Script.clearTimeout(activationTimeout); - activationTimeout = null; - teleporter.enterTeleportMode('left') - }, TELEPORT_DELAY) + teleporter.enterTeleportMode('left') return; }); teleportMapping.from(Controller.Standard.RightPrimaryThumb) @@ -716,9 +488,6 @@ function registerMappings() { if (isDisabled === 'right' || isDisabled === 'both') { return; } - if (activationTimeout !== null) { - return - } if (rightTrigger.down()) { return; } @@ -726,11 +495,7 @@ function registerMappings() { return; } - activationTimeout = Script.setTimeout(function() { - teleporter.enterTeleportMode('right') - Script.clearTimeout(activationTimeout); - activationTimeout = null; - }, TELEPORT_DELAY) + teleporter.enterTeleportMode('right') return; }); }; @@ -741,18 +506,11 @@ var teleporter = new Teleporter(); Controller.enableMapping(mappingName); -Script.scriptEnding.connect(cleanup); - function cleanup() { teleportMapping.disable(); - teleporter.disableMappings(); - teleporter.deleteTargetOverlay(); - teleporter.deleteCancelOverlay(); - teleporter.turnOffOverlayBeams(); - if (teleporter.updateConnected !== null) { - Script.update.disconnect(teleporter.update); - } + teleporter.cleanup(); } +Script.scriptEnding.connect(cleanup); var isDisabled = false; var handleHandMessages = function(channel, message, sender) { diff --git a/scripts/tutorials/entity_scripts/sit.js b/scripts/tutorials/entity_scripts/sit.js new file mode 100644 index 0000000000..2ba19231e0 --- /dev/null +++ b/scripts/tutorials/entity_scripts/sit.js @@ -0,0 +1,257 @@ +(function() { + Script.include("/~/system/libraries/utils.js"); + + var SETTING_KEY = "com.highfidelity.avatar.isSitting"; + var ROLE = "fly"; + var ANIMATION_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/clement/production/animations/sitting_idle.fbx"; + var ANIMATION_FPS = 30; + var ANIMATION_FIRST_FRAME = 1; + var ANIMATION_LAST_FRAME = 10; + var RELEASE_KEYS = ['w', 'a', 's', 'd', 'UP', 'LEFT', 'DOWN', 'RIGHT']; + var RELEASE_TIME = 500; // ms + var RELEASE_DISTANCE = 0.2; // meters + var MAX_IK_ERROR = 20; + var DESKTOP_UI_CHECK_INTERVAL = 250; + var DESKTOP_MAX_DISTANCE = 5; + var SIT_DELAY = 25 + + this.entityID = null; + this.timers = {}; + this.animStateHandlerID = null; + + this.preload = function(entityID) { + this.entityID = entityID; + } + this.unload = function() { + if (MyAvatar.sessionUUID === this.getSeatUser()) { + this.sitUp(this.entityID); + } + if (this.interval) { + Script.clearInterval(this.interval); + this.interval = null; + } + this.cleanupOverlay(); + } + + this.setSeatUser = function(user) { + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + userData = JSON.parse(userData); + + if (user) { + userData.seat.user = user; + } else { + delete userData.seat.user; + } + + Entities.editEntity(this.entityID, { + userData: JSON.stringify(userData) + }); + } + this.getSeatUser = function() { + var properties = Entities.getEntityProperties(this.entityID, ["userData", "position"]); + var userData = JSON.parse(properties.userData); + + if (userData.seat.user && userData.seat.user !== MyAvatar.sessionUUID) { + var avatar = AvatarList.getAvatar(userData.seat.user); + if (avatar && Vec3.distance(avatar.position, properties.position) > RELEASE_DISTANCE) { + return null; + } + } + return userData.seat.user; + } + + this.checkSeatForAvatar = function() { + var seatUser = this.getSeatUser(); + var avatarIdentifiers = AvatarList.getAvatarIdentifiers(); + for (var i in avatarIdentifiers) { + var avatar = AvatarList.getAvatar(avatarIdentifiers[i]); + if (avatar && avatar.sessionUUID === seatUser) { + return true; + } + } + return false; + } + + this.sitDown = function() { + if (this.checkSeatForAvatar()) { + print("Someone is already sitting in that chair."); + return; + } + + this.setSeatUser(MyAvatar.sessionUUID); + + var previousValue = Settings.getValue(SETTING_KEY); + Settings.setValue(SETTING_KEY, this.entityID); + if (previousValue === "") { + MyAvatar.characterControllerEnabled = false; + MyAvatar.hmdLeanRecenterEnabled = false; + MyAvatar.overrideRoleAnimation(ROLE, ANIMATION_URL, ANIMATION_FPS, true, ANIMATION_FIRST_FRAME, ANIMATION_LAST_FRAME); + MyAvatar.resetSensorsAndBody(); + } + + var that = this; + Script.setTimeout(function() { + var properties = Entities.getEntityProperties(that.entityID, ["position", "rotation"]); + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.pinJoint(index, properties.position, properties.rotation); + + that.animStateHandlerID = MyAvatar.addAnimationStateHandler(function(properties) { + return { headType: 0 }; + }, ["headType"]); + Script.update.connect(that, that.update); + Controller.keyPressEvent.connect(that, that.keyPressed); + Controller.keyReleaseEvent.connect(that, that.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.captureKeyEvents({ text: RELEASE_KEYS[i] }); + } + }, SIT_DELAY); + } + + this.sitUp = function() { + this.setSeatUser(null); + + if (Settings.getValue(SETTING_KEY) === this.entityID) { + MyAvatar.restoreRoleAnimation(ROLE); + MyAvatar.characterControllerEnabled = true; + MyAvatar.hmdLeanRecenterEnabled = true; + + var index = MyAvatar.getJointIndex("Hips"); + MyAvatar.clearPinOnJoint(index); + + MyAvatar.resetSensorsAndBody(); + + Script.setTimeout(function() { + MyAvatar.bodyPitch = 0.0; + MyAvatar.bodyRoll = 0.0; + }, SIT_DELAY); + + Settings.setValue(SETTING_KEY, ""); + } + + MyAvatar.removeAnimationStateHandler(this.animStateHandlerID); + Script.update.disconnect(this, this.update); + Controller.keyPressEvent.disconnect(this, this.keyPressed); + Controller.keyReleaseEvent.disconnect(this, this.keyReleased); + for (var i in RELEASE_KEYS) { + Controller.releaseKeyEvents({ text: RELEASE_KEYS[i] }); + } + } + + this.sit = function () { + this.sitDown(); + } + + this.createOverlay = function() { + var text = "Click to sit"; + var textMargin = 0.05; + var lineHeight = 0.15; + + this.overlay = Overlays.addOverlay("text3d", { + position: { x: 0.0, y: 0.0, z: 0.0}, + dimensions: { x: 0.1, y: 0.1 }, + backgroundColor: { red: 0, green: 0, blue: 0 }, + color: { red: 255, green: 255, blue: 255 }, + topMargin: textMargin, + leftMargin: textMargin, + bottomMargin: textMargin, + rightMargin: textMargin, + text: text, + lineHeight: lineHeight, + alpha: 0.9, + backgroundAlpha: 0.9, + ignoreRayIntersection: true, + visible: true, + isFacingAvatar: true + }); + var textSize = Overlays.textSize(this.overlay, text); + var overlayDimensions = { + x: textSize.width + 2 * textMargin, + y: textSize.height + 2 * textMargin + } + var properties = Entities.getEntityProperties(this.entityID, ["position", "registrationPoint", "dimensions"]); + var yOffset = (1.0 - properties.registrationPoint.y) * properties.dimensions.y + (overlayDimensions.y / 2.0); + var overlayPosition = Vec3.sum(properties.position, { x: 0, y: yOffset, z: 0 }); + Overlays.editOverlay(this.overlay, { + position: overlayPosition, + dimensions: overlayDimensions + }); + } + this.cleanupOverlay = function() { + if (this.overlay !== null) { + Overlays.deleteOverlay(this.overlay); + this.overlay = null; + } + } + + + this.update = function(dt) { + if (MyAvatar.sessionUUID === this.getSeatUser()) { + var properties = Entities.getEntityProperties(this.entityID, ["position"]); + var avatarDistance = Vec3.distance(MyAvatar.position, properties.position); + var ikError = MyAvatar.getIKErrorOnLastSolve(); + if (avatarDistance > RELEASE_DISTANCE || ikError > MAX_IK_ERROR) { + print("IK error: " + ikError + ", distance from chair: " + avatarDistance); + this.sitUp(this.entityID); + } + } + } + this.keyPressed = function(event) { + if (isInEditMode()) { + return; + } + + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + var that = this; + this.timers[event.text] = Script.setTimeout(function() { + that.sitUp(); + }, RELEASE_TIME); + } + } + this.keyReleased = function(event) { + if (RELEASE_KEYS.indexOf(event.text) !== -1) { + if (this.timers[event.text]) { + Script.clearTimeout(this.timers[event.text]); + delete this.timers[event.text]; + } + } + } + + this.canSitDesktop = function() { + var properties = Entities.getEntityProperties(this.entityID, ["position"]); + var distanceFromSeat = Vec3.distance(MyAvatar.position, properties.position); + return distanceFromSeat < DESKTOP_MAX_DISTANCE && !this.checkSeatForAvatar(); + } + + this.hoverEnterEntity = function(event) { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + return; + } + + var that = this; + this.interval = Script.setInterval(function() { + if (that.overlay === null) { + if (that.canSitDesktop()) { + that.createOverlay(); + } + } else if (!that.canSitDesktop()) { + that.cleanupOverlay(); + } + }, DESKTOP_UI_CHECK_INTERVAL); + } + this.hoverLeaveEntity = function(event) { + if (this.interval) { + Script.clearInterval(this.interval); + this.interval = null; + } + this.cleanupOverlay(); + } + + this.clickDownOnEntity = function () { + if (isInEditMode() || (MyAvatar.sessionUUID === this.getSeatUser())) { + return; + } + if (this.canSitDesktop()) { + this.sitDown(); + } + } +});