From 9914a1d523c1f1fdc1ba378c918635580eff6a11 Mon Sep 17 00:00:00 2001 From: Alexander Ivash Date: Wed, 9 May 2018 20:20:43 +0300 Subject: [PATCH] AdjustWearables: implement deleting/refreshing wearables/wearable --- interface/resources/qml/hifi/AvatarApp.qml | 61 ++++-- .../qml/hifi/avatarapp/AdjustWearables.qml | 144 ++++++++++--- scripts/system/avatarapp.js | 199 ++++++++++++++++-- 3 files changed, 342 insertions(+), 62 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 01726445e9..5ce0c8eff0 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -21,24 +21,29 @@ Rectangle { ListModel { // the only purpose of this model is to convert JS object to ListElement id: currentAvatarModel + dynamicRoles: true; + function makeAvatarEntry(avatarObject) { + clear(); + append(avatarObject); + return get(count - 1); + } } property var jointNames; - function setCurrentAvatar(currentAvatar) { - currentAvatarModel.clear(); - - root.currentAvatar = { + property var currentAvatar: null; + function setCurrentAvatar(avatar) { + var currentAvatarObject = { 'name' : '', - 'url' : allAvatars.makeThumbnailUrl(currentAvatar.avatarUrl), - 'wearables' : currentAvatar.avatarEntites ? currentAvatar.avatarEntites : [], - 'entry' : currentAvatar, + 'url' : allAvatars.makeThumbnailUrl(avatar.avatarUrl), + 'wearables' : avatar.avatarEntites ? avatar.avatarEntites : [], + 'entry' : avatar, 'getMoreAvatars' : false }; - console.debug('AvatarApp.qml: currentAvatar: ', JSON.stringify(root.currentAvatar, null, '\t')) - currentAvatarModel.append(root.currentAvatar); - root.currentAvatar = currentAvatarModel.get(currentAvatarModel.count - 1); + currentAvatar = currentAvatarModel.makeAvatarEntry(currentAvatarObject); + console.debug('AvatarApp.qml: currentAvatarObject: ', currentAvatarObject, 'currentAvatar: ', currentAvatar, JSON.stringify(currentAvatar.wearables, 0, 4)); + console.debug('currentAvatar.wearables: ', currentAvatar.wearables); } function fromScript(message) { @@ -47,6 +52,16 @@ Rectangle { if(message.method === 'initialize') { jointNames = message.reply.jointNames; emitSendToScript({'method' : getAvatarsMethod}); + } else if(message.method === 'wearableUpdated') { + adjustWearables.refreshWearable(message.entityID, message.wearableIndex, message.properties); + } else if(message.method === 'wearablesUpdated') { + var wearablesModel = currentAvatarModel.get(0).wearables; + wearablesModel.clear(); + message.wearables.forEach(function(wearable) { + wearablesModel.append(wearable); + }); + console.debug('wearablesUpdated: ', JSON.stringify(wearablesModel, 0, 4), '*****', JSON.stringify(message.wearables, 0, 4)); + adjustWearables.refresh(root.currentAvatar); } else if(message.method === 'bookmarkLoaded') { setCurrentAvatar(message.reply.currentAvatar); selectedAvatarId = message.reply.name; @@ -66,7 +81,7 @@ Rectangle { for(var i = 0; i < allAvatars.count; ++i) { var thesame = true; for(var prop in currentAvatar) { - console.debug('prop', prop); + // console.debug('prop', prop); var v1 = currentAvatar[prop]; var v2 = allAvatars.get(i).entry[prop]; @@ -75,14 +90,14 @@ Rectangle { var s1 = JSON.stringify(v1); var s2 = JSON.stringify(v2); - console.debug('comparing\n', s1, 'to\n', s2, '...'); + // console.debug('comparing\n', s1, 'to\n', s2, '...'); if(s1 !== s2) { if(!(Array.isArray(v1) && v1.length === 0 && v2 === undefined)) { thesame = false; break; } } - console.debug('values seems to be the same...'); + // console.debug('values seems to be the same...'); } if(thesame) { selectedAvatarIndex = i; @@ -102,11 +117,11 @@ Rectangle { } else { view.selectAvatar(allAvatars.get(selectedAvatarIndex)); } + } else if(message.method === 'selectAvatarEntity') { + adjustWearables.selectWearableByID(message.entityID); } } - property var currentAvatar; - property string selectedAvatarId: '' onSelectedAvatarIdChanged: { console.debug('selectedAvatarId: ', selectedAvatarId) @@ -188,8 +203,20 @@ Rectangle { anchors.top: header.bottom anchors.bottom: parent.bottom jointNames: root.jointNames - onWearableChanged: { - emitSendToScript({'method' : 'adjustWearable', 'id' : id, 'properties' : properties}) + onWearableUpdated: { + emitSendToScript({'method' : 'adjustWearable', 'entityID' : id, 'wearableIndex' : index, 'properties' : properties}) + } + onWearableDeleted: { + emitSendToScript({'method' : 'deleteWearable', 'entityID' : id, 'avatarName' : avatarName}); + } + onAdjustWearablesOpened: { + emitSendToScript({'method' : 'adjustWearablesOpened'}); + } + onAdjustWearablesClosed: { + emitSendToScript({'method' : 'adjustWearablesClosed'}); + } + onWearableSelected: { + emitSendToScript({'method' : 'selectWearable', 'entityID' : id}); } z: 3 diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 653c3a75b0..3ae0e17ca8 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -11,7 +11,12 @@ Rectangle { height: 706 color: 'white' - signal wearableChanged(var id, var properties); + signal wearableUpdated(var id, int index, var properties); + signal wearableSelected(var id); + signal wearableDeleted(string avatarName, var id); + + signal adjustWearablesOpened(); + signal adjustWearablesClosed(); property bool modified: false; Component.onCompleted: { @@ -25,16 +30,33 @@ Rectangle { property var onButton2Clicked; property var onButton1Clicked; property var jointNames; - property var wearables: ({}) + property string avatarName: '' + property var wearablesModel; + + function backupWearables(avatar) { + for(var i = 0; i < avatar.wearables.count; ++i) { + var wearable = avatar.wearables.get(i).properties; + wearables[wearable.id] = JSON.stringify(wearable) + } + } + function open(avatar) { + adjustWearablesOpened(); console.debug('AdjustWearables.qml: open: ', JSON.stringify(avatar, null, '\t')); visible = true; - wearablesCombobox.model.clear(); + avatarName = avatar.name; + wearablesModel = avatar.wearables; wearables = {}; - console.debug('AdjustWearables.qml: avatar.wearables.count: ', avatar.wearables.count); + refresh(avatar); + backupWearables(avatar); + } + + function refresh(avatar) { + wearablesCombobox.model.clear(); + console.debug('AdjustWearables.qml: open: avatar.wearables.count: ', avatar.wearables.count); for(var i = 0; i < avatar.wearables.count; ++i) { var wearable = avatar.wearables.get(i).properties; console.debug('wearable: ', JSON.stringify(wearable, null, '\t')) @@ -42,12 +64,6 @@ Rectangle { for(var j = (wearable.modelURL.length - 1); j >= 0; --j) { if(wearable.modelURL[j] === '/') { wearable.text = wearable.modelURL.substring(j + 1) + ' [%jointIndex%]'.replace('%jointIndex%', jointNames[wearable.parentJointIndex]); - wearables[wearable.id] = { - position: wearable.localPosition, - rotation: wearable.localRotation, - dimensions: wearable.dimensions - }; - console.debug('wearable.text = ', wearable.text); break; } @@ -58,8 +74,44 @@ Rectangle { wearablesCombobox.currentIndex = 0; } + function refreshWearable(wearableID, wearableIndex, properties) { + var wearable = wearablesCombobox.model.get(wearableIndex); + for(var prop in properties) { + wearable[prop] = properties[prop]; + + // 2do: consider removing 'properties' and manipulating localPosition/localRotation directly + var wearablesModelProps = wearablesModel.get(wearableIndex).properties; + wearablesModelProps[prop] = properties[prop]; + wearablesModel.setProperty(wearableIndex, 'properties', wearablesModelProps); + + console.debug('updated wearable', prop, + 'old = ', JSON.stringify(wearable[prop], 0, 4), + 'new = ', JSON.stringify(properties[prop], 0, 4), + 'model = ', JSON.stringify(wearablesCombobox.model.get(wearableIndex)[prop]), + 'wearablesModel = ', JSON.stringify(wearablesModel.get(wearableIndex).properties[prop], 0, 4) + ); + } + + console.debug('wearablesModel.get(wearableIndex).properties: ', JSON.stringify(wearablesModel.get(wearableIndex).properties, 0, 4)) + } + + function getCurrentWearable() { + return wearablesCombobox.model.get(wearablesCombobox.currentIndex) + } + + function selectWearableByID(entityID) { + for(var i = 0; i < wearablesCombobox.model.count; ++i) { + var wearable = wearablesCombobox.model.get(i); + if(wearable.id === entityID) { + wearablesCombobox.currentIndex = i; + break; + } + } + } + function close() { visible = false; + adjustWearablesClosed(); } HifiConstants { id: hifi } @@ -92,20 +144,30 @@ Rectangle { comboBox.onCurrentIndexChanged: { console.debug('wearable index changed: ', currentIndex); - var currentWearable = wearablesCombobox.model.get(currentIndex) + + var currentWearable = getCurrentWearable(); if(currentWearable) { position.notify = false; position.xvalue = currentWearable.localPosition.x position.yvalue = currentWearable.localPosition.y position.zvalue = currentWearable.localPosition.z + console.debug('currentWearable.localPosition = ', JSON.stringify(currentWearable.localPosition, 0, 4)) position.notify = true; rotation.notify = false; rotation.xvalue = currentWearable.localRotationAngles.x rotation.yvalue = currentWearable.localRotationAngles.y rotation.zvalue = currentWearable.localRotationAngles.z + console.debug('currentWearable.localRotationAngles = ', JSON.stringify(currentWearable.localRotationAngles, 0, 4)) rotation.notify = true; + + scalespinner.notify = false; + scalespinner.realValue = currentWearable.dimensions.x / currentWearable.naturalDimensions.x + console.debug('currentWearable.scale = ', scalespinner.realValue) + scalespinner.notify = true; + + wearableSelected(currentWearable.id); } } } @@ -132,11 +194,11 @@ Rectangle { function notifyPositionChanged() { modified = true; - var properties = {}; - properties.localPosition = { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + var properties = { + localPosition: { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + }; - var currentWearable = wearablesCombobox.model.get(wearablesCombobox.currentIndex) - wearableChanged(currentWearable.id, properties); + wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties); } property bool notify: false; @@ -145,10 +207,10 @@ Rectangle { onYvalueChanged: if(notify) notifyPositionChanged(); onZvalueChanged: if(notify) notifyPositionChanged(); - decimals: 4 - realFrom: -100 - realTo: 100 - realStepSize: 0.0001 + decimals: 2 + realFrom: -10 + realTo: 10 + realStepSize: 0.01 } } @@ -174,11 +236,11 @@ Rectangle { function notifyRotationChanged() { modified = true; - var properties = {}; - properties.localRotationAngles = { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + var properties = { + localRotationAngles: { 'x' : xvalue, 'y' : yvalue, 'z' : zvalue } + }; - var currentWearable = wearablesCombobox.model.get(wearablesCombobox.currentIndex) - wearableChanged(currentWearable.id, properties); + wearableUpdated(getCurrentWearable().id, wearablesCombobox.currentIndex, properties); } property bool notify: false; @@ -187,10 +249,10 @@ Rectangle { onYvalueChanged: if(notify) notifyRotationChanged(); onZvalueChanged: if(notify) notifyRotationChanged(); - decimals: 4 - realFrom: -100 - realTo: 100 - realStepSize: 0.0001 + decimals: 2 + realFrom: -10 + realTo: 10 + realStepSize: 0.01 } } @@ -208,11 +270,33 @@ Rectangle { HifiControlsUit.SpinBox { id: scalespinner - value: 0 + decimals: 2 + realStepSize: 0.1 + realFrom: 0.1 + realTo: 3.0 + realValue: 1.0 backgroundColor: "lightgray" width: position.spinboxWidth colorScheme: hifi.colorSchemes.light - onValueChanged: modified = true; + + property bool notify: false; + onValueChanged: if(notify) notifyScaleChanged(); + + function notifyScaleChanged() { + modified = true; + var currentWearable = getCurrentWearable(); + var naturalDimensions = currentWearable.naturalDimensions; + + var properties = { + dimensions: { + 'x' : realValue * naturalDimensions.x, + 'y' : realValue * naturalDimensions.y, + 'z' : realValue * naturalDimensions.z + } + }; + + wearableUpdated(currentWearable.id, wearablesCombobox.currentIndex, properties); + } } HifiControlsUit.Button { @@ -220,6 +304,8 @@ Rectangle { color: hifi.buttons.red; colorScheme: hifi.colorSchemes.dark; text: "TAKE IT OFF" + onClicked: wearableDeleted(root.avatarName, getCurrentWearable().id); + enabled: wearablesCombobox.model.count !== 0 } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index e05f5b23d9..8875ea2a3b 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -25,23 +25,81 @@ var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; var ENTRY_VERSION = "version"; -function getCurrentAvatar() { - var currentAvatar = {} - currentAvatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; - currentAvatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); - currentAvatar[ENTRY_AVATAR_ATTACHMENTS] = MyAvatar.getAttachmentsVariant(); - currentAvatar[ENTRY_AVATAR_ENTITIES] = MyAvatar.getAvatarEntitiesVariant(); +function getMyAvatarWearables() { + var wearablesArray = MyAvatar.getAvatarEntitiesVariant(); + + console.debug('avatarapp.js: getMyAvatarWearables(): wearables count: ', wearablesArray.length); + for(var i = 0; i < wearablesArray.length; ++i) { + var wearable = wearablesArray[i]; + console.debug('updating localRotationAngles for wearable: ', + wearable.properties.id, + wearable.properties.itemName, + wearable.properties.parentID, + wearable.properties.owningAvatarID, + isGrabbable(wearable.properties.id)); - for(var i = 0; i < currentAvatar[ENTRY_AVATAR_ENTITIES].length; ++i) { - var wearable = currentAvatar[ENTRY_AVATAR_ENTITIES][i]; - console.debug('updating localRotationAngles for wearable: ', JSON.stringify(wearable, null, '\t')); var localRotation = wearable.properties.localRotation; wearable.properties.localRotationAngles = Quat.safeEulerAngles(localRotation) } - return currentAvatar; + return wearablesArray; + + /* + var getAttachedModelEntities = function() { + var resultEntities = []; + Entities.findEntitiesByType('Model', MyAvatar.position, 100).forEach(function(entityID) { + if (isEntityBeingWorn(entityID)) { + resultEntities.push({properties : entityID}); + } + }); + return resultEntities; + }; + + return getAttachedModelEntities(); + */ } +function getMyAvatar() { + var avatar = {} + avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; + avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); + avatar[ENTRY_AVATAR_ATTACHMENTS] = MyAvatar.getAttachmentsVariant(); + avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables(); + console.debug('getMyAvatar: ', JSON.stringify(avatar, null, 4)); + + return avatar; +} + +function updateAvatarWearables(avatar, wearables) { + avatar[ENTRY_AVATAR_ENTITIES] = wearables; +} + +var adjustWearables = { + opened : false, + cameraMode : '', + setOpened : function(value) { + if(this.opened !== value) { + console.debug('avatarapp.js: adjustWearables.setOpened: ', value) + if(value) { + this.cameraMode = Camera.mode; + console.debug('avatarapp.js: adjustWearables.setOpened: storing camera mode: ', this.cameraMode); + + if(!HMD.active) { + Camera.mode = 'mirror'; + } + } else { + Camera.mode = this.cameraMode; + } + + this.opened = value; + } + } +} + +var currentAvatar = getMyAvatar(); +var selectedAvatarEntityGrabbable = false; +var selectedAvatarEntity = null; + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. console.debug('fromQml: message = ', JSON.stringify(message, null, '\t')) @@ -50,7 +108,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See message.reply = { 'bookmarks' : AvatarBookmarks.getBookmarks(), - 'currentAvatar' : getCurrentAvatar() + 'currentAvatar' : currentAvatar }; console.debug('avatarapp.js: currentAvatar: ', JSON.stringify(message.reply.currentAvatar, null, '\t')) @@ -59,30 +117,130 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearable': if(message.properties.localRotationAngles) { message.properties.localRotation = Quat.fromVec3Degrees(message.properties.localRotationAngles) - delete message.properties.localRotationAngles; } - console.debug('Entities.editEntity(message.id, message.properties)'.replace('message.id', message.id).replace('message.properties', JSON.stringify(message.properties))); - Entities.editEntity(message.id, message.properties); + console.debug('Entities.editEntity(message.entityID, message.properties)'.replace('message.entityID', message.entityID).replace('message.properties', JSON.stringify(message.properties))); + Entities.editEntity(message.entityID, message.properties); + sendToQml({'method' : 'wearableUpdated', 'wearable' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties}) break; case 'selectAvatar': console.debug('avatarapp.js: selecting avatar: ', message.name); AvatarBookmarks.loadBookmark(message.name); break; + case 'adjustWearablesOpened': + adjustWearables.setOpened(true); + Entities.mousePressOnEntity.connect(onSelectedEntity); + break; + case 'adjustWearablesClosed': + adjustWearables.setOpened(false); + ensureWearableSelected(null); + Entities.mousePressOnEntity.disconnect(onSelectedEntity); + break; + case 'selectWearable': + ensureWearableSelected(message.entityID); + break; + case 'deleteWearable': + console.debug('before deleting: wearables.length: ', getMyAvatarWearables().length); + console.debug(JSON.stringify(Entities.getEntityProperties(message.entityID, ['parentID']))); + Entities.editEntity(message.entityID, { parentID : null }) // 2do: remove this hack when backend will be fixed + console.debug(JSON.stringify(Entities.getEntityProperties(message.entityID, ['parentID']))); + + Entities.deleteEntity(message.entityID); + var wearables = getMyAvatarWearables(); + console.debug('after deleting: wearables.length: ', wearables.length); + + updateAvatarWearables(currentAvatar, wearables); + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables, 'avatarName' : message.avatarName}) + break; default: print('Unrecognized message from AvatarApp.qml:', JSON.stringify(message)); } } +function isGrabbable(entityID) { + if(entityID === null) { + return false; + } + + var properties = Entities.getEntityProperties(entityID, ['clientOnly', 'userData']); + if (properties.clientOnly) { + var userData; + try { + userData = JSON.parse(properties.userData); + } catch (e) { + userData = {}; + } + + return userData.grabbableKey && userData.grabbableKey.grabbable; + } + + return false; +} + +function setGrabbable(entityID, grabbable) { + console.debug('making entity', entityID, grabbable ? 'grabbable' : 'not grabbable'); + + var properties = Entities.getEntityProperties(entityID, ['clientOnly', 'userData']); + if (properties.clientOnly) { + var userData; + try { + userData = JSON.parse(properties.userData); + } catch (e) { + userData = {}; + } + + if (userData.grabbableKey === undefined) { + userData.grabbableKey = {}; + } + userData.grabbableKey.grabbable = grabbable; + Entities.editEntity(entityID, {userData: JSON.stringify(userData)}); + } +} + +function ensureWearableSelected(entityID) { + if(selectedAvatarEntity !== entityID) { + + if(selectedAvatarEntity !== null) { + setGrabbable(selectedAvatarEntity, selectedAvatarEntityGrabbable); + } + + selectedAvatarEntity = entityID; + selectedAvatarEntityGrabbable = isGrabbable(entityID); + + if(selectedAvatarEntity !== null) { + setGrabbable(selectedAvatarEntity, true); + } + + return true; + } + + return false; +} + +function isEntityBeingWorn(entityID) { + return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID; +}; + +function onSelectedEntity(entityID, pointerEvent) { + if (isEntityBeingWorn(entityID)) { + console.debug('onSelectedEntity: clicked on wearable', entityID); + + if(ensureWearableSelected(entityID)) { + sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntity}); + } + } +} + function sendToQml(message) { + console.debug('avatarapp.js: sendToQml: ', JSON.stringify(message, 0, 4)); tablet.sendToQml(message); } function onBookmarkLoaded(bookmarkName) { - var currentAvatar = getCurrentAvatar(); + currentAvatar = getMyAvatar(); console.debug('avatarapp.js: onBookmarkLoaded: ', JSON.stringify(currentAvatar, 0, 4)); - sendToQml({'method' : 'bookmarkLoaded', 'reply' : {'name' : bookmarkName, 'currentAvatar' : getCurrentAvatar()} }); + sendToQml({'method' : 'bookmarkLoaded', 'reply' : {'name' : bookmarkName, 'currentAvatar' : currentAvatar} }); } // @@ -103,6 +261,7 @@ function startup() { button.clicked.connect(onTabletButtonClicked); tablet.screenChanged.connect(onTabletScreenChanged); AvatarBookmarks.bookmarkLoaded.connect(onBookmarkLoaded); + // Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); // Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); // Users.avatarDisconnected.connect(avatarDisconnected); @@ -121,6 +280,12 @@ function off() { tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); isWired = false; } + + if(adjustWearables.opened) { + adjustWearables.setOpened(false); + ensureWearableSelected(null); + Entities.mousePressOnEntity.disconnect(onSelectedEntity); + } } function tabletVisibilityChanged() { @@ -188,6 +353,8 @@ function onTabletScreenChanged(type, url) { } function shutdown() { + console.debug('shutdown: adjustWearables.opened', adjustWearables.opened); + if (onAvatarAppScreen) { tablet.gotoHomeScreen(); }