diff --git a/scripts/system/edit.js b/scripts/system/edit.js index 92cd103c85..69cf278ab3 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -2363,41 +2363,62 @@ var PropertiesTool = function (opts) { } var i, properties, dY, diff, newPosition; if (data.type === "update") { - if (selectionManager.selections.length > 1) { - for (i = 0; i < selectionManager.selections.length; i++) { - Entities.editEntity(selectionManager.selections[i], data.properties); + + if (data.properties || data.propertiesMap) { + var propertiesMap = data.propertiesMap; + if (propertiesMap === undefined) { + propertiesMap = [{ + entityIDs: data.ids, + properties: data.properties, + }]; } - } else if (data.properties) { - if (data.properties.dynamic === false) { - // this object is leaving dynamic, so we zero its velocities - data.properties.localVelocity = Vec3.ZERO; - data.properties.localAngularVelocity = Vec3.ZERO; - } - if (data.properties.rotation !== undefined) { - data.properties.rotation = Quat.fromVec3Degrees(data.properties.rotation); - } - if (data.properties.localRotation !== undefined) { - data.properties.localRotation = Quat.fromVec3Degrees(data.properties.localRotation); - } - if (data.properties.emitOrientation !== undefined) { - data.properties.emitOrientation = Quat.fromVec3Degrees(data.properties.emitOrientation); - } - if (data.properties.keyLight !== undefined && data.properties.keyLight.direction !== undefined) { - var currentKeyLightDirection = Vec3.toPolar(Entities.getEntityProperties(selectionManager.selections[0], ['keyLight.direction']).keyLight.direction); - if (data.properties.keyLight.direction.x === undefined) { - data.properties.keyLight.direction.x = currentKeyLightDirection.x; + + var sendListUpdate = false; + propertiesMap.forEach(function(propertiesObject) { + var properties = propertiesObject.properties; + var updateEntityIDs = propertiesObject.entityIDs; + if (properties.dynamic === false) { + // this object is leaving dynamic, so we zero its velocities + properties.localVelocity = Vec3.ZERO; + properties.localAngularVelocity = Vec3.ZERO; } - if (data.properties.keyLight.direction.y === undefined) { - data.properties.keyLight.direction.y = currentKeyLightDirection.y; + if (properties.rotation !== undefined) { + properties.rotation = Quat.fromVec3Degrees(properties.rotation); } - data.properties.keyLight.direction = Vec3.fromPolar(data.properties.keyLight.direction.x, data.properties.keyLight.direction.y); - } - Entities.editEntity(selectionManager.selections[0], data.properties); - if (data.properties.name !== undefined || data.properties.modelURL !== undefined || data.properties.materialURL !== undefined || - data.properties.visible !== undefined || data.properties.locked !== undefined) { + if (properties.localRotation !== undefined) { + properties.localRotation = Quat.fromVec3Degrees(properties.localRotation); + } + if (properties.emitOrientation !== undefined) { + properties.emitOrientation = Quat.fromVec3Degrees(properties.emitOrientation); + } + if (properties.keyLight !== undefined && properties.keyLight.direction !== undefined) { + var currentKeyLightDirection = Vec3.toPolar(Entities.getEntityProperties(selectionManager.selections[0], ['keyLight.direction']).keyLight.direction); + if (properties.keyLight.direction.x === undefined) { + properties.keyLight.direction.x = currentKeyLightDirection.x; + } + if (properties.keyLight.direction.y === undefined) { + properties.keyLight.direction.y = currentKeyLightDirection.y; + } + properties.keyLight.direction = Vec3.fromPolar(properties.keyLight.direction.x, properties.keyLight.direction.y); + } + + updateEntityIDs.forEach(function (entityID) { + Entities.editEntity(entityID, properties); + }); + + if (properties.name !== undefined || properties.modelURL !== undefined || properties.materialURL !== undefined || + properties.visible !== undefined || properties.locked !== undefined) { + + sendListUpdate = true; + } + + }); + if (sendListUpdate) { entityListTool.sendUpdate(); } } + + if (data.onlyUpdateEntities) { blockPropertyUpdates = true; } else { @@ -2407,9 +2428,9 @@ var PropertiesTool = function (opts) { selectionManager._update(false, this); blockPropertyUpdates = false; } else if (data.type === 'saveUserData' || data.type === 'saveMaterialData') { - //the event bridge and json parsing handle our avatar id string differently. - var actualID = data.id.split('"')[1]; - Entities.editEntity(actualID, data.properties); + data.ids.forEach(function(entityID) { + Entities.editEntity(entityID, data.properties); + }); } else if (data.type === "showMarketplace") { showMarketplace(); } else if (data.type === "action") { diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index d6a281b0c4..470e57ad6d 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -193,8 +193,8 @@ td { } td.hidden { - padding-left: 0px; - padding-right: 0px; + padding-left: 0; + padding-right: 0; } td.url { @@ -262,6 +262,42 @@ input[type="text"] { width: 100%; } +input.multi-diff:not(:focus) + span.multi-diff, +textarea.multi-diff:not(:focus) + span.multi-diff, +.draggable-number.multi-diff>input:not(:focus)+span.multi-diff, +dl>dt.multi-diff:not(:focus) + span.multi-diff { + visibility: visible; + position: absolute; + display: inline-block; + z-index: 2; + top: 7.5px; + left: 20px; + max-width: 50px; + min-width: 10px; + width: 50%; + height: 13px; + background-image: linear-gradient(transparent 0%, transparent 10%, #afafaf 10%, #afafaf 20%, transparent 20%, transparent 45%, #afafaf 45%, #afafaf 55%, transparent 55%, transparent 80%, #afafaf 80%, #afafaf 90%, transparent 90%, transparent 100%); + background-repeat: no-repeat; + pointer-events: none; +} + +input.multi-diff:not(:focus)::-webkit-input-placeholder, input.multi-diff:not(:focus) { + color: transparent; +} + +.draggable-number.multi-diff .text { + color: transparent; +} + +.dropdown > span.multi-diff { + top: 5px; + left: 10px; +} + +.text, .url, .texture, .textarea { + position: relative; +} + input[type="search"] { height: 28px; width: 100%; @@ -333,7 +369,7 @@ input[type=range]::-webkit-slider-thumb { input[type=range]::-webkit-slider-thumb:hover { background-color: white; } -input[type=range]:focus { /*#252525*/ +input[type=range]:focus { outline: none; } @@ -443,6 +479,21 @@ input[type=checkbox]:checked + label { input[type=checkbox]:checked + label:hover { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEySURBVDhPnZLPSsNAEMa/XVPBCE0RhNy0OarP4Av4AD6JB0GwVBA8efBBxHsgh4CQswcRoUIpiIpVAm3zZ5M4szFSbQPBH3xkJvNNZskOer2eLIriKM/ze1JOcS1UHmdZduF5ngEKjr/fN4Z6+oKerwA2gxC4HAFPEWVLsAzgZAvYt3Q6Enw6jg7uBAaTFMNwhpnKdbXCkAJdy8ROu4XrXW2HTJIErHcFDD6nC02Mom8PwymeE2gvS0ZRBBaTlsOXEmdlrfLLOI7Bakrl/zWxCT8T/904f9QW/b06qtrCUdtFCqdjYs2Q2jAPX8c2XQd7Kr/wfV8vwIPs4Ga1ixe5Xrr/YFLTYfKIvWzM6ZtwXZdX7lxXG0L+sxXHcW5t254opRzawQ0S72+dPmjTroIgOP0CQSMt5LDn1T8AAAAASUVORK5CYII=); } +input.multi-diff[type=checkbox] + label { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFbSURBVDhPY2xoaGD68+dPMSMjY9L////VgTQjAw4AlH8PxLOPHj1azWxjY1MBVNsBFBfBpwkEgNKcQGwtJyfHyALkF4IE34gqM9zU9WT4wicG4mIA1l/fGIyOL2EQeP8EZEAiC5AQBUlcMQ5ieMXIwfDo9SeG73/+gRXDAAsTI4Pd9wdgTVAgw/Tv3z8GEP7Jwctw78M3DE0goPr6BoPludVgdTAM1wgCv//9B9PIQOPNDYaAGxtRNIEw03+gYhDGBtSBNgVc3wiWR8dM//4DTQBidKD++jqD//X1YDlsGMWpMKD26jqD79V1GM5DxihOZQWGntqrawy+V9ZiOA0dw21k/f6JwerzHQbvS2swTMeGGfPz8l8BLRP9KizDwP0WHk+EwGum/3//94M8y/nmEdZAwIb//vs7g/nk6ZPHzE3NvwITrxLDfwYhIAamZpz4PcM/hslXrl6pBwAmfz5iaAlAuAAAAABJRU5ErkJggg==); +} +input.multi-diff[type=checkbox] + label:hover { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFPSURBVDhPjZJBSwJBHMXfrG6rWEkl1MFDGOihDn2JIIrqc3QJunbyFhUkRieD+hYepWteuxctXiJ1Q5xmdmZ3bWZTUHezfvAu/3lv3n+HRblcTrbb7fN+v/8eBMFgFpxz13Gcu3q9bqHb7V4M5/9GhatE3cIsy0o99YBKC3jliCWbBK43gK0MoDI9otfTB/vPBC9Uwu4xMC8IzSOSBsFxIYNqMTGcAIYQAlodD3j5/IqENIc5gqt1P/SNZKhaXR0a5E/5BEcrwH1xEHrGZbiuC604DpZ81AoiPJ/WROM4e4sSt3kaaRopNrg7z1FZdSLmcU2saqrX20lTXC5/RFabFmk2m+GLnBnbWJMOThJv4SV/QRqNBjNNM9UiGeQHdDiejZSSG5TSG71zjnVivyVOKlNLlEqlx+xCds7zvU31G6Z938dvEq4QjLMH27ZPvwHFVYQr3h7uHwAAAABJRU5ErkJggg==); +} + +.rgb.fstuple .color-picker.multi-diff:after { + width: 20px; + height: 20px; + content: ' '; + background: darkgray; + display: flex; + clip-path: polygon(0 0, 0 100%, 100% 100%); +} .icon-input input { position: relative; @@ -535,7 +586,6 @@ input[type=checkbox]:checked + label:hover { div.section-header, hr { display: flex; flex-flow: row nowrap; - padding: 10px 16px; font-family: Raleway-Regular; font-size: 12px; @@ -731,8 +781,6 @@ span.indented { .dropdown dl { clear: both; cursor: pointer; -} -.dropdown dl { font-family: FiraSans-SemiBold; font-size: 15px; width: 292px; @@ -741,7 +789,10 @@ span.indented { color: #afafaf; background: #575757; position: relative; + display: flex; + align-items: center; } + .dropdown dl[dropped="true"] { color: #404040; background: linear-gradient(#afafaf, #afafaf); @@ -878,6 +929,8 @@ div.refresh { div.refresh input[type="button"] { float: right; margin-right: -44px; + position: relative; + left: 10px; } .color-picker { @@ -930,6 +983,8 @@ div.refresh input[type="button"] { position: relative; height: 28px; flex: 0 1 124px; + display: flex; + align-items: center; } .draggable-number .text { @@ -1735,3 +1790,7 @@ input[type=number].hide-spinner::-webkit-inner-spin-button { -webkit-appearance: none; visibility: hidden; } + +div.jsoneditor-menu a.jsoneditor-poweredBy { + display: none; +} diff --git a/scripts/system/html/js/draggableNumber.js b/scripts/system/html/js/draggableNumber.js index 0d6af01ebd..3c7b74290c 100644 --- a/scripts/system/html/js/draggableNumber.js +++ b/scripts/system/html/js/draggableNumber.js @@ -13,6 +13,7 @@ function DraggableNumber(min, max, step, decimals, dragStart, dragEnd) { this.min = min; this.max = max; this.step = step !== undefined ? step : 1; + this.multiDiffModeEnabled = false; this.decimals = decimals; this.dragStartFunction = dragStart; this.dragEndFunction = dragEnd; @@ -20,6 +21,7 @@ function DraggableNumber(min, max, step, decimals, dragStart, dragEnd) { this.initialMouseEvent = null; this.lastMouseEvent = null; this.valueChangeFunction = null; + this.multiDiffStepFunction = null; this.initialize(); } @@ -70,22 +72,22 @@ DraggableNumber.prototype = { this.lastMouseEvent = event; } if (this.dragging && this.lastMouseEvent) { - let initialValue = this.elInput.value; - let dx = event.clientX - this.lastMouseEvent.clientX; - let changeValue = dx !== 0; - if (changeValue) { - while (dx !== 0) { - if (dx > 0) { - this.elInput.stepUp(); - --dx; - } else { - this.elInput.stepDown(); - ++dx; + let dragDelta = event.clientX - this.lastMouseEvent.clientX; + if (dragDelta !== 0) { + if (this.multiDiffModeEnabled) { + if (this.multiDiffStepFunction) { + this.multiDiffStepFunction(dragDelta * this.step); + } + } else { + if (dragDelta > 0) { + this.elInput.stepUp(dragDelta); + } else { + this.elInput.stepDown(-dragDelta); + } + this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); } - } - this.inputChange(); - if (this.valueChangeFunction) { - this.valueChangeFunction(); } } this.lastMouseEvent = event; @@ -106,25 +108,46 @@ DraggableNumber.prototype = { stepUp: function() { if (!this.isDisabled()) { - this.elInput.value = parseFloat(this.elInput.value) + this.step; - this.inputChange(); - if (this.valueChangeFunction) { - this.valueChangeFunction(); + if (this.multiDiffModeEnabled) { + if (this.multiDiffStepFunction) { + this.multiDiffStepFunction(this.step, true); + } + } else { + this.elInput.value = parseFloat(this.elInput.value) + this.step; + this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } } } }, stepDown: function() { if (!this.isDisabled()) { - this.elInput.value = parseFloat(this.elInput.value) - this.step; - this.inputChange(); - if (this.valueChangeFunction) { - this.valueChangeFunction(); + if (this.multiDiffModeEnabled) { + if (this.multiDiffStepFunction) { + this.multiDiffStepFunction(-this.step, true); + } + } else { + this.elInput.value = parseFloat(this.elInput.value) - this.step; + this.inputChange(); + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } } } }, - setValue: function(newValue) { + setValue: function(newValue, isMultiDiff) { + if (isMultiDiff !== undefined) { + this.setMultiDiff(isMultiDiff); + } + + if (isNaN(newValue)) { + console.error("DraggableNumber.setValue() > " + newValue + " is not a number."); + return; + } + if (newValue !== "" && this.decimals !== undefined) { this.elInput.value = parseFloat(newValue).toFixed(this.decimals); } else { @@ -133,11 +156,24 @@ DraggableNumber.prototype = { this.elText.firstChild.data = this.elInput.value; }, + setMultiDiff: function(isMultiDiff) { + this.multiDiffModeEnabled = isMultiDiff; + if (isMultiDiff) { + this.elDiv.classList.add('multi-diff'); + } else { + this.elDiv.classList.remove('multi-diff'); + } + }, + setValueChangeFunction: function(valueChangeFunction) { this.valueChangeFunction = valueChangeFunction.bind(this.elInput); this.elInput.addEventListener("change", this.valueChangeFunction); }, - + + setMultiDiffStepFunction: function (multiDiffStepFunction) { + this.multiDiffStepFunction = multiDiffStepFunction; + }, + inputChange: function() { let value = this.elInput.value; if (this.max !== undefined) { @@ -155,6 +191,9 @@ DraggableNumber.prototype = { keyPress: function(event) { if (event.keyCode === ENTER_KEY) { + if (this.valueChangeFunction) { + this.valueChangeFunction(); + } this.inputBlur(); } }, @@ -203,7 +242,10 @@ DraggableNumber.prototype = { this.elRightArrow.className = 'right-arrow'; this.elRightArrow.innerHTML = 'D'; this.elRightArrow.addEventListener("click", this.onStepUp); - + + this.elMultiDiff = document.createElement('span'); + this.elMultiDiff.className = 'multi-diff'; + this.elInput = document.createElement('input'); this.elInput.className = "input"; this.elInput.setAttribute("type", "number"); @@ -220,6 +262,7 @@ DraggableNumber.prototype = { this.elDiv.appendChild(this.elLeftArrow); this.elDiv.appendChild(this.elText); this.elDiv.appendChild(this.elInput); + this.elDiv.appendChild(this.elMultiDiff); this.elDiv.appendChild(this.elRightArrow); } }; diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 021a27152a..4cee3c0bc7 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -8,17 +8,34 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html /* global alert, augmentSpinButtons, clearTimeout, console, document, Element, - EventBridge, JSONEditor, openEventBridge, setTimeout, window, _ $ */ + EventBridge, JSONEditor, openEventBridge, setTimeout, window, _, $ */ const DEGREES_TO_RADIANS = Math.PI / 180.0; const NO_SELECTION = ","; -const PROPERTY_SPACE_MODE = { +const PROPERTY_SPACE_MODE = Object.freeze({ ALL: 0, LOCAL: 1, WORLD: 2 -}; +}); + +const PROPERTY_SELECTION_VISIBILITY = Object.freeze({ + SINGLE_SELECTION: 1, + MULTIPLE_SELECTIONS: 2, + MULTI_DIFF_SELECTIONS: 4, + ANY_SELECTIONS: 7, /* SINGLE_SELECTION | MULTIPLE_SELECTIONS | MULTI_DIFF_SELECTIONS */ +}); + +// Multiple-selection behavior +const PROPERTY_MULTI_DISPLAY_MODE = Object.freeze({ + DEFAULT: 0, + /** + * Comma separated values + * Limited for properties with type "string" or "textarea" and readOnly enabled + */ + COMMA_SEPARATED_VALUES: 1, +}); const GROUPS = [ { @@ -45,6 +62,7 @@ const GROUPS = [ placeholder: "ID", readOnly: true, replaceID: "placeholder-property-id", + multiDisplayMode: PROPERTY_MULTI_DISPLAY_MODE.COMMA_SEPARATED_VALUES, }, { label: "Description", @@ -708,6 +726,7 @@ const GROUPS = [ type: "dynamic-multiselect", propertyUpdate: materialTargetPropertyUpdate, propertyID: "parentMaterialName", + selectionVisibility: PROPERTY_SELECTION_VISIBILITY.SINGLE_SELECTION, }, { label: "Priority", @@ -1340,6 +1359,7 @@ const GROUPS = [ type: "placeholder", indentedLabel: true, propertyID: "serverScriptStatus", + selectionVisibility: PROPERTY_SELECTION_VISIBILITY.SINGLE_SELECTION, }, { label: "Lifetime", @@ -1528,7 +1548,6 @@ const GROUPS_PER_TYPE = { }; const EDITOR_TIMEOUT_DURATION = 1500; -const IMAGE_DEBOUNCE_TIMEOUT = 250; const DEBOUNCE_TIMEOUT = 125; const COLOR_MIN = 0; @@ -1553,7 +1572,7 @@ const ENABLE_DISABLE_SELECTOR = "input, textarea, span, .dropdown dl, .color-pic const PROPERTY_NAME_DIVISION = { GROUP: 0, PROPERTY: 1, - SUBPROPERTY: 2, + SUB_PROPERTY: 2, }; const RECT_ELEMENTS = { @@ -1588,17 +1607,22 @@ let properties = {}; let propertyRangeRequests = []; let colorPickers = {}; let particlePropertyUpdates = {}; -let selectedEntityProperties; -let lastEntityID = null; +let selectedEntityIDs = new Set(); +let currentSelections = []; let createAppTooltip = new CreateAppTooltip(); let currentSpaceMode = PROPERTY_SPACE_MODE.LOCAL; + function createElementFromHTML(htmlString) { let elTemplate = document.createElement('template'); elTemplate.innerHTML = htmlString.trim(); return elTemplate.content.firstChild; } +function isFlagSet(value, flag) { + return (value & flag) === flag; +} + /** * GENERAL PROPERTY/GROUP FUNCTIONS */ @@ -1680,8 +1704,11 @@ function disableProperties() { } function showPropertyElement(propertyID, show) { - let elProperty = properties[propertyID].elContainer; - elProperty.style.display = show ? "" : "none"; + setPropertyVisibility(properties[propertyID], show); +} + +function setPropertyVisibility(property, visible) { + property.elContainer.style.display = visible ? null : "none"; } function resetProperties() { @@ -1692,6 +1719,7 @@ function resetProperties() { switch (propertyData.type) { case 'number': case 'string': { + property.elInput.classList.remove('multi-diff'); if (propertyData.defaultValue !== undefined) { property.elInput.value = propertyData.defaultValue; } else { @@ -1700,46 +1728,49 @@ function resetProperties() { break; } case 'bool': { + property.elInput.classList.remove('multi-diff'); property.elInput.checked = false; break; } case 'number-draggable': { if (propertyData.defaultValue !== undefined) { - property.elNumber.setValue(propertyData.defaultValue); + property.elNumber.setValue(propertyData.defaultValue, false); } else { - property.elNumber.setValue(""); + property.elNumber.setValue("", false); } break; } case 'rect': { - property.elNumberX.setValue(""); - property.elNumberY.setValue(""); - property.elNumberWidth.setValue(""); - property.elNumberHeight.setValue(""); + property.elNumberX.setValue("", false); + property.elNumberY.setValue("", false); + property.elNumberWidth.setValue("", false); + property.elNumberHeight.setValue("", false); break; } case 'vec3': case 'vec2': { - property.elNumberX.setValue(""); - property.elNumberY.setValue(""); + property.elNumberX.setValue("", false); + property.elNumberY.setValue("", false); if (property.elNumberZ !== undefined) { - property.elNumberZ.setValue(""); + property.elNumberZ.setValue("", false); } break; } case 'color': { property.elColorPicker.style.backgroundColor = "rgb(" + 0 + "," + 0 + "," + 0 + ")"; - property.elNumberR.setValue(""); - property.elNumberG.setValue(""); - property.elNumberB.setValue(""); + property.elNumberR.setValue("", false); + property.elNumberG.setValue("", false); + property.elNumberB.setValue("", false); break; } case 'dropdown': { + property.elInput.classList.remove('multi-diff'); property.elInput.value = ""; setDropdownText(property.elInput); break; } case 'textarea': { + property.elInput.classList.remove('multi-diff'); property.elInput.value = ""; setTextareaScrolling(property.elInput); break; @@ -1749,6 +1780,7 @@ function resetProperties() { break; } case 'texture': { + property.elInput.classList.remove('multi-diff'); property.elInput.value = ""; property.elInput.imageLoad(property.elInput.value); break; @@ -1766,7 +1798,11 @@ function resetProperties() { } } } - + + resetServerScriptStatus(); +} + +function resetServerScriptStatus() { let elServerScriptError = document.getElementById("property-serverScripts-error"); let elServerScriptStatus = document.getElementById("property-serverScripts-status"); elServerScriptError.parentElement.style.display = "none"; @@ -1775,43 +1811,194 @@ function resetProperties() { function showGroupsForType(type) { if (type === "Box" || type === "Sphere") { - type = "Shape"; + showGroupsForTypes(["Shape"]); + return; } - - let typeGroups = GROUPS_PER_TYPE[type]; + showGroupsForTypes([type]); +} - for (let groupKey in elGroups) { - let elGroup = elGroups[groupKey]; - if (typeGroups && typeGroups.indexOf(groupKey) > -1) { +function getGroupsForTypes(types) { + return Object.keys(elGroups).filter((groupKey) => { + return types.map(type => GROUPS_PER_TYPE[type].includes(groupKey)).every(function (hasGroup) { + return hasGroup; + }); + }); +} + +function showGroupsForTypes(types) { + Object.entries(elGroups).forEach(([groupKey, elGroup]) => { + if (types.map(type => GROUPS_PER_TYPE[type].includes(groupKey)).every(function (hasGroup) { return hasGroup; })) { elGroup.style.display = "block"; } else { elGroup.style.display = "none"; } - } + }); } -function getPropertyValue(originalPropertyName) { - // if this is a compound property name (i.e. animation.running) +function getFirstSelectedID() { + if (selectedEntityIDs.size === 0) { + return null; + } + return selectedEntityIDs.values().next().value; +} + +/** + * Returns true when the user is currently dragging the numeric slider control of the property + * @param propertyName - name of property + * @returns {boolean} currentlyDragging + */ +function isCurrentlyDraggingProperty(propertyName) { + return properties[propertyName] && properties[propertyName].dragging === true; +} + +const SUPPORTED_FALLBACK_TYPES = ['number', 'number-draggable', 'rect', 'vec3', 'vec2', 'color']; + +function getMultiplePropertyValue(originalPropertyName) { + // if this is a compound property name (i.e. animation.running) // then split it by . up to 3 times to find property value - let propertyValue; + + let propertyData = null; + if (properties[originalPropertyName] !== undefined) { + propertyData = properties[originalPropertyName].data; + } + + let propertyValues = []; let splitPropertyName = originalPropertyName.split('.'); if (splitPropertyName.length > 1) { let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; - let groupProperties = selectedEntityProperties[propertyGroupName]; - if (groupProperties === undefined || groupProperties[propertyName] === undefined) { - return undefined; - } - if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { - let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; - propertyValue = groupProperties[propertyName][subPropertyName]; - } else { - propertyValue = groupProperties[propertyName]; - } + propertyValues = currentSelections.map(selection => { + let groupProperties = selection.properties[propertyGroupName]; + if (groupProperties === undefined || groupProperties[propertyName] === undefined) { + return undefined; + } + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUB_PROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUB_PROPERTY]; + return groupProperties[propertyName][subPropertyName]; + } else { + return groupProperties[propertyName]; + } + }); } else { - propertyValue = selectedEntityProperties[originalPropertyName]; + propertyValues = currentSelections.map(selection => selection.properties[originalPropertyName]); + } + + if (propertyData !== null && propertyData.fallbackProperty !== undefined && + SUPPORTED_FALLBACK_TYPES.includes(propertyData.type)) { + + let fallbackMultiValue = null; + + for (let i = 0; i < propertyValues.length; ++i) { + let isPropertyNotNumber = false; + let propertyValue = propertyValues[i]; + if (propertyValue === undefined) { + continue; + } + switch (propertyData.type) { + case 'number': + case 'number-draggable': + isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null; + break; + case 'rect': + case 'vec3': + case 'vec2': + isPropertyNotNumber = isNaN(propertyValue.x) || propertyValue.x === null; + break; + case 'color': + isPropertyNotNumber = isNaN(propertyValue.red) || propertyValue.red === null; + break; + } + if (isPropertyNotNumber) { + if (fallbackMultiValue === null) { + fallbackMultiValue = getMultiplePropertyValue(propertyData.fallbackProperty); + } + propertyValues[i] = fallbackMultiValue.values[i]; + } + } + } + + const firstValue = propertyValues[0]; + const isMultiDiffValue = !propertyValues.every((x) => deepEqual(firstValue, x)); + + if (isMultiDiffValue) { + return { + value: undefined, + values: propertyValues, + isMultiDiffValue: true + } + } + + return { + value: propertyValues[0], + values: propertyValues, + isMultiDiffValue: false + }; +} + +/** + * Retrieve more detailed info for differing Numeric MultiplePropertyValue + * @param multiplePropertyValue - input multiplePropertyValue + * @param propertyData + * @returns {{keys: *[], propertyComponentDiff, averagePerPropertyComponent}} + */ +function getDetailedNumberMPVDiff(multiplePropertyValue, propertyData) { + let detailedValues = {}; + // Fixed numbers can't be easily averaged since they're strings, so lets keep an array of unmodified numbers + let unmodifiedValues = {}; + const DEFAULT_KEY = 0; + let uniqueKeys = new Set([]); + multiplePropertyValue.values.forEach(function(propertyValue) { + if (typeof propertyValue === "object") { + Object.entries(propertyValue).forEach(function([key, value]) { + if (!uniqueKeys.has(key)) { + uniqueKeys.add(key); + detailedValues[key] = []; + unmodifiedValues[key] = []; + } + detailedValues[key].push(applyInputNumberPropertyModifiers(value, propertyData)); + unmodifiedValues[key].push(value); + }); + } else { + if (!uniqueKeys.has(DEFAULT_KEY)) { + uniqueKeys.add(DEFAULT_KEY); + detailedValues[DEFAULT_KEY] = []; + unmodifiedValues[DEFAULT_KEY] = []; + } + detailedValues[DEFAULT_KEY].push(applyInputNumberPropertyModifiers(propertyValue, propertyData)); + unmodifiedValues[DEFAULT_KEY].push(propertyValue); + } + }); + let keys = [...uniqueKeys]; + + let propertyComponentDiff = {}; + Object.entries(detailedValues).forEach(function([key, value]) { + propertyComponentDiff[key] = [...new Set(value)].length > 1; + }); + + let averagePerPropertyComponent = {}; + Object.entries(unmodifiedValues).forEach(function([key, value]) { + let average = value.reduce((a, b) => a + b) / value.length; + averagePerPropertyComponent[key] = applyInputNumberPropertyModifiers(average, propertyData); + }); + + return { + keys, + propertyComponentDiff, + averagePerPropertyComponent, + }; +} + +function getDetailedSubPropertyMPVDiff(multiplePropertyValue, subPropertyName) { + let isChecked = false; + let checkedValues = multiplePropertyValue.values.map((value) => value.split(",").includes(subPropertyName)); + let isMultiDiff = !checkedValues.every(value => value === checkedValues[0]); + if (!isMultiDiff) { + isChecked = checkedValues[0]; + } + return { + isChecked, + isMultiDiff } - return propertyValue; } function updateVisibleSpaceModeProperties() { @@ -1829,12 +2016,11 @@ function updateVisibleSpaceModeProperties() { } } - /** * PROPERTY UPDATE FUNCTIONS */ -function updateProperty(originalPropertyName, propertyValue, isParticleProperty) { +function createPropertyUpdateObject(originalPropertyName, propertyValue) { let propertyUpdate = {}; // if this is a compound property name (i.e. animation.running) then split it by . up to 3 times let splitPropertyName = originalPropertyName.split('.'); @@ -1842,8 +2028,8 @@ function updateProperty(originalPropertyName, propertyValue, isParticleProperty) let propertyGroupName = splitPropertyName[PROPERTY_NAME_DIVISION.GROUP]; let propertyName = splitPropertyName[PROPERTY_NAME_DIVISION.PROPERTY]; propertyUpdate[propertyGroupName] = {}; - if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUBPROPERTY + 1) { - let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUBPROPERTY]; + if (splitPropertyName.length === PROPERTY_NAME_DIVISION.SUB_PROPERTY + 1) { + let subPropertyName = splitPropertyName[PROPERTY_NAME_DIVISION.SUB_PROPERTY]; propertyUpdate[propertyGroupName][propertyName] = {}; propertyUpdate[propertyGroupName][propertyName][subPropertyName] = propertyValue; } else { @@ -1852,6 +2038,12 @@ function updateProperty(originalPropertyName, propertyValue, isParticleProperty) } else { propertyUpdate[originalPropertyName] = propertyValue; } + return propertyUpdate; +} + +function updateProperty(originalPropertyName, propertyValue, isParticleProperty) { + let propertyUpdate = createPropertyUpdateObject(originalPropertyName, propertyValue); + // queue up particle property changes with the debounced sync to avoid // causing particle emitting to reset excessively with each value change if (isParticleProperty) { @@ -1861,9 +2053,9 @@ function updateProperty(originalPropertyName, propertyValue, isParticleProperty) particleSyncDebounce(); } else { // only update the entity property value itself if in the middle of dragging - // prevent undo command push, saving new property values, and property update + // prevent undo command push, saving new property values, and property update // callback until drag is complete (additional update sent via dragEnd callback) - let onlyUpdateEntity = properties[originalPropertyName] && properties[originalPropertyName].dragging === true; + let onlyUpdateEntity = isCurrentlyDraggingProperty(originalPropertyName); updateProperties(propertyUpdate, onlyUpdateEntity); } } @@ -1878,15 +2070,27 @@ function updateProperties(propertiesToUpdate, onlyUpdateEntity) { onlyUpdateEntity = false; } EventBridge.emitWebEvent(JSON.stringify({ - id: lastEntityID, + ids: [...selectedEntityIDs], type: "update", properties: propertiesToUpdate, onlyUpdateEntities: onlyUpdateEntity })); } +function updateMultiDiffProperties(propertiesMapToUpdate, onlyUpdateEntity) { + if (onlyUpdateEntity === undefined) { + onlyUpdateEntity = false; + } + EventBridge.emitWebEvent(JSON.stringify({ + type: "update", + propertiesMap: propertiesMapToUpdate, + onlyUpdateEntities: onlyUpdateEntity + })); +} + function createEmitTextPropertyUpdateFunction(property) { return function() { + property.elInput.classList.remove('multi-diff'); updateProperty(property.name, this.value, property.isParticleProperty); }; } @@ -1906,60 +2110,70 @@ function createDragStartFunction(property) { function createDragEndFunction(property) { return function() { property.dragging = false; - // send an additional update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action - this.valueChangeFunction(); + + if (this.multiDiffModeEnabled) { + let propertyMultiValue = getMultiplePropertyValue(property.name); + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let entityID = selectedEntityIDsArray[i]; + updateObjects.push({ + entityIDs: [entityID], + properties: createPropertyUpdateObject(property.name, propertyMultiValue.values[i]), + }); + } + + // send a full updateMultiDiff post-dragging to count as an action in the undo stack + updateMultiDiffProperties(updateObjects); + } else { + // send an additional update post-dragging to consider whole property change from dragStart to dragEnd to be 1 action + this.valueChangeFunction(); + } }; } function createEmitNumberPropertyUpdateFunction(property) { return function() { - let multiplier = property.data.multiplier; - if (multiplier === undefined) { - multiplier = 1; - } - let value = parseFloat(this.value) * multiplier; + let value = parseFloat(applyOutputNumberPropertyModifiers(parseFloat(this.value), property.data)); updateProperty(property.name, value, property.isParticleProperty); }; } -function createEmitVec2PropertyUpdateFunction(property) { - return function () { - let multiplier = property.data.multiplier; - if (multiplier === undefined) { - multiplier = 1; - } - let newValue = { - x: property.elNumberX.elInput.value * multiplier, - y: property.elNumberY.elInput.value * multiplier - }; - updateProperty(property.name, newValue, property.isParticleProperty); - }; -} - -function createEmitVec3PropertyUpdateFunction(property) { +function createEmitNumberPropertyComponentUpdateFunction(property, propertyComponent) { return function() { - let multiplier = property.data.multiplier; - if (multiplier === undefined) { - multiplier = 1; - } - let newValue = { - x: property.elNumberX.elInput.value * multiplier, - y: property.elNumberY.elInput.value * multiplier, - z: property.elNumberZ.elInput.value * multiplier - }; - updateProperty(property.name, newValue, property.isParticleProperty); - }; -} + let propertyMultiValue = getMultiplePropertyValue(property.name); + let value = parseFloat(applyOutputNumberPropertyModifiers(parseFloat(this.value), property.data)); -function createEmitRectPropertyUpdateFunction(property) { - return function() { - let newValue = { - x: property.elNumberX.elInput.value, - y: property.elNumberY.elInput.value, - width: property.elNumberWidth.elInput.value, - height: property.elNumberHeight.elInput.value, - }; - updateProperty(property.name, newValue, property.isParticleProperty); + if (propertyMultiValue.isMultiDiffValue) { + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let entityID = selectedEntityIDsArray[i]; + + let propertyObject = propertyMultiValue.values[i]; + propertyObject[propertyComponent] = value; + + let updateObject = createPropertyUpdateObject(property.name, propertyObject); + updateObjects.push({ + entityIDs: [entityID], + properties: updateObject, + }); + + mergeDeep(currentSelections[i].properties, updateObject); + } + + // only update the entity property value itself if in the middle of dragging + // prevent undo command push, saving new property values, and property update + // callback until drag is complete (additional update sent via dragEnd callback) + let onlyUpdateEntity = isCurrentlyDraggingProperty(property.name); + updateMultiDiffProperties(updateObjects, onlyUpdateEntity); + } else { + let propertyValue = propertyMultiValue.value; + propertyValue[propertyComponent] = value; + updateProperty(property.name, propertyValue, property.isParticleProperty); + } }; } @@ -1979,16 +2193,34 @@ function emitColorPropertyUpdate(propertyName, red, green, blue, isParticlePrope updateProperty(propertyName, newValue, isParticleProperty); } -function updateCheckedSubProperty(propertyName, propertyValue, subPropertyElement, subPropertyString, isParticleProperty) { - if (subPropertyElement.checked) { - if (propertyValue.indexOf(subPropertyString)) { - propertyValue += subPropertyString + ','; - } - } else { - // We've unchecked, so remove - propertyValue = propertyValue.replace(subPropertyString + ",", ""); +function toggleBooleanCSV(inputCSV, property, enable) { + let values = inputCSV.split(","); + if (enable && !values.includes(property)) { + values.push(property); + } else if (!enable && values.includes(property)) { + values = values.filter(value => value !== property); + } + return values.join(","); +} + +function updateCheckedSubProperty(propertyName, propertyMultiValue, subPropertyElement, subPropertyString, isParticleProperty) { + if (propertyMultiValue.isMultiDiffValue) { + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let newValue = toggleBooleanCSV(propertyMultiValue.values[i], subPropertyString, subPropertyElement.checked); + updateObjects.push({ + entityIDs: [selectedEntityIDsArray[i]], + properties: createPropertyUpdateObject(propertyName, newValue), + }); + } + + updateMultiDiffProperties(updateObjects); + } else { + updateProperty(propertyName, toggleBooleanCSV(propertyMultiValue.value, subPropertyString, subPropertyElement.checked), + isParticleProperty); } - updateProperty(propertyName, propertyValue, isParticleProperty); } /** @@ -2005,7 +2237,7 @@ function createStringProperty(property, elProperty) { + ${propertyData.readOnly ? 'readonly' : ''}/> `); @@ -2014,7 +2246,12 @@ function createStringProperty(property, elProperty) { elInput.addEventListener('change', propertyData.onChange); } + + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elInput); + elProperty.appendChild(elMultiDiff); if (propertyData.buttons !== undefined) { addButtons(elProperty, elementID, propertyData.buttons, false); @@ -2047,7 +2284,10 @@ function createBoolProperty(property, elProperty) { let subPropertyOf = propertyData.subPropertyOf; if (subPropertyOf !== undefined) { elInput.addEventListener('change', function() { - updateCheckedSubProperty(subPropertyOf, selectedEntityProperties[subPropertyOf], + let subPropertyMultiValue = getMultiplePropertyValue(subPropertyOf); + + updateCheckedSubProperty(subPropertyOf, + subPropertyMultiValue, elInput, propertyName, property.isParticleProperty); }); } else { @@ -2068,8 +2308,8 @@ function createNumberProperty(property, elProperty) { class='hide-spinner' type="number" ${propertyData.placeholder ? 'placeholder="' + propertyData.placeholder + '"' : ''} - ${propertyData.readOnly ? 'readonly' : ''}> - `) + ${propertyData.readOnly ? 'readonly' : ''}/> + `); if (propertyData.min !== undefined) { elInput.setAttribute("min", propertyData.min); @@ -2086,7 +2326,11 @@ function createNumberProperty(property, elProperty) { elInput.addEventListener('change', createEmitNumberPropertyUpdateFunction(property)); + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elInput); + elProperty.appendChild(elMultiDiff); if (propertyData.buttons !== undefined) { addButtons(elProperty, elementID, propertyData.buttons, false); @@ -2107,24 +2351,79 @@ function updateNumberMinMax(property) { } } +/** + * + * @param {object} property - property update on step + * @param {string} [propertyComponent] - propertyComponent to update on step (e.g. enter 'x' to just update position.x) + * @returns {Function} + */ +function createMultiDiffStepFunction(property, propertyComponent) { + return function(step, shouldAddToUndoHistory) { + if (shouldAddToUndoHistory === undefined) { + shouldAddToUndoHistory = false; + } + + let propertyMultiValue = getMultiplePropertyValue(property.name); + if (!propertyMultiValue.isMultiDiffValue) { + console.log("setMultiDiffStepFunction is only supposed to be called in MultiDiff mode."); + return; + } + + let multiplier = property.data.multiplier !== undefined ? property.data.multiplier : 1; + + let applyDelta = step * multiplier; + + if (selectedEntityIDs.size !== propertyMultiValue.values.length) { + console.log("selectedEntityIDs and propertyMultiValue got out of sync."); + return; + } + let updateObjects = []; + const selectedEntityIDsArray = [...selectedEntityIDs]; + + for (let i = 0; i < selectedEntityIDsArray.length; ++i) { + let entityID = selectedEntityIDsArray[i]; + + let updatedValue; + if (propertyComponent !== undefined) { + let objectToUpdate = propertyMultiValue.values[i]; + objectToUpdate[propertyComponent] += applyDelta; + updatedValue = objectToUpdate; + } else { + updatedValue = propertyMultiValue.values[i] + applyDelta; + } + let propertiesUpdate = createPropertyUpdateObject(property.name, updatedValue); + updateObjects.push({ + entityIDs: [entityID], + properties: propertiesUpdate + }); + // We need to store these so that we can send a full update on the dragEnd + mergeDeep(currentSelections[i].properties, propertiesUpdate); + } + + updateMultiDiffProperties(updateObjects, !shouldAddToUndoHistory); + } +} + function createNumberDraggableProperty(property, elProperty) { let elementID = property.elementID; let propertyData = property.data; - + elProperty.className += " draggable-number-container"; let dragStartFunction = createDragStartFunction(property); let dragEndFunction = createDragEndFunction(property); let elDraggableNumber = new DraggableNumber(propertyData.min, propertyData.max, propertyData.step, propertyData.decimals, dragStartFunction, dragEndFunction); - - let defaultValue = propertyData.defaultValue; - if (defaultValue !== undefined) { - elDraggableNumber.elInput.value = defaultValue; + + let defaultValue = propertyData.defaultValue; + if (defaultValue !== undefined) { + elDraggableNumber.elInput.value = defaultValue; } let valueChangeFunction = createEmitNumberPropertyUpdateFunction(property); elDraggableNumber.setValueChangeFunction(valueChangeFunction); + + elDraggableNumber.setMultiDiffStepFunction(createMultiDiffStepFunction(property)); elDraggableNumber.elInput.setAttribute("id", elementID); elProperty.appendChild(elDraggableNumber.elDiv); @@ -2165,11 +2464,15 @@ function createRectProperty(property, elProperty) { elWidthHeightRow.appendChild(elNumberWidth.elDiv); elWidthHeightRow.appendChild(elNumberHeight.elDiv); - let valueChangeFunction = createEmitRectPropertyUpdateFunction(property); - elNumberX.setValueChangeFunction(valueChangeFunction); - elNumberY.setValueChangeFunction(valueChangeFunction); - elNumberWidth.setValueChangeFunction(valueChangeFunction); - elNumberHeight.setValueChangeFunction(valueChangeFunction); + elNumberX.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'x')); + elNumberY.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'y')); + elNumberWidth.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'width')); + elNumberHeight.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'height')); + + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'x')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'y')); + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'width')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'height')); let elResult = []; elResult[RECT_ELEMENTS.X_NUMBER] = elNumberX; @@ -2199,11 +2502,14 @@ function createVec3Property(property, elProperty) { elProperty.appendChild(elNumberX.elDiv); elProperty.appendChild(elNumberY.elDiv); elProperty.appendChild(elNumberZ.elDiv); - - let valueChangeFunction = createEmitVec3PropertyUpdateFunction(property); - elNumberX.setValueChangeFunction(valueChangeFunction); - elNumberY.setValueChangeFunction(valueChangeFunction); - elNumberZ.setValueChangeFunction(valueChangeFunction); + + elNumberX.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'x')); + elNumberY.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'y')); + elNumberZ.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'z')); + + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'x')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'y')); + elNumberZ.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'z')); let elResult = []; elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; @@ -2226,10 +2532,12 @@ function createVec2Property(property, elProperty) { let elNumberY = createTupleNumberInput(property, propertyData.subLabels[VECTOR_ELEMENTS.Y_NUMBER]); elProperty.appendChild(elNumberX.elDiv); elProperty.appendChild(elNumberY.elDiv); - - let valueChangeFunction = createEmitVec2PropertyUpdateFunction(property); - elNumberX.setValueChangeFunction(valueChangeFunction); - elNumberY.setValueChangeFunction(valueChangeFunction); + + elNumberX.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'x')); + elNumberY.setValueChangeFunction(createEmitNumberPropertyComponentUpdateFunction(property, 'y')); + + elNumberX.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'x')); + elNumberY.setMultiDiffStepFunction(createMultiDiffStepFunction(property, 'y')); let elResult = []; elResult[VECTOR_ELEMENTS.X_NUMBER] = elNumberX; @@ -2293,7 +2601,6 @@ function createColorProperty(property, elProperty) { color: '000000', submit: false, // We don't want to have a submission button onShow: function(colpick) { - console.log("Showing"); // The original color preview within the picker needs to be updated on show because // prior to the picker being shown we don't have access to the selections' starting color. colorPickers[colorPickerID].colpickSetColor({ @@ -2342,7 +2649,7 @@ function createDropdownProperty(property, propertyID, elProperty) { } elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); - + elProperty.appendChild(elInput); return elInput; @@ -2361,8 +2668,12 @@ function createTextareaProperty(property, elProperty) { } elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); - + + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elInput); + elProperty.appendChild(elMultiDiff); if (propertyData.buttons !== undefined) { addButtons(elProperty, elementID, propertyData.buttons, true); @@ -2373,8 +2684,7 @@ function createTextareaProperty(property, elProperty) { function createIconProperty(property, elProperty) { let elementID = property.elementID; - let propertyData = property.data; - + elProperty.className = "value"; let elSpan = document.createElement('span'); @@ -2401,6 +2711,7 @@ function createTextureProperty(property, elProperty) { elInput.setAttribute("type", "text"); let imageLoad = function(url) { + elDiv.style.display = null; if (url.slice(0, 5).toLowerCase() === "atp:/") { elImage.src = ""; elImage.style.display = "none"; @@ -2422,12 +2733,18 @@ function createTextureProperty(property, elProperty) { } }; elInput.imageLoad = imageLoad; + elInput.setMultipleValues = function() { + elDiv.style.display = "none"; + }; elInput.addEventListener('change', createEmitTextPropertyUpdateFunction(property)); elInput.addEventListener('change', function(ev) { imageLoad(ev.target.value); }); elProperty.appendChild(elInput); + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + elProperty.appendChild(elMultiDiff); elProperty.appendChild(elDiv); let elResult = []; @@ -2457,7 +2774,7 @@ function createDynamicMultiselectProperty(property, elProperty) { let elDivOptions = document.createElement('div'); elDivOptions.setAttribute("id", elementID + "-options"); - elDivOptions.style = "overflow-y:scroll;max-height:160px;" + elDivOptions.style = "overflow-y:scroll;max-height:160px;"; let elDivButtons = document.createElement('div'); elDivButtons.setAttribute("id", elDivOptions.getAttribute("id") + "-buttons"); @@ -2633,7 +2950,7 @@ function createProperty(propertyData, propertyElementID, propertyName, propertyI */ function parentIDChanged() { - if (selectedEntityProperties.type === "Material") { + if (currentSelections.length === 1 && currentSelections[0].type === "Material") { requestMaterialTarget(); } } @@ -2710,6 +3027,7 @@ function clearUserData() { } function newJSONEditor() { + getPropertyInputElement("userData").classList.remove('multi-diff'); deleteJSONEditor(); createJSONEditor(); let data = {}; @@ -2719,8 +3037,11 @@ function newJSONEditor() { showSaveUserDataButton(); } -function saveUserData() { - saveJSONUserData(true); +/** + * @param {Set.} [entityIDsToUpdate] Entity IDs to update userData for. + */ +function saveUserData(entityIDsToUpdate) { + saveJSONUserData(true, entityIDsToUpdate); } function setJSONError(property, isError) { @@ -2730,11 +3051,14 @@ function setJSONError(property, isError) { $propertyUserDataEditorStatus.text(isError ? 'Invalid JSON code - look for red X in your code' : ''); } -function setUserDataFromEditor(noUpdate) { - let json = null; +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update userData for. + */ +function setUserDataFromEditor(noUpdate, entityIDsToUpdate) { let errorFound = false; try { - json = editor.get(); + editor.get(); } catch (e) { errorFound = true; } @@ -2749,7 +3073,7 @@ function setUserDataFromEditor(noUpdate) { if (noUpdate) { EventBridge.emitWebEvent( JSON.stringify({ - id: lastEntityID, + ids: [...entityIDsToUpdate], type: "saveUserData", properties: { userData: text @@ -2761,61 +3085,6 @@ function setUserDataFromEditor(noUpdate) { } } -function multiDataUpdater(groupName, updateKeyPair, userDataElement, defaults, removeKeys) { - let propertyUpdate = {}; - let parsedData = {}; - let keysToBeRemoved = removeKeys ? removeKeys : []; - try { - if ($('#property-userData-editor').css('height') !== "0px") { - // if there is an expanded, we want to use its json. - parsedData = getEditorJSON(); - } else { - parsedData = JSON.parse(userDataElement.value); - } - } catch (e) { - // TODO: Should an alert go here? - } - - if (!(groupName in parsedData)) { - parsedData[groupName] = {}; - } - let keys = Object.keys(updateKeyPair); - keys.forEach(function (key) { - if (updateKeyPair[key] !== null && updateKeyPair[key] !== "null") { - if (updateKeyPair[key] instanceof Element) { - if (updateKeyPair[key].type === "checkbox") { - parsedData[groupName][key] = updateKeyPair[key].checked; - } else { - let val = isNaN(updateKeyPair[key].value) ? updateKeyPair[key].value : parseInt(updateKeyPair[key].value); - parsedData[groupName][key] = val; - } - } else { - parsedData[groupName][key] = updateKeyPair[key]; - } - } else if (defaults[key] !== null && defaults[key] !== "null") { - parsedData[groupName][key] = defaults[key]; - } - }); - keysToBeRemoved.forEach(function(key) { - if (parsedData[groupName].hasOwnProperty(key)) { - delete parsedData[groupName][key]; - } - }); - - if (Object.keys(parsedData[groupName]).length === 0) { - delete parsedData[groupName]; - } - if (Object.keys(parsedData).length > 0) { - propertyUpdate.userData = JSON.stringify(parsedData); - } else { - propertyUpdate.userData = ''; - } - - userDataElement.value = propertyUpdate.userData; - - updateProperties(propertyUpdate, false); -} - let editor = null; function createJSONEditor() { @@ -2825,9 +3094,6 @@ function createJSONEditor() { mode: 'tree', modes: ['code', 'tree'], name: 'userData', - onModeChange: function() { - $('.jsoneditor-poweredBy').remove(); - }, onError: function(e) { alert('JSON editor:' + e); }, @@ -2838,8 +3104,6 @@ function createJSONEditor() { return; } $('#property-userData-button-save').attr('disabled', false); - - } }; editor = new JSONEditor(container, options); @@ -2897,10 +3161,6 @@ function setEditorJSON(json) { } } -function getEditorJSON() { - return editor.get(); -} - function deleteJSONEditor() { if (editor !== null) { setJSONError('userData', false); @@ -2911,8 +3171,12 @@ function deleteJSONEditor() { let savedJSONTimer = null; -function saveJSONUserData(noUpdate) { - setUserDataFromEditor(noUpdate); +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] Entity IDs to update userData for + */ +function saveJSONUserData(noUpdate, entityIDsToUpdate) { + setUserDataFromEditor(noUpdate, entityIDsToUpdate ? entityIDsToUpdate : selectedEntityIDs); $('#property-userData-saved').show(); $('#property-userData-button-save').attr('disabled', true); if (savedJSONTimer !== null) { @@ -2939,6 +3203,7 @@ function clearMaterialData() { } function newJSONMaterialEditor() { + getPropertyInputElement("materialData").classList.remove('multi-diff'); deleteJSONMaterialEditor(); createJSONMaterialEditor(); let data = {}; @@ -2952,11 +3217,14 @@ function saveMaterialData() { saveJSONMaterialData(true); } -function setMaterialDataFromEditor(noUpdate) { - let json = null; +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update materialData for. + */ +function setMaterialDataFromEditor(noUpdate, entityIDsToUpdate) { let errorFound = false; try { - json = materialEditor.get(); + materialEditor.get(); } catch (e) { errorFound = true; } @@ -2970,7 +3238,7 @@ function setMaterialDataFromEditor(noUpdate) { if (noUpdate) { EventBridge.emitWebEvent( JSON.stringify({ - id: lastEntityID, + ids: [...entityIDsToUpdate], type: "saveMaterialData", properties: { materialData: text @@ -2991,9 +3259,6 @@ function createJSONMaterialEditor() { mode: 'tree', modes: ['code', 'tree'], name: 'materialData', - onModeChange: function() { - $('.jsoneditor-poweredBy').remove(); - }, onError: function(e) { alert('JSON editor:' + e); }, @@ -3004,8 +3269,6 @@ function createJSONMaterialEditor() { return; } $('#property-materialData-button-save').attr('disabled', false); - - } }; materialEditor = new JSONEditor(container, options); @@ -3063,10 +3326,6 @@ function setMaterialEditorJSON(json) { } } -function getMaterialEditorJSON() { - return materialEditor.get(); -} - function deleteJSONMaterialEditor() { if (materialEditor !== null) { setJSONError('materialData', false); @@ -3077,8 +3336,12 @@ function deleteJSONMaterialEditor() { let savedMaterialJSONTimer = null; -function saveJSONMaterialData(noUpdate) { - setMaterialDataFromEditor(noUpdate); +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update materialData for. + */ +function saveJSONMaterialData(noUpdate, entityIDsToUpdate) { + setMaterialDataFromEditor(noUpdate, entityIDsToUpdate ? entityIDsToUpdate : selectedEntityIDs); $('#property-materialData-saved').show(); $('#property-materialData-button-save').attr('disabled', true); if (savedMaterialJSONTimer !== null) { @@ -3101,13 +3364,12 @@ function bindAllNonJSONEditorElements() { if (e.target.id === "property-userData-button-edit" || e.target.id === "property-userData-button-clear" || e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear") { return; - } else { - if ($('#property-userData-editor').css('height') !== "0px") { - saveUserData(); - } - if ($('#property-materialData-editor').css('height') !== "0px") { - saveMaterialData(); - } + } + if ($('#property-userData-editor').css('height') !== "0px") { + saveUserData(); + } + if ($('#property-materialData-editor').css('height') !== "0px") { + saveMaterialData(); } }); } @@ -3140,14 +3402,14 @@ function toggleDropdown(event) { } function closeAllDropdowns() { - elDropdowns = document.querySelectorAll("div.dropdown > dl"); + let elDropdowns = document.querySelectorAll("div.dropdown > dl"); for (let i = 0; i < elDropdowns.length; ++i) { elDropdowns[i].setAttribute('dropped', 'false'); } } function setDropdownValue(event) { - let dt = event.target.parentNode.parentNode.previousSibling; + let dt = event.target.parentNode.parentNode.previousSibling.previousSibling; dt.value = event.target.getAttribute("value"); dt.firstChild.textContent = event.target.textContent; @@ -3174,7 +3436,10 @@ function setTextareaScrolling(element) { */ function requestMaterialTarget() { - EventBridge.emitWebEvent(JSON.stringify({ type: 'materialTargetRequest', entityID: selectedEntityProperties.id })); + EventBridge.emitWebEvent(JSON.stringify({ + type: 'materialTargetRequest', + entityID: getFirstSelectedID(), + })); } function setMaterialTargetData(materialTargetData) { @@ -3268,7 +3533,7 @@ function sendMaterialTargetProperty() { if (elInput.checked) { let targetID = elInput.getAttribute("targetID"); if (elInput.getAttribute("isMaterialName") === "true") { - materialTargetList.push("mat::" + targetID); + materialTargetList.push(MATERIAL_PREFIX_STRING + targetID); } else { materialTargetList.push(targetID); } @@ -3300,7 +3565,7 @@ function materialTargetPropertyUpdate(propertyValue) { let targetID = elInput.getAttribute("targetID"); let materialTargetName = targetID; if (elInput.getAttribute("isMaterialName") === "true") { - materialTargetName = "mat::" + targetID; + materialTargetName = MATERIAL_PREFIX_STRING + targetID; } elInput.checked = materialTargets.indexOf(materialTargetName) >= 0; } @@ -3308,12 +3573,358 @@ function materialTargetPropertyUpdate(propertyValue) { elDivOptions.propertyValue = propertyValue; } +function roundAndFixNumber(number, propertyData) { + let result = number; + if (propertyData.round !== undefined) { + result = Math.round(result * propertyData.round) / propertyData.round; + } + if (propertyData.decimals !== undefined) { + return result.toFixed(propertyData.decimals) + } + return result; +} + +function applyInputNumberPropertyModifiers(number, propertyData) { + const multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + return roundAndFixNumber(number / multiplier, propertyData); +} + +function applyOutputNumberPropertyModifiers(number, propertyData) { + const multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; + return roundAndFixNumber(number * multiplier, propertyData); +} + +const areSetsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value)); + + +function handleEntitySelectionUpdate(selections, isPropertiesToolUpdate) { + const previouslySelectedEntityIDs = selectedEntityIDs; + currentSelections = selections; + selectedEntityIDs = new Set(selections.map(selection => selection.id)); + const multipleSelections = currentSelections.length > 1; + const hasSelectedEntityChanged = !areSetsEqual(selectedEntityIDs, previouslySelectedEntityIDs); + + if (selections.length === 0) { + deleteJSONEditor(); + deleteJSONMaterialEditor(); + + resetProperties(); + showGroupsForType("None"); + + let elIcon = properties.type.elSpan; + elIcon.innerText = NO_SELECTION; + elIcon.style.display = 'inline-block'; + + getPropertyInputElement("userData").value = ""; + showUserDataTextArea(); + showSaveUserDataButton(); + showNewJSONEditorButton(); + + getPropertyInputElement("materialData").value = ""; + showMaterialDataTextArea(); + showSaveMaterialDataButton(); + showNewJSONMaterialEditorButton(); + + disableProperties(); + } else { + if (!isPropertiesToolUpdate && !hasSelectedEntityChanged && document.hasFocus()) { + // in case the selection has not changed and we still have focus on the properties page, + // we will ignore the event. + return; + } + + if (hasSelectedEntityChanged) { + if (!multipleSelections) { + resetServerScriptStatus(); + } + } + + const doSelectElement = !hasSelectedEntityChanged; + + // Get unique entity types, and convert the types Sphere and Box to Shape + const shapeTypes = ["Sphere", "Box"]; + const entityTypes = [...new Set(currentSelections.map(a => + shapeTypes.includes(a.properties.type) ? "Shape" : a.properties.type))]; + + const shownGroups = getGroupsForTypes(entityTypes); + showGroupsForTypes(entityTypes); + + const lockedMultiValue = getMultiplePropertyValue('locked'); + + if (lockedMultiValue.isMultiDiffValue || lockedMultiValue.value) { + disableProperties(); + getPropertyInputElement('locked').removeAttribute('disabled'); + } else { + enableProperties(); + disableSaveUserDataButton(); + disableSaveMaterialDataButton() + } + + const certificateIDMultiValue = getMultiplePropertyValue('certificateID'); + const hasCertifiedInSelection = certificateIDMultiValue.isMultiDiffValue || certificateIDMultiValue.value !== ""; + + Object.entries(properties).forEach(function([propertyID, property]) { + const propertyData = property.data; + const propertyName = property.name; + let propertyMultiValue = getMultiplePropertyValue(propertyName); + let isMultiDiffValue = propertyMultiValue.isMultiDiffValue; + let propertyValue = propertyMultiValue.value; + + if (propertyData.selectionVisibility !== undefined) { + let visibility = propertyData.selectionVisibility; + let propertyVisible = true; + if (!multipleSelections) { + propertyVisible = isFlagSet(visibility, PROPERTY_SELECTION_VISIBILITY.SINGLE_SELECTION); + } else if (isMultiDiffValue) { + propertyVisible = isFlagSet(visibility, PROPERTY_SELECTION_VISIBILITY.MULTI_DIFF_SELECTIONS); + } else { + propertyVisible = isFlagSet(visibility, PROPERTY_SELECTION_VISIBILITY.MULTIPLE_SELECTIONS); + } + setPropertyVisibility(property, propertyVisible); + } + + const isSubProperty = propertyData.subPropertyOf !== undefined; + if (propertyValue === undefined && !isMultiDiffValue && !isSubProperty) { + return; + } + + if (!shownGroups.includes(property.group_id)) { + const WANT_DEBUG_SHOW_HIDDEN_FROM_GROUPS = false; + if (WANT_DEBUG_SHOW_HIDDEN_FROM_GROUPS) { + console.log("Skipping property " + property.data.label + " [" + property.name + + "] from hidden group " + property.group_id); + } + return; + } + + if (propertyData.hideIfCertified && hasCertifiedInSelection) { + propertyValue = "** Certified **"; + property.elInput.disabled = true; + } + + if (propertyName === "type") { + propertyValue = entityTypes.length > 1 ? "Multiple" : propertyMultiValue.values[0]; + } + + switch (propertyData.type) { + case 'string': { + if (isMultiDiffValue) { + if (propertyData.readOnly && propertyData.multiDisplayMode + && propertyData.multiDisplayMode === PROPERTY_MULTI_DISPLAY_MODE.COMMA_SEPARATED_VALUES) { + property.elInput.value = propertyMultiValue.values.join(", "); + } else { + property.elInput.classList.add('multi-diff'); + property.elInput.value = ""; + } + } else { + property.elInput.classList.remove('multi-diff'); + property.elInput.value = propertyValue; + } + break; + } + case 'bool': { + const inverse = propertyData.inverse !== undefined ? propertyData.inverse : false; + if (isSubProperty) { + let subPropertyMultiValue = getMultiplePropertyValue(propertyData.subPropertyOf); + let propertyValue = subPropertyMultiValue.value; + isMultiDiffValue = subPropertyMultiValue.isMultiDiffValue; + if (isMultiDiffValue) { + let detailedSubProperty = getDetailedSubPropertyMPVDiff(subPropertyMultiValue, propertyName); + property.elInput.checked = detailedSubProperty.isChecked; + property.elInput.classList.toggle('multi-diff', detailedSubProperty.isMultiDiff); + } else { + let subProperties = propertyValue.split(","); + let subPropertyValue = subProperties.indexOf(propertyName) > -1; + property.elInput.checked = inverse ? !subPropertyValue : subPropertyValue; + property.elInput.classList.remove('multi-diff'); + } + + } else { + if (isMultiDiffValue) { + property.elInput.checked = false; + } else { + property.elInput.checked = inverse ? !propertyValue : propertyValue; + } + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + } + + break; + } + case 'number': { + property.elInput.value = isMultiDiffValue ? "" : propertyValue; + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + break; + } + case 'number-draggable': { + let detailedNumberDiff = getDetailedNumberMPVDiff(propertyMultiValue, propertyData); + property.elNumber.setValue(detailedNumberDiff.averagePerPropertyComponent[0], detailedNumberDiff.propertyComponentDiff[0]); + break; + } + case 'rect': { + let detailedNumberDiff = getDetailedNumberMPVDiff(propertyMultiValue, propertyData); + property.elNumberX.setValue(detailedNumberDiff.averagePerPropertyComponent.x, detailedNumberDiff.propertyComponentDiff.x); + property.elNumberY.setValue(detailedNumberDiff.averagePerPropertyComponent.y, detailedNumberDiff.propertyComponentDiff.y); + property.elNumberWidth.setValue(detailedNumberDiff.averagePerPropertyComponent.width, detailedNumberDiff.propertyComponentDiff.width); + property.elNumberHeight.setValue(detailedNumberDiff.averagePerPropertyComponent.height, detailedNumberDiff.propertyComponentDiff.height); + break; + } + case 'vec3': + case 'vec2': { + let detailedNumberDiff = getDetailedNumberMPVDiff(propertyMultiValue, propertyData); + property.elNumberX.setValue(detailedNumberDiff.averagePerPropertyComponent.x, detailedNumberDiff.propertyComponentDiff.x); + property.elNumberY.setValue(detailedNumberDiff.averagePerPropertyComponent.y, detailedNumberDiff.propertyComponentDiff.y); + if (property.elNumberZ !== undefined) { + property.elNumberZ.setValue(detailedNumberDiff.averagePerPropertyComponent.z, detailedNumberDiff.propertyComponentDiff.z); + } + break; + } + case 'color': { + let displayColor = propertyMultiValue.isMultiDiffValue ? propertyMultiValue.values[0] : propertyValue; + property.elColorPicker.style.backgroundColor = "rgb(" + displayColor.red + "," + + displayColor.green + "," + + displayColor.blue + ")"; + property.elColorPicker.classList.toggle('multi-diff', propertyMultiValue.isMultiDiffValue); + + if (hasSelectedEntityChanged && $(property.elColorPicker).attr('active') === 'true') { + // Set the color picker inactive before setting the color, + // otherwise an update will be sent directly after setting it here. + $(property.elColorPicker).attr('active', 'false'); + colorPickers['#' + property.elementID].colpickSetColor({ + "r": displayColor.red, + "g": displayColor.green, + "b": displayColor.blue + }); + $(property.elColorPicker).attr('active', 'true'); + } + + property.elNumberR.setValue(displayColor.red); + property.elNumberG.setValue(displayColor.green); + property.elNumberB.setValue(displayColor.blue); + break; + } + case 'dropdown': { + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + property.elInput.value = isMultiDiffValue ? "" : propertyValue; + setDropdownText(property.elInput); + break; + } + case 'textarea': { + property.elInput.value = propertyValue; + setTextareaScrolling(property.elInput); + break; + } + case 'icon': { + property.elSpan.innerHTML = propertyData.icons[propertyValue]; + property.elSpan.style.display = "inline-block"; + break; + } + case 'texture': { + property.elInput.value = isMultiDiffValue ? "" : propertyValue; + property.elInput.classList.toggle('multi-diff', isMultiDiffValue); + if (isMultiDiffValue) { + property.elInput.setMultipleValues(); + } else { + property.elInput.imageLoad(property.elInput.value); + } + break; + } + case 'dynamic-multiselect': { + if (!isMultiDiffValue && property.data.propertyUpdate) { + property.data.propertyUpdate(propertyValue); + } + break; + } + } + + let showPropertyRules = property.showPropertyRules; + if (showPropertyRules !== undefined) { + for (let propertyToShow in showPropertyRules) { + let showIfThisPropertyValue = showPropertyRules[propertyToShow]; + let show = String(propertyValue) === String(showIfThisPropertyValue); + showPropertyElement(propertyToShow, show); + } + } + }); + + updateVisibleSpaceModeProperties(); + + let userDataMultiValue = getMultiplePropertyValue("userData"); + let userDataTextArea = getPropertyInputElement("userData"); + let json = null; + if (!userDataMultiValue.isMultiDiffValue) { + try { + json = JSON.parse(userDataMultiValue.value); + } catch (e) { + + } + } + if (json !== null) { + if (editor === null) { + createJSONEditor(); + } + userDataTextArea.classList.remove('multi-diff'); + setEditorJSON(json); + showSaveUserDataButton(); + hideUserDataTextArea(); + hideNewJSONEditorButton(); + hideUserDataSaved(); + } else { + // normal text + deleteJSONEditor(); + userDataTextArea.classList.toggle('multi-diff', userDataMultiValue.isMultiDiffValue); + userDataTextArea.value = userDataMultiValue.isMultiDiffValue ? "" : userDataMultiValue.value; + + showUserDataTextArea(); + showNewJSONEditorButton(); + hideSaveUserDataButton(); + hideUserDataSaved(); + } + + let materialDataMultiValue = getMultiplePropertyValue("materialData"); + let materialDataTextArea = getPropertyInputElement("materialData"); + let materialJson = null; + if (!materialDataMultiValue.isMultiDiffValue) { + try { + materialJson = JSON.parse(materialDataMultiValue.value); + } catch (e) { + + } + } + if (materialJson !== null) { + if (materialEditor === null) { + createJSONMaterialEditor(); + } + materialDataTextArea.classList.remove('multi-diff'); + setMaterialEditorJSON(materialJson); + showSaveMaterialDataButton(); + hideMaterialDataTextArea(); + hideNewJSONMaterialEditorButton(); + hideMaterialDataSaved(); + } else { + // normal text + deleteJSONMaterialEditor(); + materialDataTextArea.classList.toggle('multi-diff', materialDataMultiValue.isMultiDiffValue); + materialDataTextArea.value = materialDataMultiValue.isMultiDiffValue ? "" : materialDataMultiValue.value; + showMaterialDataTextArea(); + showNewJSONMaterialEditorButton(); + hideSaveMaterialDataButton(); + hideMaterialDataSaved(); + } + + if (hasSelectedEntityChanged && selections.length === 1 && entityTypes[0] === "Material") { + requestMaterialTarget(); + } + + let activeElement = document.activeElement; + if (doSelectElement && typeof activeElement.select !== "undefined") { + activeElement.select(); + } + } +} function loaded() { openEventBridge(function() { let elPropertiesList = document.getElementById("properties-list"); - - let templatePropertyRow = document.getElementById('property-row'); GROUPS.forEach(function(group) { let elGroup; @@ -3420,6 +4031,7 @@ function loaded() { property.isParticleProperty = group.id.includes("particles"); property.elContainer = elContainer; property.spaceMode = propertySpaceMode; + property.group_id = group.id; let elLabel = createElementFromHTML(`
${innerPropertyData.label}
`); createAppTooltip.registerTooltipElement(elLabel, propertyID, propertyName); @@ -3438,6 +4050,7 @@ function loaded() { property.isParticleProperty = group.id.includes("particles"); property.elContainer = elContainer; property.spaceMode = propertySpaceMode; + property.group_id = group.id; if (property.type !== 'placeholder') { properties[propertyID] = property; @@ -3473,7 +4086,7 @@ function loaded() { if (window.EventBridge !== undefined) { EventBridge.scriptEventReceived.connect(function(data) { data = JSON.parse(data); - if (data.type === "server_script_status") { + if (data.type === "server_script_status" && selectedEntityIDs.size === 1) { let elServerScriptError = document.getElementById("property-serverScripts-error"); let elServerScriptStatus = document.getElementById("property-serverScripts-status"); elServerScriptError.value = data.errorInfo; @@ -3492,324 +4105,7 @@ function loaded() { if (data.spaceMode !== undefined) { currentSpaceMode = data.spaceMode === "local" ? PROPERTY_SPACE_MODE.LOCAL : PROPERTY_SPACE_MODE.WORLD; } - if (data.selections.length === 0) { - if (lastEntityID !== null) { - if (editor !== null) { - saveUserData(); - deleteJSONEditor(); - } - if (materialEditor !== null) { - saveMaterialData(); - deleteJSONMaterialEditor(); - } - } - lastEntityID = null; - - resetProperties(); - showGroupsForType("None"); - - let elIcon = properties.type.elSpan; - elIcon.innerText = NO_SELECTION; - elIcon.style.display = 'inline-block'; - - deleteJSONEditor(); - getPropertyInputElement("userData").value = ""; - showUserDataTextArea(); - showSaveUserDataButton(); - showNewJSONEditorButton(); - - deleteJSONMaterialEditor(); - getPropertyInputElement("materialData").value = ""; - showMaterialDataTextArea(); - showSaveMaterialDataButton(); - showNewJSONMaterialEditorButton(); - - disableProperties(); - } else if (data.selections.length > 1) { - deleteJSONEditor(); - deleteJSONMaterialEditor(); - - let selections = data.selections; - - let ids = []; - let types = {}; - let numTypes = 0; - - for (let i = 0; i < selections.length; ++i) { - ids.push(selections[i].id); - let currentSelectedType = selections[i].properties.type; - if (types[currentSelectedType] === undefined) { - types[currentSelectedType] = 0; - numTypes += 1; - } - types[currentSelectedType]++; - } - - let type = "Multiple"; - if (numTypes === 1) { - type = selections[0].properties.type; - } - - resetProperties(); - showGroupsForType(type); - - let typeProperty = properties["type"]; - typeProperty.elSpan.innerHTML = typeProperty.data.icons[type]; - typeProperty.elSpan.style.display = "inline-block"; - - disableProperties(); - } else { - selectedEntityProperties = data.selections[0].properties; - - if (lastEntityID !== '"' + selectedEntityProperties.id + '"' && lastEntityID !== null) { - if (editor !== null) { - saveUserData(); - } - if (materialEditor !== null) { - saveMaterialData(); - } - } - - let hasSelectedEntityChanged = lastEntityID !== '"' + selectedEntityProperties.id + '"'; - - if (!data.isPropertiesToolUpdate && !hasSelectedEntityChanged && document.hasFocus()) { - // in case the selection has not changed and we still have focus on the properties page, - // we will ignore the event. - return; - } - - let doSelectElement = !hasSelectedEntityChanged; - - // the event bridge and json parsing handle our avatar id string differently. - lastEntityID = '"' + selectedEntityProperties.id + '"'; - - showGroupsForType(selectedEntityProperties.type); - - if (selectedEntityProperties.locked) { - disableProperties(); - getPropertyInputElement("locked").removeAttribute('disabled'); - } else { - enableProperties(); - disableSaveUserDataButton(); - disableSaveMaterialDataButton() - } - - for (let propertyID in properties) { - let property = properties[propertyID]; - let propertyData = property.data; - let propertyName = property.name; - let propertyValue = getPropertyValue(propertyName); - - let isSubProperty = propertyData.subPropertyOf !== undefined; - if (propertyValue === undefined && !isSubProperty) { - continue; - } - - if (propertyData.hideIfCertified) { - let shouldHide = selectedEntityProperties.certificateID !== ""; - if (shouldHide) { - propertyValue = "** Certified **"; - property.elInput.disabled = true; - } - } - - let isPropertyNotNumber = false; - switch (propertyData.type) { - case 'number': - case 'number-draggable': - isPropertyNotNumber = isNaN(propertyValue) || propertyValue === null; - break; - case 'rect': - case 'vec3': - case 'vec2': - isPropertyNotNumber = isNaN(propertyValue.x) || propertyValue.x === null; - break; - case 'color': - isPropertyNotNumber = isNaN(propertyValue.red) || propertyValue.red === null; - break; - } - if (isPropertyNotNumber && propertyData.fallbackProperty !== undefined) { - propertyValue = getPropertyValue(propertyData.fallbackProperty); - } - - switch (propertyData.type) { - case 'string': { - property.elInput.value = propertyValue; - break; - } - case 'bool': { - let inverse = propertyData.inverse !== undefined ? propertyData.inverse : false; - if (isSubProperty) { - let propertyValue = selectedEntityProperties[propertyData.subPropertyOf]; - let subProperties = propertyValue.split(","); - let subPropertyValue = subProperties.indexOf(propertyName) > -1; - property.elInput.checked = inverse ? !subPropertyValue : subPropertyValue; - } else { - property.elInput.checked = inverse ? !propertyValue : propertyValue; - } - break; - } - case 'number': { - property.elInput.value = propertyValue; - break; - } - case 'number-draggable': { - let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; - let value = propertyValue / multiplier; - if (propertyData.round !== undefined) { - value = Math.round(value.round) / propertyData.round; - } - property.elNumber.setValue(value); - break; - } - case 'rect': - property.elNumberX.setValue(propertyValue.x); - property.elNumberY.setValue(propertyValue.y); - property.elNumberWidth.setValue(propertyValue.width); - property.elNumberHeight.setValue(propertyValue.height); - break; - case 'vec3': - case 'vec2': { - let multiplier = propertyData.multiplier !== undefined ? propertyData.multiplier : 1; - let valueX = propertyValue.x / multiplier; - let valueY = propertyValue.y / multiplier; - let valueZ = propertyValue.z / multiplier; - if (propertyData.round !== undefined) { - valueX = Math.round(valueX * propertyData.round) / propertyData.round; - valueY = Math.round(valueY * propertyData.round) / propertyData.round; - valueZ = Math.round(valueZ * propertyData.round) / propertyData.round; - } - if (propertyData.decimals !== undefined) { - property.elNumberX.setValue(valueX.toFixed(propertyData.decimals)); - property.elNumberY.setValue(valueY.toFixed(propertyData.decimals)); - if (property.elNumberZ !== undefined) { - property.elNumberZ.setValue(valueZ.toFixed(propertyData.decimals)); - } - } else { - property.elNumberX.setValue(valueX); - property.elNumberY.setValue(valueY); - if (property.elNumberZ !== undefined) { - property.elNumberZ.setValue(valueZ); - } - } - break; - } - case 'color': { - property.elColorPicker.style.backgroundColor = "rgb(" + propertyValue.red + "," + - propertyValue.green + "," + - propertyValue.blue + ")"; - if (hasSelectedEntityChanged && $(property.elColorPicker).attr('active') === 'true') { - // Set the color picker inactive before setting the color, - // otherwise an update will be sent directly after setting it here. - $(property.elColorPicker).attr('active', 'false'); - colorPickers['#' + property.elementID].colpickSetColor({ - "r": propertyValue.red, - "g": propertyValue.green, - "b": propertyValue.blue - }); - $(property.elColorPicker).attr('active', 'true'); - } - - property.elNumberR.setValue(propertyValue.red); - property.elNumberG.setValue(propertyValue.green); - property.elNumberB.setValue(propertyValue.blue); - break; - } - case 'dropdown': { - property.elInput.value = propertyValue; - setDropdownText(property.elInput); - break; - } - case 'textarea': { - property.elInput.value = propertyValue; - setTextareaScrolling(property.elInput); - break; - } - case 'icon': { - property.elSpan.innerHTML = propertyData.icons[propertyValue]; - property.elSpan.style.display = "inline-block"; - break; - } - case 'texture': { - property.elInput.value = propertyValue; - property.elInput.imageLoad(property.elInput.value); - break; - } - case 'dynamic-multiselect': { - if (property.data.propertyUpdate) { - property.data.propertyUpdate(propertyValue); - } - break; - } - } - - let showPropertyRules = property.showPropertyRules; - if (showPropertyRules !== undefined) { - for (let propertyToShow in showPropertyRules) { - let showIfThisPropertyValue = showPropertyRules[propertyToShow]; - let show = String(propertyValue) === String(showIfThisPropertyValue); - showPropertyElement(propertyToShow, show); - } - } - } - - updateVisibleSpaceModeProperties(); - - let json = null; - try { - json = JSON.parse(selectedEntityProperties.userData); - } catch (e) { - // normal text - deleteJSONEditor(); - getPropertyInputElement("userData").value = selectedEntityProperties.userData; - showUserDataTextArea(); - showNewJSONEditorButton(); - hideSaveUserDataButton(); - hideUserDataSaved(); - } - if (json !== null) { - if (editor === null) { - createJSONEditor(); - } - setEditorJSON(json); - showSaveUserDataButton(); - hideUserDataTextArea(); - hideNewJSONEditorButton(); - hideUserDataSaved(); - } - - let materialJson = null; - try { - materialJson = JSON.parse(selectedEntityProperties.materialData); - } catch (e) { - // normal text - deleteJSONMaterialEditor(); - getPropertyInputElement("materialData").value = selectedEntityProperties.materialData; - showMaterialDataTextArea(); - showNewJSONMaterialEditorButton(); - hideSaveMaterialDataButton(); - hideMaterialDataSaved(); - } - if (materialJson !== null) { - if (materialEditor === null) { - createJSONMaterialEditor(); - } - setMaterialEditorJSON(materialJson); - showSaveMaterialDataButton(); - hideMaterialDataTextArea(); - hideNewJSONMaterialEditorButton(); - hideMaterialDataSaved(); - } - - if (hasSelectedEntityChanged && selectedEntityProperties.type === "Material") { - requestMaterialTarget(); - } - - let activeElement = document.activeElement; - if (doSelectElement && typeof activeElement.select !== "undefined") { - activeElement.select(); - } - } + handleEntitySelectionUpdate(data.selections, data.isPropertiesToolUpdate); } else if (data.type === 'tooltipsReply') { createAppTooltip.setIsEnabled(!data.hmdActive); createAppTooltip.setTooltipData(data.tooltips); @@ -3825,13 +4121,13 @@ function loaded() { if (propertyRange !== undefined) { let propertyData = properties[property].data; let multiplier = propertyData.multiplier; - if (propertyData.min === undefined && propertyRange.minimum != "") { + if (propertyData.min === undefined && propertyRange.minimum !== "") { propertyData.min = propertyRange.minimum; if (multiplier !== undefined) { propertyData.min /= multiplier; } } - if (propertyData.max === undefined && propertyRange.maximum != "") { + if (propertyData.max === undefined && propertyRange.maximum !== "") { propertyData.max = propertyRange.maximum; if (multiplier !== undefined) { propertyData.max /= multiplier; @@ -3855,7 +4151,7 @@ function loaded() { } } } else if (data.type === 'materialTargetReply') { - if (data.entityID === selectedEntityProperties.id) { + if (data.entityID === getFirstSelectedID()) { setMaterialTargetData(data.materialTargetData); } } @@ -3877,7 +4173,7 @@ function loaded() { // Server Script Error let elServerScripts = getPropertyInputElement("serverScripts"); - elDiv = document.createElement('div'); + let elDiv = document.createElement('div'); elDiv.className = "property"; let elServerScriptError = document.createElement('textarea'); let serverScriptErrorElementID = 'property-serverScripts-error'; @@ -3994,6 +4290,10 @@ function loaded() { dt.addEventListener("click", toggleDropdown, true); dl.appendChild(dt); + let elMultiDiff = document.createElement('span'); + elMultiDiff.className = "multi-diff"; + dl.appendChild(elMultiDiff); + let span = document.createElement("span"); span.setAttribute("value", options[selectedOption].value); span.textContent = options[selectedOption].firstChild.textContent; diff --git a/scripts/system/html/js/utils.js b/scripts/system/html/js/utils.js index d61b4d1762..9556856089 100644 --- a/scripts/system/html/js/utils.js +++ b/scripts/system/html/js/utils.js @@ -25,3 +25,70 @@ function disableDragDrop() { event.preventDefault(); }, false); } + +// mergeDeep function from https://stackoverflow.com/a/34749873 +/** + * Simple object check. + * @param item + * @returns {boolean} + */ +function mergeDeepIsObject(item) { + return (item && typeof item === 'object' && !Array.isArray(item)); +} + +/** + * Deep merge two objects. + * @param target + * @param sources + */ +function mergeDeep(target, ...sources) { + if (!sources.length) { + return target; + } + const source = sources.shift(); + + if (mergeDeepIsObject(target) && mergeDeepIsObject(source)) { + for (const key in source) { + if (!source.hasOwnProperty(key)) { + continue; + } + if (mergeDeepIsObject(source[key])) { + if (!target[key]) { + Object.assign(target, { [key]: {} }); + } + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + + return mergeDeep(target, ...sources); +} + +function deepEqual(a, b) { + if (a === b) { + return true; + } + + if (typeof(a) !== "object" || typeof(b) !== "object") { + return false; + } + + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + + for (let property in a) { + if (!a.hasOwnProperty(property)) { + continue; + } + if (!b.hasOwnProperty(property)) { + return false; + } + if (!deepEqual(a[property], b[property])) { + return false; + } + } + return true; +}