875 lines · 36.5 KB · JavaScript
RawDownload
"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 controllerStandard = Controller.Standard;var DEFAULT_SPHERE_MODEL_URL = Script.resolvePath("../../assets/models/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,            "grab": { "grabbable": false },        }, "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.NONE) {                hasParent = false;            }             if (hasParent || entityHasActions(hotspot.entityID)) {                return false;            }             return true;        };         this.handToController = function() {            return (this.hand === RIGHT_HAND) ? controllerStandard.RightHand : controllerStandard.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;

Lines 1–500 of 875.