//// entitySelectionTool.js//// Created by Brad hefta-Gaub on October 1st, 2014.// Modified by Daniela Fontes * @DanielaFifo and Tiago Andrade @TagoWill on April 7th, 2017// Modified by David Back on January 9th, 2018// Copyright 2014 High Fidelity, Inc.// Copyright 2020 Vircadia contributors// Copyright 2022-2025 Overte e.V.//// This script implements a class useful for building tools for editing entities.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// SPDX-License-Identifier: Apache-2.0// /* global SelectionManager, SelectionDisplay, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, getMainTabletIDs, getControllerWorldLocation, TRIGGER_ON_VALUE */ const SPACE_LOCAL = "local";const SPACE_WORLD = "world";const HIGHLIGHT_LIST_NAME = "editHandleHighlightList";const MIN_DISTANCE_TO_REZ_FROM_AVATAR = 3; var controllerStandard = Controller.Standard; Script.include([ "../../libraries/controllers.js", "../../libraries/controllerDispatcherUtils.js", "../../libraries/utils.js"]); function deepCopy(v) { return JSON.parse(JSON.stringify(v));} function getDuplicateAppendedName(name) { var appendiceChar = "(0123456789)"; var currentChar = ""; var rippedName = name; var existingSequence = 0; var sequenceReader = ""; for (var i = name.length - 1; i >= 0; i--) { currentChar = name.charAt(i); if (i === (name.length - 1) && currentChar !== ")") { rippedName = name; break; } else { if (appendiceChar.indexOf(currentChar) === -1) { rippedName = name; break; } else { if (currentChar == "(" && i === name.length - 2) { rippedName = name; break; } else { if (currentChar == "(") { rippedName = name.substr(0, i-1); existingSequence = parseInt(sequenceReader); break; } else { sequenceReader = currentChar + sequenceReader; } } } } } if (existingSequence === 0) { rippedName = rippedName.trim() + " (2)"; } else { existingSequence++; rippedName = rippedName.trim() + " (" + existingSequence + ")"; } return rippedName.trim();} SelectionManager = (function() { var that = {}; // FUNCTION: SUBSCRIBE TO UPDATE MESSAGES function subscribeToUpdateMessages() { Messages.subscribe("entityToolUpdates"); Messages.messageReceived.connect(handleEntitySelectionToolUpdates); } // FUNCTION: HANDLE ENTITY SELECTION TOOL UPDATES function handleEntitySelectionToolUpdates(channel, message, sender) { //print("Channel: " + channel + " Sender: " + sender + " Message: " + JSON.stringify(message)); if (channel !== 'entityToolUpdates') { return; } if (sender !== MyAvatar.sessionUUID) { return; } var wantDebug = false; var messageParsed; try { messageParsed = JSON.parse(message); } catch (err) { print("ERROR: entitySelectionTool.handleEntitySelectionToolUpdates - got malformed message"); return; } if (messageParsed.method === "selectEntity") { if (!that.editEnabled) { return; } if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) { if (wantDebug) { print("setting selection to " + messageParsed.entityID); } if (that.createApp.expectingRotateAsClickedSurface) { if (!SelectionManager.hasSelection() || !SelectionManager.hasUnlockedSelection()) { audioFeedback.rejection(); Window.notifyEditError("You have nothing selected, or the selection is locked."); that.createApp.expectingRotateAsClickedSurface = false; } else { //Rotate Selection according the Surface Normal var normalRotation = Quat.lookAtSimple(Vec3.ZERO, Vec3.multiply(messageParsed.surfaceNormal, -1)); selectionDisplay.rotateSelection(normalRotation); //Translate Selection according the clicked Surface var distanceFromSurface; if (selectionDisplay.getSpaceMode() === SPACE_WORLD){ distanceFromSurface = SelectionManager.worldDimensions.z / 2; } else { distanceFromSurface = SelectionManager.localDimensions.z / 2; } selectionDisplay.moveSelection(Vec3.sum(messageParsed.intersection, Vec3.multiplyQbyV( normalRotation, {"x": 0.0, "y":0.0, "z": distanceFromSurface}))); that._update(false, this); that.createApp.pushCommandForSelections(); that.createApp.expectingRotateAsClickedSurface = false; audioFeedback.action(); } } else { var pickRay = SelectionDisplay.controllerComputePickRay(); var intersectObj = SelectionDisplay.testRayIntersect(pickRay, SelectionDisplay.allToolEntities); if (intersectObj.intersects) { return; } if (that.createApp.hmdMultiSelectMode) { that.addEntity(messageParsed.entityID, true, that); } else { that.setSelections([messageParsed.entityID], that); } } } } else if (messageParsed.method === "clearSelection") { if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) { that.clearSelections(); } } else if (messageParsed.method === "pointingAt") { if (messageParsed.hand === controllerStandard.RightHand) { that.pointingAtDesktopWindowRight = messageParsed.desktopWindow; that.pointingAtTabletRight = messageParsed.tablet; } else { that.pointingAtDesktopWindowLeft = messageParsed.desktopWindow; that.pointingAtTabletLeft = messageParsed.tablet; } } } subscribeToUpdateMessages(); // disabling this for now as it is causing rendering issues with the other handle toolEntities /* var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 }; var editHandleOutlineStyle = { outlineUnoccludedColor: COLOR_ORANGE_HIGHLIGHT, outlineOccludedColor: COLOR_ORANGE_HIGHLIGHT, fillUnoccludedColor: COLOR_ORANGE_HIGHLIGHT, fillOccludedColor: COLOR_ORANGE_HIGHLIGHT, outlineUnoccludedAlpha: 1, outlineOccludedAlpha: 0, fillUnoccludedAlpha: 0, fillOccludedAlpha: 0, outlineWidth: 3, isOutlineSmooth: true }; Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); */ that.savedProperties = {}; that.selections = []; var listeners = []; that.localRotation = Quat.IDENTITY; that.localPosition = Vec3.ZERO; that.localDimensions = Vec3.ZERO; that.localRegistrationPoint = Vec3.HALF; that.worldRotation = Quat.IDENTITY; that.worldPosition = Vec3.ZERO; that.worldDimensions = Vec3.ZERO; that.worldRegistrationPoint = Vec3.HALF; that.centerPosition = Vec3.ZERO; that.pointingAtDesktopWindowLeft = false; that.pointingAtDesktopWindowRight = false; that.pointingAtTabletLeft = false; that.pointingAtTabletRight = false; that.editEnabled = true; that.updateEditSettings = function(data) { if (data.createAppMode) { if (data.createAppMode === "object"){ that.editEnabled = true; } else { that.editEnabled = false; if(that.hasSelection()){ that.clearSelections(); } } } } that.saveProperties = function() { that.savedProperties = {}; for (var i = 0; i < that.selections.length; i++) { var entityID = that.selections[i]; that.savedProperties[entityID] = Entities.getEntityProperties(entityID); } }; that.addEventListener = function(func, thisContext) { listeners.push({ callback: func, thisContext: thisContext }); }; that.hasSelection = function() { return that.selections.length > 0; }; that.setSelections = function(entityIDs, caller) { print("setSelections: " + JSON.stringify(entityIDs)); Script.logBacktrace("setSelections"); // Clear highlight handle list const selection = Selection.getSelectedItemsList(HIGHLIGHT_LIST_NAME); if ( selection && selection.entities && selection.entities.length > 0 ) { for (let i = 0; i < that.selections.length; i++) { Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", that.selections[i]); } } // Clear class-level selections list that.selections = []; // Add entities to the highlight handle list and Class selections list for (let i = 0; i < entityIDs.length; i++) { var entityID = entityIDs[i]; that.selections.push(entityID); Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } that._update(true, caller); }; that.addEntity = function(entityID, toggleSelection, caller) { if (entityID) { var idx = -1; for (var i = 0; i < that.selections.length; i++) { if (entityID === that.selections[i]) { idx = i; break; } } if (idx === -1) { that.selections.push(entityID); Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } else if (toggleSelection) { that.selections.splice(idx, 1); Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } } that._update(true, caller); }; function removeEntityByID(entityID) { var idx = that.selections.indexOf(entityID); if (idx >= 0) { that.selections.splice(idx, 1); Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } } that.removeEntity = function (entityID, caller) { removeEntityByID(entityID); that._update(true, caller); }; that.removeEntities = function(entityIDs, caller) { for (var i = 0, length = entityIDs.length; i < length; i++) { removeEntityByID(entityIDs[i]); } that._update(true, caller); }; that.clearSelections = function(caller) { const selection = Selection.getSelectedItemsList(HIGHLIGHT_LIST_NAME); // Clear highlight handle list if ( selection && selection.entities && selection.entities.length > 0 ) { for (let i = 0; i < that.selections.length; i++) { Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", that.selections[i]); } } // Clear class-level selections list that.selections = []; that._update(true, caller); }; that.addChildrenEntities = function(parentEntityID, entityList, entityHostType) { var wantDebug = false; var children = Entities.getChildrenIDs(parentEntityID); var entityHostTypes = Entities.getMultipleEntityProperties(children, 'entityHostType'); for (var i = 0; i < children.length; i++) { var childID = children[i]; if (entityHostTypes[i].entityHostType !== entityHostType) { if (wantDebug) { console.log("Skipping addition of entity " + childID + " with conflicting entityHostType: " + entityHostTypes[i].entityHostType + ", expected: " + entityHostType); } continue; } if (entityList.indexOf(childID) < 0) { entityList.push(childID); } that.addChildrenEntities(childID, entityList, entityHostType); } }; // Determine if an entity is being grabbed. // This is mostly a heuristic - there is no perfect way to know if an entity is being // grabbed. // // @return {boolean} true if the given entity with `properties` is being grabbed by an avatar function nonDynamicEntityIsBeingGrabbedByAvatar(properties) { if (properties.dynamic || Uuid.isNull(properties.parentID)) { return false; } var avatar = AvatarList.getAvatar(properties.parentID); if (Uuid.isNull(avatar.sessionUUID)) { return false; } var grabJointNames = [ 'RightHand', 'LeftHand', '_CONTROLLER_RIGHTHAND', '_CONTROLLER_LEFTHAND', '_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND', '_CAMERA_RELATIVE_CONTROLLER_LEFTHAND', '_FARGRAB_RIGHTHAND', '_FARGRAB_LEFTHAND', '_FARGRAB_MOUSE' ]; for (var i = 0; i < grabJointNames.length; ++i) { if (avatar.getJointIndex(grabJointNames[i]) === properties.parentJointIndex) { return true; } } return false; } var entityClipboard = { entities: {}, // Map of id -> properties for copied entities position: { x: 0, y: 0, z: 0 }, dimensions: { x: 0, y: 0, z: 0 }, }; that.duplicateSelection = function() { var entitiesToDuplicate = []; var duplicatedEntityIDs = []; var duplicatedChildrenWithOldParents = []; var originalEntityToNewEntityID = []; SelectionManager.saveProperties(); // build list of entities to duplicate by including any unselected children of selected parent entities var originalEntityIDs = Object.keys(that.savedProperties); var entityHostTypes = Entities.getMultipleEntityProperties(originalEntityIDs, 'entityHostType'); for (var i = 0; i < originalEntityIDs.length; i++) { var originalEntityID = originalEntityIDs[i]; if (entitiesToDuplicate.indexOf(originalEntityID) === -1) { entitiesToDuplicate.push(originalEntityID); } that.addChildrenEntities(originalEntityID, entitiesToDuplicate, entityHostTypes[i].entityHostType); } var duplicateInterrupted = false; for (var j = 0; j < entitiesToDuplicate.length; j++) { var lockedEntities = Entities.getEntityProperties(entitiesToDuplicate[j], 'locked'); if (lockedEntities.locked) { duplicateInterrupted = true; break; } } if (duplicateInterrupted) { audioFeedback.rejection(); Window.notifyEditError("At least one of the selected entities or one of their children are locked."); return []; } // duplicate entities from above and store their original to new entity mappings and children needing re-parenting for (var i = 0; i < entitiesToDuplicate.length; i++) { var originalEntityID = entitiesToDuplicate[i]; var properties = that.savedProperties[originalEntityID]; if (properties === undefined) { properties = Entities.getEntityProperties(originalEntityID); } if (!properties.locked && (!properties.avatarEntity || properties.owningAvatarID === MyAvatar.sessionUUID)) { if (nonDynamicEntityIsBeingGrabbedByAvatar(properties)) { properties.parentID = null; properties.parentJointIndex = null; properties.localPosition = properties.position; properties.localRotation = properties.rotation; } properties.name = getDuplicateAppendedName(properties.name); properties.localVelocity = Vec3.ZERO; properties.localAngularVelocity = Vec3.ZERO; delete properties.actionData; var newEntityID = Entities.addEntity(properties); // Re-apply actions from the original entity var actionIDs = Entities.getActionIDs(properties.id); for (var j = 0; j < actionIDs.length; ++j) { var actionID = actionIDs[j]; var actionArguments = Entities.getActionArguments(properties.id, actionID); if (actionArguments) { var type = actionArguments.type; if (type === 'hold' || type === 'far-grab') { continue; } delete actionArguments.ttl; Entities.addAction(type, newEntityID, actionArguments); } } duplicatedEntityIDs.push({ entityID: newEntityID, properties: properties }); if (properties.parentID !== Uuid.NONE) { duplicatedChildrenWithOldParents[newEntityID] = properties.parentID; } originalEntityToNewEntityID[originalEntityID] = newEntityID; } } // re-parent duplicated children to the duplicate entities of their original parents (if they were duplicated) Object.keys(duplicatedChildrenWithOldParents).forEach(function(childIDNeedingNewParent) { var originalParentID = duplicatedChildrenWithOldParents[childIDNeedingNewParent]; var newParentID = originalEntityToNewEntityID[originalParentID]; if (newParentID) { Entities.editEntity(childIDNeedingNewParent, { parentID: newParentID }); for (var i = 0; i < duplicatedEntityIDs.length; i++) { var duplicatedEntity = duplicatedEntityIDs[i]; if (duplicatedEntity.entityID === childIDNeedingNewParent) { duplicatedEntity.properties.parentID = newParentID; } } } }); audioFeedback.confirmation(); return duplicatedEntityIDs; }; // Create the entities in entityProperties, maintaining parent-child relationships. // @param entityProperties {array} - Array of entity property objects that.createEntities = function(entityProperties) { var entitiesToCreate = []; var createdEntityIDs = []; var createdChildrenWithOldParents = []; var originalEntityToNewEntityID = []; that.saveProperties(); for (var i = 0; i < entityProperties.length; ++i) { var properties = entityProperties[i]; if (properties.parentID in originalEntityToNewEntityID) { properties.parentID = originalEntityToNewEntityID[properties.parentID]; } else { delete properties.parentID; } delete properties.actionData; var newEntityID = Entities.addEntity(properties);Lines 1–500 of 3501. Load more
// // entitySelectionTool.js // // Created by Brad hefta-Gaub on October 1st, 2014. // Modified by Daniela Fontes * @DanielaFifo and Tiago Andrade @TagoWill on April 7th, 2017 // Modified by David Back on January 9th, 2018 // Copyright 2014 High Fidelity, Inc. // Copyright 2020 Vircadia contributors // Copyright 2022-2025 Overte e.V. // // This script implements a class useful for building tools for editing entities. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // /* global SelectionManager, SelectionDisplay, grid, rayPlaneIntersection, rayPlaneIntersection2, pushCommandForSelections, getMainTabletIDs, getControllerWorldLocation, TRIGGER_ON_VALUE */ const SPACE_LOCAL = "local"; const SPACE_WORLD = "world"; const HIGHLIGHT_LIST_NAME = "editHandleHighlightList"; const MIN_DISTANCE_TO_REZ_FROM_AVATAR = 3; var controllerStandard = Controller.Standard; Script.include([ "../../libraries/controllers.js", "../../libraries/controllerDispatcherUtils.js", "../../libraries/utils.js" ]); function deepCopy(v) { return JSON.parse(JSON.stringify(v)); } function getDuplicateAppendedName(name) { var appendiceChar = "(0123456789)"; var currentChar = ""; var rippedName = name; var existingSequence = 0; var sequenceReader = ""; for (var i = name.length - 1; i >= 0; i--) { currentChar = name.charAt(i); if (i === (name.length - 1) && currentChar !== ")") { rippedName = name; break; } else { if (appendiceChar.indexOf(currentChar) === -1) { rippedName = name; break; } else { if (currentChar == "(" && i === name.length - 2) { rippedName = name; break; } else { if (currentChar == "(") { rippedName = name.substr(0, i-1); existingSequence = parseInt(sequenceReader); break; } else { sequenceReader = currentChar + sequenceReader; } } } } } if (existingSequence === 0) { rippedName = rippedName.trim() + " (2)"; } else { existingSequence++; rippedName = rippedName.trim() + " (" + existingSequence + ")"; } return rippedName.trim(); } SelectionManager = (function() { var that = {}; // FUNCTION: SUBSCRIBE TO UPDATE MESSAGES function subscribeToUpdateMessages() { Messages.subscribe("entityToolUpdates"); Messages.messageReceived.connect(handleEntitySelectionToolUpdates); } // FUNCTION: HANDLE ENTITY SELECTION TOOL UPDATES function handleEntitySelectionToolUpdates(channel, message, sender) { //print("Channel: " + channel + " Sender: " + sender + " Message: " + JSON.stringify(message)); if (channel !== 'entityToolUpdates') { return; } if (sender !== MyAvatar.sessionUUID) { return; } var wantDebug = false; var messageParsed; try { messageParsed = JSON.parse(message); } catch (err) { print("ERROR: entitySelectionTool.handleEntitySelectionToolUpdates - got malformed message"); return; } if (messageParsed.method === "selectEntity") { if (!that.editEnabled) { return; } if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) { if (wantDebug) { print("setting selection to " + messageParsed.entityID); } if (that.createApp.expectingRotateAsClickedSurface) { if (!SelectionManager.hasSelection() || !SelectionManager.hasUnlockedSelection()) { audioFeedback.rejection(); Window.notifyEditError("You have nothing selected, or the selection is locked."); that.createApp.expectingRotateAsClickedSurface = false; } else { //Rotate Selection according the Surface Normal var normalRotation = Quat.lookAtSimple(Vec3.ZERO, Vec3.multiply(messageParsed.surfaceNormal, -1)); selectionDisplay.rotateSelection(normalRotation); //Translate Selection according the clicked Surface var distanceFromSurface; if (selectionDisplay.getSpaceMode() === SPACE_WORLD){ distanceFromSurface = SelectionManager.worldDimensions.z / 2; } else { distanceFromSurface = SelectionManager.localDimensions.z / 2; } selectionDisplay.moveSelection(Vec3.sum(messageParsed.intersection, Vec3.multiplyQbyV( normalRotation, {"x": 0.0, "y":0.0, "z": distanceFromSurface}))); that._update(false, this); that.createApp.pushCommandForSelections(); that.createApp.expectingRotateAsClickedSurface = false; audioFeedback.action(); } } else { var pickRay = SelectionDisplay.controllerComputePickRay(); var intersectObj = SelectionDisplay.testRayIntersect(pickRay, SelectionDisplay.allToolEntities); if (intersectObj.intersects) { return; } if (that.createApp.hmdMultiSelectMode) { that.addEntity(messageParsed.entityID, true, that); } else { that.setSelections([messageParsed.entityID], that); } } } } else if (messageParsed.method === "clearSelection") { if (!SelectionDisplay.triggered() || SelectionDisplay.triggeredHand === messageParsed.hand) { that.clearSelections(); } } else if (messageParsed.method === "pointingAt") { if (messageParsed.hand === controllerStandard.RightHand) { that.pointingAtDesktopWindowRight = messageParsed.desktopWindow; that.pointingAtTabletRight = messageParsed.tablet; } else { that.pointingAtDesktopWindowLeft = messageParsed.desktopWindow; that.pointingAtTabletLeft = messageParsed.tablet; } } } subscribeToUpdateMessages(); // disabling this for now as it is causing rendering issues with the other handle toolEntities /* var COLOR_ORANGE_HIGHLIGHT = { red: 255, green: 99, blue: 9 }; var editHandleOutlineStyle = { outlineUnoccludedColor: COLOR_ORANGE_HIGHLIGHT, outlineOccludedColor: COLOR_ORANGE_HIGHLIGHT, fillUnoccludedColor: COLOR_ORANGE_HIGHLIGHT, fillOccludedColor: COLOR_ORANGE_HIGHLIGHT, outlineUnoccludedAlpha: 1, outlineOccludedAlpha: 0, fillUnoccludedAlpha: 0, fillOccludedAlpha: 0, outlineWidth: 3, isOutlineSmooth: true }; Selection.enableListHighlight(HIGHLIGHT_LIST_NAME, editHandleOutlineStyle); */ that.savedProperties = {}; that.selections = []; var listeners = []; that.localRotation = Quat.IDENTITY; that.localPosition = Vec3.ZERO; that.localDimensions = Vec3.ZERO; that.localRegistrationPoint = Vec3.HALF; that.worldRotation = Quat.IDENTITY; that.worldPosition = Vec3.ZERO; that.worldDimensions = Vec3.ZERO; that.worldRegistrationPoint = Vec3.HALF; that.centerPosition = Vec3.ZERO; that.pointingAtDesktopWindowLeft = false; that.pointingAtDesktopWindowRight = false; that.pointingAtTabletLeft = false; that.pointingAtTabletRight = false; that.editEnabled = true; that.updateEditSettings = function(data) { if (data.createAppMode) { if (data.createAppMode === "object"){ that.editEnabled = true; } else { that.editEnabled = false; if(that.hasSelection()){ that.clearSelections(); } } } } that.saveProperties = function() { that.savedProperties = {}; for (var i = 0; i < that.selections.length; i++) { var entityID = that.selections[i]; that.savedProperties[entityID] = Entities.getEntityProperties(entityID); } }; that.addEventListener = function(func, thisContext) { listeners.push({ callback: func, thisContext: thisContext }); }; that.hasSelection = function() { return that.selections.length > 0; }; that.setSelections = function(entityIDs, caller) { print("setSelections: " + JSON.stringify(entityIDs)); Script.logBacktrace("setSelections"); // Clear highlight handle list const selection = Selection.getSelectedItemsList(HIGHLIGHT_LIST_NAME); if ( selection && selection.entities && selection.entities.length > 0 ) { for (let i = 0; i < that.selections.length; i++) { Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", that.selections[i]); } } // Clear class-level selections list that.selections = []; // Add entities to the highlight handle list and Class selections list for (let i = 0; i < entityIDs.length; i++) { var entityID = entityIDs[i]; that.selections.push(entityID); Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } that._update(true, caller); }; that.addEntity = function(entityID, toggleSelection, caller) { if (entityID) { var idx = -1; for (var i = 0; i < that.selections.length; i++) { if (entityID === that.selections[i]) { idx = i; break; } } if (idx === -1) { that.selections.push(entityID); Selection.addToSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } else if (toggleSelection) { that.selections.splice(idx, 1); Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } } that._update(true, caller); }; function removeEntityByID(entityID) { var idx = that.selections.indexOf(entityID); if (idx >= 0) { that.selections.splice(idx, 1); Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", entityID); } } that.removeEntity = function (entityID, caller) { removeEntityByID(entityID); that._update(true, caller); }; that.removeEntities = function(entityIDs, caller) { for (var i = 0, length = entityIDs.length; i < length; i++) { removeEntityByID(entityIDs[i]); } that._update(true, caller); }; that.clearSelections = function(caller) { const selection = Selection.getSelectedItemsList(HIGHLIGHT_LIST_NAME); // Clear highlight handle list if ( selection && selection.entities && selection.entities.length > 0 ) { for (let i = 0; i < that.selections.length; i++) { Selection.removeFromSelectedItemsList(HIGHLIGHT_LIST_NAME, "entity", that.selections[i]); } } // Clear class-level selections list that.selections = []; that._update(true, caller); }; that.addChildrenEntities = function(parentEntityID, entityList, entityHostType) { var wantDebug = false; var children = Entities.getChildrenIDs(parentEntityID); var entityHostTypes = Entities.getMultipleEntityProperties(children, 'entityHostType'); for (var i = 0; i < children.length; i++) { var childID = children[i]; if (entityHostTypes[i].entityHostType !== entityHostType) { if (wantDebug) { console.log("Skipping addition of entity " + childID + " with conflicting entityHostType: " + entityHostTypes[i].entityHostType + ", expected: " + entityHostType); } continue; } if (entityList.indexOf(childID) < 0) { entityList.push(childID); } that.addChildrenEntities(childID, entityList, entityHostType); } }; // Determine if an entity is being grabbed. // This is mostly a heuristic - there is no perfect way to know if an entity is being // grabbed. // // @return {boolean} true if the given entity with `properties` is being grabbed by an avatar function nonDynamicEntityIsBeingGrabbedByAvatar(properties) { if (properties.dynamic || Uuid.isNull(properties.parentID)) { return false; } var avatar = AvatarList.getAvatar(properties.parentID); if (Uuid.isNull(avatar.sessionUUID)) { return false; } var grabJointNames = [ 'RightHand', 'LeftHand', '_CONTROLLER_RIGHTHAND', '_CONTROLLER_LEFTHAND', '_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND', '_CAMERA_RELATIVE_CONTROLLER_LEFTHAND', '_FARGRAB_RIGHTHAND', '_FARGRAB_LEFTHAND', '_FARGRAB_MOUSE' ]; for (var i = 0; i < grabJointNames.length; ++i) { if (avatar.getJointIndex(grabJointNames[i]) === properties.parentJointIndex) { return true; } } return false; } var entityClipboard = { entities: {}, // Map of id -> properties for copied entities position: { x: 0, y: 0, z: 0 }, dimensions: { x: 0, y: 0, z: 0 }, }; that.duplicateSelection = function() { var entitiesToDuplicate = []; var duplicatedEntityIDs = []; var duplicatedChildrenWithOldParents = []; var originalEntityToNewEntityID = []; SelectionManager.saveProperties(); // build list of entities to duplicate by including any unselected children of selected parent entities var originalEntityIDs = Object.keys(that.savedProperties); var entityHostTypes = Entities.getMultipleEntityProperties(originalEntityIDs, 'entityHostType'); for (var i = 0; i < originalEntityIDs.length; i++) { var originalEntityID = originalEntityIDs[i]; if (entitiesToDuplicate.indexOf(originalEntityID) === -1) { entitiesToDuplicate.push(originalEntityID); } that.addChildrenEntities(originalEntityID, entitiesToDuplicate, entityHostTypes[i].entityHostType); } var duplicateInterrupted = false; for (var j = 0; j < entitiesToDuplicate.length; j++) { var lockedEntities = Entities.getEntityProperties(entitiesToDuplicate[j], 'locked'); if (lockedEntities.locked) { duplicateInterrupted = true; break; } } if (duplicateInterrupted) { audioFeedback.rejection(); Window.notifyEditError("At least one of the selected entities or one of their children are locked."); return []; } // duplicate entities from above and store their original to new entity mappings and children needing re-parenting for (var i = 0; i < entitiesToDuplicate.length; i++) { var originalEntityID = entitiesToDuplicate[i]; var properties = that.savedProperties[originalEntityID]; if (properties === undefined) { properties = Entities.getEntityProperties(originalEntityID); } if (!properties.locked && (!properties.avatarEntity || properties.owningAvatarID === MyAvatar.sessionUUID)) { if (nonDynamicEntityIsBeingGrabbedByAvatar(properties)) { properties.parentID = null; properties.parentJointIndex = null; properties.localPosition = properties.position; properties.localRotation = properties.rotation; } properties.name = getDuplicateAppendedName(properties.name); properties.localVelocity = Vec3.ZERO; properties.localAngularVelocity = Vec3.ZERO; delete properties.actionData; var newEntityID = Entities.addEntity(properties); // Re-apply actions from the original entity var actionIDs = Entities.getActionIDs(properties.id); for (var j = 0; j < actionIDs.length; ++j) { var actionID = actionIDs[j]; var actionArguments = Entities.getActionArguments(properties.id, actionID); if (actionArguments) { var type = actionArguments.type; if (type === 'hold' || type === 'far-grab') { continue; } delete actionArguments.ttl; Entities.addAction(type, newEntityID, actionArguments); } } duplicatedEntityIDs.push({ entityID: newEntityID, properties: properties }); if (properties.parentID !== Uuid.NONE) { duplicatedChildrenWithOldParents[newEntityID] = properties.parentID; } originalEntityToNewEntityID[originalEntityID] = newEntityID; } } // re-parent duplicated children to the duplicate entities of their original parents (if they were duplicated) Object.keys(duplicatedChildrenWithOldParents).forEach(function(childIDNeedingNewParent) { var originalParentID = duplicatedChildrenWithOldParents[childIDNeedingNewParent]; var newParentID = originalEntityToNewEntityID[originalParentID]; if (newParentID) { Entities.editEntity(childIDNeedingNewParent, { parentID: newParentID }); for (var i = 0; i < duplicatedEntityIDs.length; i++) { var duplicatedEntity = duplicatedEntityIDs[i]; if (duplicatedEntity.entityID === childIDNeedingNewParent) { duplicatedEntity.properties.parentID = newParentID; } } } }); audioFeedback.confirmation(); return duplicatedEntityIDs; }; // Create the entities in entityProperties, maintaining parent-child relationships. // @param entityProperties {array} - Array of entity property objects that.createEntities = function(entityProperties) { var entitiesToCreate = []; var createdEntityIDs = []; var createdChildrenWithOldParents = []; var originalEntityToNewEntityID = []; that.saveProperties(); for (var i = 0; i < entityProperties.length; ++i) { var properties = entityProperties[i]; if (properties.parentID in originalEntityToNewEntityID) { properties.parentID = originalEntityToNewEntityID[properties.parentID]; } else { delete properties.parentID; } delete properties.actionData; var newEntityID = Entities.addEntity(properties);