diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index d982a032cc..cf3c9af83a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -266,6 +266,27 @@ CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING] = { }; CONTROLLER_STATE_MACHINE[STATE_OVERLAY_LASER_TOUCHING] = CONTROLLER_STATE_MACHINE[STATE_OVERLAY_STYLUS_TOUCHING]; +// Object assign polyfill +if (typeof Object.assign != 'function') { + Object.assign = function(target, varArgs) { + 'use strict'; + if (target == null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + var to = Object(target); + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + if (nextSource != null) { + for (var nextKey in nextSource) { + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} function distanceBetweenPointAndEntityBoundingBox(point, entityProps) { var entityXform = new Xform(entityProps.rotation, entityProps.position); @@ -1455,7 +1476,18 @@ function MyController(hand) { return true; }; + this.entityIsCloneable = function(entityID) { + var entityProps = entityPropertiesCache.getGrabbableProps(entityID); + var props = entityPropertiesCache.getProps(entityID); + if (!props) { + return false; + } + if (entityProps.hasOwnProperty("cloneable")) { + return entityProps.cloneable; + } + return false; + } this.entityIsGrabbable = function(entityID) { var grabbableProps = entityPropertiesCache.getGrabbableProps(entityID); var props = entityPropertiesCache.getProps(entityID); @@ -1535,7 +1567,7 @@ function MyController(hand) { this.entityIsNearGrabbable = function(entityID, handPosition, maxDistance) { - if (!this.entityIsGrabbable(entityID)) { + if (!this.entityIsCloneable(entityID) && !this.entityIsGrabbable(entityID)) { return false; } @@ -2385,6 +2417,9 @@ function MyController(hand) { this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); } + // This boolean is used to check if the object that is grabbed has just been cloned + // It is only set true, if the object that is grabbed creates a new clone. + var isClone = false; var isPhysical = propsArePhysical(grabbedProperties) || (!this.grabbedIsOverlay && entityHasActions(this.grabbedThingID)); if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) { @@ -2423,6 +2458,54 @@ function MyController(hand) { if (this.grabbedIsOverlay) { Overlays.editOverlay(this.grabbedThingID, reparentProps); } else { + if (grabbedProperties.userData.length > 0) { + try{ + var userData = JSON.parse(grabbedProperties.userData); + var grabInfo = userData.grabbableKey; + if (grabInfo && grabInfo.cloneable) { + // Check if + var worldEntities = Entities.findEntitiesInBox(Vec3.subtract(MyAvatar.position, {x:25,y:25, z:25}), {x:50, y: 50, z: 50}) + var count = 0; + worldEntities.forEach(function(item) { + var item = Entities.getEntityProperties(item, ["name"]); + if (item.name === grabbedProperties.name) { + count++; + } + }) + var cloneableProps = Entities.getEntityProperties(grabbedProperties.id); + var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300; + var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 10; + var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false; + var cUserData = Object.assign({}, userData); + var cProperties = Object.assign({}, cloneableProps); + isClone = true; + + if (count > limit) { + delete cloneableProps; + delete lifetime; + delete cUserData; + delete cProperties; + return; + } + + delete cUserData.grabbableKey.cloneLifetime; + delete cUserData.grabbableKey.cloneable; + delete cUserData.grabbableKey.cloneDynamic; + delete cUserData.grabbableKey.cloneLimit; + delete cProperties.id + + cProperties.dynamic = dynamic; + cProperties.locked = false; + cUserData.grabbableKey.triggerable = true; + cUserData.grabbableKey.grabbable = true; + cProperties.lifetime = lifetime; + cProperties.userData = JSON.stringify(cUserData); + var cloneID = Entities.addEntity(cProperties); + this.grabbedThingID = cloneID; + grabbedProperties = Entities.getEntityProperties(cloneID); + } + }catch(e) {} + } Entities.editEntity(this.grabbedThingID, reparentProps); } @@ -2434,7 +2517,6 @@ function MyController(hand) { this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; } - Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', grabbedEntity: this.grabbedThingID, @@ -2450,22 +2532,37 @@ function MyController(hand) { }); } - if (this.state == STATE_NEAR_GRABBING) { - this.callEntityMethodOnGrabbed("startNearGrab"); - } else { // this.state == STATE_HOLD - this.callEntityMethodOnGrabbed("startEquip"); + var _this = this; + /* + * Setting context for function that is either called via timer or directly, depending if + * if the object in question is a clone. If it is a clone, we need to make sure that the intial equipment event + * is called correctly, as these just freshly created entity may not have completely initialized. + */ + var grabEquipCheck = function () { + if (_this.state == STATE_NEAR_GRABBING) { + _this.callEntityMethodOnGrabbed("startNearGrab"); + } else { // this.state == STATE_HOLD + _this.callEntityMethodOnGrabbed("startEquip"); + } + + _this.currentHandControllerTipPosition = + (_this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; + _this.currentObjectTime = Date.now(); + + _this.currentObjectPosition = grabbedProperties.position; + _this.currentObjectRotation = grabbedProperties.rotation; + _this.currentVelocity = ZERO_VEC; + _this.currentAngularVelocity = ZERO_VEC; + + _this.prevDropDetected = false; } - this.currentHandControllerTipPosition = - (this.hand === RIGHT_HAND) ? MyAvatar.rightHandTipPosition : MyAvatar.leftHandTipPosition; - this.currentObjectTime = Date.now(); - - this.currentObjectPosition = grabbedProperties.position; - this.currentObjectRotation = grabbedProperties.rotation; - this.currentVelocity = ZERO_VEC; - this.currentAngularVelocity = ZERO_VEC; - - this.prevDropDetected = false; + 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); + } else { + grabEquipCheck(); + } }; this.nearGrabbing = function(deltaTime, timestamp) { diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index cc73d71517..5022dbd6a6 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -295,12 +295,29 @@ +
+ + +
+
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index c51293f4bc..3280e1f196 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -27,6 +27,7 @@ var EDITOR_TIMEOUT_DURATION = 1500; const KEY_P = 80; //Key code for letter p used for Parenting hotkey. var colorPickers = []; var lastEntityID = null; + debugPrint = function(message) { EventBridge.emitWebEvent( JSON.stringify({ @@ -323,13 +324,9 @@ function setUserDataFromEditor(noUpdate) { }) ); } - } - - } - -function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, defaultValue) { +function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults) { var properties = {}; var parsedData = {}; try { @@ -339,17 +336,31 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d } else { parsedData = JSON.parse(userDataElement.value); } - } catch (e) {} if (!(groupName in parsedData)) { parsedData[groupName] = {} } - delete parsedData[groupName][keyName]; - if (checkBoxElement.checked !== defaultValue) { - parsedData[groupName][keyName] = checkBoxElement.checked; - } - + var keys = Object.keys(updateKeyPair); + keys.forEach(function (key) { + delete parsedData[groupName][key]; + if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { + if (updateKeyPair[key] instanceof Element) { + if(updateKeyPair[key].type === "checkbox") { + if (updateKeyPair[key].checked !== defaults[key]) { + parsedData[groupName][key] = updateKeyPair[key].checked; + } + } else { + var val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); + if (val !== defaults[key]) { + parsedData[groupName][key] = val; + } + } + } else { + parsedData[groupName][key] = updateKeyPair[key]; + } + } + }); if (Object.keys(parsedData[groupName]).length == 0) { delete parsedData[groupName]; } @@ -368,6 +379,12 @@ function userDataChanger(groupName, keyName, checkBoxElement, userDataElement, d properties: properties, }) ); +} +function userDataChanger(groupName, keyName, values, userDataElement, defaultValue) { + var val = {}, def = {}; + val[keyName] = values; + def[keyName] = defaultValue; + multiDataUpdater(groupName, val, userDataElement, def); }; function setTextareaScrolling(element) { @@ -521,6 +538,7 @@ function unbindAllInputs() { function loaded() { openEventBridge(function() { + var allSections = []; var elID = document.getElementById("property-id"); var elType = document.getElementById("property-type"); @@ -584,6 +602,13 @@ function loaded() { var elCollisionSoundURL = document.getElementById("property-collision-sound-url"); var elGrabbable = document.getElementById("property-grabbable"); + + var elCloneable = document.getElementById("property-cloneable"); + var elCloneableDynamic = document.getElementById("property-cloneable-dynamic"); + var elCloneableGroup = document.getElementById("group-cloneable-group"); + var elCloneableLifetime = document.getElementById("property-cloneable-lifetime"); + var elCloneableLimit = document.getElementById("property-cloneable-limit"); + var elWantsTrigger = document.getElementById("property-wants-trigger"); var elIgnoreIK = document.getElementById("property-ignore-ik"); @@ -847,8 +872,16 @@ function loaded() { elCollideOtherAvatar.checked = properties.collidesWith.indexOf("otherAvatar") > -1; elGrabbable.checked = properties.dynamic; + elWantsTrigger.checked = false; elIgnoreIK.checked = true; + + elCloneable.checked = false; + elCloneableDynamic.checked = false; + elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; + elCloneableLimit.value = 10; + elCloneableLifetime.value = 300; + var parsedUserData = {} try { parsedUserData = JSON.parse(properties.userData); @@ -863,8 +896,25 @@ function loaded() { if ("ignoreIK" in parsedUserData["grabbableKey"]) { elIgnoreIK.checked = parsedUserData["grabbableKey"].ignoreIK; } + if ("cloneable" in parsedUserData["grabbableKey"]) { + elCloneable.checked = parsedUserData["grabbableKey"].cloneable; + elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; + elCloneableLimit.value = elCloneable.checked ? 10: 0; + elCloneableLifetime.value = elCloneable.checked ? 300: 0; + elCloneableDynamic.checked = parsedUserData["grabbableKey"].cloneDynamic ? parsedUserData["grabbableKey"].cloneDynamic : properties.dynamic; + elDynamic.checked = elCloneable.checked ? false: properties.dynamic; + if (elCloneable.checked) { + if ("cloneLifetime" in parsedUserData["grabbableKey"]) { + elCloneableLifetime.value = parsedUserData["grabbableKey"].cloneLifetime ? parsedUserData["grabbableKey"].cloneLifetime : 300; + } + if ("cloneLimit" in parsedUserData["grabbableKey"]) { + elCloneableLimit.value = parsedUserData["grabbableKey"].cloneLimit ? parsedUserData["grabbableKey"].cloneLimit : 10; + } + } + } } - } catch (e) {} + } catch (e) { + } elCollisionSoundURL.value = properties.collisionSoundURL; elLifetime.value = properties.lifetime; @@ -1154,8 +1204,38 @@ function loaded() { }); elGrabbable.addEventListener('change', function() { + if(elCloneable.checked) { + elGrabbable.checked = false; + } userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, properties.dynamic); }); + elCloneableDynamic.addEventListener('change', function (event){ + userDataChanger("grabbableKey", "cloneDynamic", event.target, elUserData, -1); + }); + elCloneable.addEventListener('change', function (event) { + var checked = event.target.checked; + if (checked) { + multiDataUpdater("grabbableKey", + {cloneLifetime: elCloneableLifetime, cloneLimit: elCloneableLimit, cloneDynamic: elCloneableDynamic, cloneable: event.target}, + elUserData, {}); + elCloneableGroup.style.display = "block"; + EventBridge.emitWebEvent( + '{"id":' + lastEntityID + ', "type":"update", "properties":{"dynamic":false, "grabbable": false}}' + ); + } else { + multiDataUpdater("grabbableKey", + {cloneLifetime: null, cloneLimit: null, cloneDynamic: null, cloneable: false}, + elUserData, {}); + elCloneableGroup.style.display = "none"; + } + }); + + var numberListener = function (event) { + userDataChanger("grabbableKey", event.target.getAttribute("data-user-data-type"), parseInt(event.target.value), elUserData, false); + }; + elCloneableLifetime.addEventListener('change', numberListener); + elCloneableLimit.addEventListener('change', numberListener); + elWantsTrigger.addEventListener('change', function() { userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); });