diff --git a/lib/mayaUsd/ufe/MayaUsdContextOps.cpp b/lib/mayaUsd/ufe/MayaUsdContextOps.cpp index e66f1c7d5e..96e1511146 100644 --- a/lib/mayaUsd/ufe/MayaUsdContextOps.cpp +++ b/lib/mayaUsd/ufe/MayaUsdContextOps.cpp @@ -236,12 +236,89 @@ makeUSDReferenceFilePathRelativeIfRequested(const std::string& filePath, const U return relativePathAndSuccess.first; } -bool sceneItemSupportsShading(const Ufe::SceneItem::Ptr& sceneItem) +#ifdef UFE_V4_FEATURES_AVAILABLE +void addNewMaterialItems(const Ufe::ContextOps::ItemPath& itemPath, Ufe::ContextOps::Items& items) { - if (MayaUsd::ufe::BindMaterialUndoableCommand::CompatiblePrim(sceneItem)) { - return true; + std::multimap renderersAndMaterials; + MStringArray materials; + MGlobal::executeCommand("mayaUsdGetMaterialsFromRenderers", materials); + + for (const auto& materials : materials) { + // Expects a string in the format "renderer/Material Name|Material Identifier". + MStringArray rendererAndMaterial; + MStatus status = materials.split('/', rendererAndMaterial); + if (status == MS::kSuccess && rendererAndMaterial.length() == 2) { + renderersAndMaterials.emplace( + std::string(rendererAndMaterial[0].asChar()), rendererAndMaterial[1]); + } + } + + if (itemPath.size() == 1u) { + // Populate list of known renderers (first menu level). + for (auto it = renderersAndMaterials.begin(), end = renderersAndMaterials.end(); it != end; + it = renderersAndMaterials.upper_bound(it->first)) { + items.emplace_back(it->first, it->first, Ufe::ContextItem::kHasChildren); + } + } else if (itemPath.size() == 2u) { + // Populate list of materials for a given renderer (second menu level). + const auto range = renderersAndMaterials.equal_range(itemPath[1]); + for (auto it = range.first; it != range.second; ++it) { + MStringArray materialAndIdentifier; + // Expects a string in the format "Material Name|MaterialIdentifer". + MStatus status = it->second.split('|', materialAndIdentifier); + if (status == MS::kSuccess && materialAndIdentifier.length() == 2) { + items.emplace_back( + materialAndIdentifier[1].asChar(), materialAndIdentifier[0].asChar()); + } + } } - return false; +} + +void assignExistingMaterialItems( + const UsdSceneItem::Ptr& item, + const Ufe::ContextOps::ItemPath& itemPath, + Ufe::ContextOps::Items& items) +{ + std::multimap pathsAndMaterials; + MStringArray materials; + MString script; + script.format( + "mayaUsdGetMaterialsInStage \"^1s\"", Ufe::PathString::string(item->path()).c_str()); + MGlobal::executeCommand(script, materials); + + for (const auto& material : materials) { + MStringArray pathAndMaterial; + // Expects a string in the format "/path1/path2/Material". + const int lastSlash = material.rindex('/'); + if (lastSlash >= 0) { + MString pathToMaterial = material.substring(0, lastSlash); + pathsAndMaterials.emplace(std::string(pathToMaterial.asChar()), material); + } + } + + if (itemPath.size() == 1u) { + // Populate list of paths to materials (first menu level). + for (auto it = pathsAndMaterials.begin(), end = pathsAndMaterials.end(); it != end; + it = pathsAndMaterials.upper_bound(it->first)) { + items.emplace_back(it->first, it->first, Ufe::ContextItem::kHasChildren); + } + } else if (itemPath.size() == 2u) { + // Populate list of to materials for given path (second menu level). + const auto range = pathsAndMaterials.equal_range(itemPath[1]); + for (auto it = range.first; it != range.second; ++it) { + const int lastSlash = it->second.rindex('/'); + if (lastSlash >= 0) { + MString materialName = it->second.substring(lastSlash + 1, it->second.length() - 1); + items.emplace_back(it->second.asChar(), materialName.asChar()); + } + } + } +} +#endif + +inline bool sceneItemSupportsShading(const Ufe::SceneItem::Ptr& sceneItem) +{ + return MayaUsd::ufe::BindMaterialUndoableCommand::CompatiblePrim(sceneItem); } bool selectionSupportsShading() @@ -256,6 +333,19 @@ bool selectionSupportsShading() return false; } +#ifdef UFE_V4_FEATURES_AVAILABLE +bool canAssignMaterialToNodeType(const Ufe::SceneItem::Ptr& sceneItem) +{ + int allowMaterialFunctions = 0; + MString script; + script.format( + "mayaUsdMaterialBindings \"^1s\" -canAssignMaterialToNodeType true", + Ufe::PathString::string(sceneItem->path()).c_str()); + MGlobal::executeCommand(script, allowMaterialFunctions); + return (allowMaterialFunctions != 0); +} +#endif // UFE_V4_FEATURES_AVAILABLE + #ifdef UFE_V3_FEATURES_AVAILABLE void executeEditAsMaya(const Ufe::Path& path) @@ -307,12 +397,15 @@ MayaUsdContextOps::Ptr MayaUsdContextOps::create(const UsdSceneItem::Ptr& item) Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPath& itemPath) const { + if (isBulkEdit()) + return getBulkItems(itemPath); + // Get the items from our base class. - const auto baseItems = UsdUfe::UsdContextOps::getItems(itemPath); + const auto baseItems = Parent::getItems(itemPath); Ufe::ContextOps::Items items; if (itemPath.empty()) { - if (fItem->prim().IsA() && selectionSupportsShading()) { + if (_item->prim().IsA() && selectionSupportsShading()) { items.emplace_back(kBindMaterialToSelectionItem, kBindMaterialToSelectionLabel); items.emplace_back(Ufe::ContextItem::kSeparator); } @@ -324,7 +417,7 @@ Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPa #ifdef UFE_V3_FEATURES_AVAILABLE const bool isMayaRef = (prim().GetTypeName() == TfToken("MayaReference")); - if (!fIsAGatewayType && PrimUpdaterManager::getInstance().canEditAsMaya(path())) { + if (!_isAGatewayType && PrimUpdaterManager::getInstance().canEditAsMaya(path())) { items.emplace_back(kEditAsMayaItem, kEditAsMayaLabel, kEditAsMayaImage); #if (UFE_PREVIEW_VERSION_NUM >= 5007) Ufe::ContextItem item(kEditAsMayaOptionsItem, kEditAsMayaOptionsLabel); @@ -344,22 +437,17 @@ Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPa // Add the items from our base class here items.insert(items.end(), baseItems.begin(), baseItems.end()); - if (!fIsAGatewayType) { + if (!_isAGatewayType) { items.emplace_back(kAddRefOrPayloadItem, kAddRefOrPayloadLabel); items.emplace_back(kClearAllRefsOrPayloadsItem, kClearAllRefsOrPayloadsLabel); } - if (!fIsAGatewayType) { + if (!_isAGatewayType) { // Top level item - Bind/unbind existing materials bool materialSeparatorsAdded = false; - int allowMaterialFunctions = 0; + bool allowMaterialFunctions = false; #ifdef UFE_V4_FEATURES_AVAILABLE - MString script; - script.format( - "mayaUsdMaterialBindings \"^1s\" -canAssignMaterialToNodeType true", - Ufe::PathString::string(fItem->path()).c_str()); - MGlobal::executeCommand(script, allowMaterialFunctions); - - if (allowMaterialFunctions && sceneItemSupportsShading(fItem)) { + allowMaterialFunctions = canAssignMaterialToNodeType(_item); + if (allowMaterialFunctions && sceneItemSupportsShading(_item)) { if (!materialSeparatorsAdded) { items.emplace_back(Ufe::ContextItem::kSeparator); materialSeparatorsAdded = true; @@ -371,9 +459,10 @@ Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPa // Only show this option if we actually have materials in the stage. MStringArray materials; + MString script; script.format( "mayaUsdGetMaterialsInStage \"^1s\"", - Ufe::PathString::string(fItem->path()).c_str()); + Ufe::PathString::string(_item->path()).c_str()); MGlobal::executeCommand(script, materials); if (materials.length() > 0) { items.emplace_back( @@ -383,8 +472,8 @@ Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPa } } #endif - if (allowMaterialFunctions && fItem->prim().HasAPI()) { - UsdShadeMaterialBindingAPI bindingAPI(fItem->prim()); + if (allowMaterialFunctions && _item->prim().HasAPI()) { + UsdShadeMaterialBindingAPI bindingAPI(_item->prim()); // Show unbind menu item if there is a direct binding relationship: auto directBinding = bindingAPI.GetDirectBinding(); if (directBinding.GetMaterial()) { @@ -398,7 +487,7 @@ Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPa } } #ifdef UFE_V4_FEATURES_AVAILABLE - if (UsdUndoAddNewMaterialCommand::CompatiblePrim(fItem)) { + if (UsdUndoAddNewMaterialCommand::CompatiblePrim(_item)) { if (!materialSeparatorsAdded) { items.emplace_back(Ufe::ContextItem::kSeparator); materialSeparatorsAdded = true; @@ -413,8 +502,8 @@ Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPa items.insert(items.end(), baseItems.begin(), baseItems.end()); if (itemPath[0] == BindMaterialUndoableCommand::commandName) { - if (fItem) { - auto prim = fItem->prim(); + if (_item) { + auto prim = _item->prim(); if (prim) { // Find materials in the global selection. Either directly selected or a direct // child of the selection: @@ -449,83 +538,57 @@ Ufe::ContextOps::Items MayaUsdContextOps::getItems(const Ufe::ContextOps::ItemPa } #ifdef UFE_V4_FEATURES_AVAILABLE } else if (itemPath[0] == kAssignNewMaterialItem || itemPath[0] == kAddNewMaterialItem) { - std::multimap renderersAndMaterials; - MStringArray materials; - MGlobal::executeCommand("mayaUsdGetMaterialsFromRenderers", materials); - - for (const auto& materials : materials) { - // Expects a string in the format "renderer/Material Name|Material Identifier". - MStringArray rendererAndMaterial; - MStatus status = materials.split('/', rendererAndMaterial); - if (status == MS::kSuccess && rendererAndMaterial.length() == 2) { - renderersAndMaterials.emplace( - std::string(rendererAndMaterial[0].asChar()), rendererAndMaterial[1]); - } - } - - if (itemPath.size() == 1u) { - // Populate list of known renderers (first menu level). - for (auto it = renderersAndMaterials.begin(), end = renderersAndMaterials.end(); - it != end; - it = renderersAndMaterials.upper_bound(it->first)) { - items.emplace_back(it->first, it->first, Ufe::ContextItem::kHasChildren); - } - } else if (itemPath.size() == 2u) { - // Populate list of materials for a given renderer (second menu level). - const auto range = renderersAndMaterials.equal_range(itemPath[1]); - for (auto it = range.first; it != range.second; ++it) { - MStringArray materialAndIdentifier; - // Expects a string in the format "Material Name|MaterialIdentifer". - MStatus status = it->second.split('|', materialAndIdentifier); - if (status == MS::kSuccess && materialAndIdentifier.length() == 2) { - items.emplace_back( - materialAndIdentifier[1].asChar(), materialAndIdentifier[0].asChar()); - } - } - } + addNewMaterialItems(itemPath, items); } else if (itemPath[0] == kAssignExistingMaterialItem) { - std::multimap pathsAndMaterials; - MStringArray materials; - MString script; - script.format( - "mayaUsdGetMaterialsInStage \"^1s\"", - Ufe::PathString::string(fItem->path()).c_str()); - MGlobal::executeCommand(script, materials); - - for (const auto& material : materials) { - MStringArray pathAndMaterial; - // Expects a string in the format "/path1/path2/Material". - const int lastSlash = material.rindex('/'); - if (lastSlash >= 0) { - MString pathToMaterial = material.substring(0, lastSlash); - pathsAndMaterials.emplace(std::string(pathToMaterial.asChar()), material); - } - } - - if (itemPath.size() == 1u) { - // Populate list of paths to materials (first menu level). - for (auto it = pathsAndMaterials.begin(), end = pathsAndMaterials.end(); it != end; - it = pathsAndMaterials.upper_bound(it->first)) { - items.emplace_back(it->first, it->first, Ufe::ContextItem::kHasChildren); - } - } else if (itemPath.size() == 2u) { - // Populate list of to materials for given path (second menu level). - const auto range = pathsAndMaterials.equal_range(itemPath[1]); - for (auto it = range.first; it != range.second; ++it) { - const int lastSlash = it->second.rindex('/'); - if (lastSlash >= 0) { - MString materialName - = it->second.substring(lastSlash + 1, it->second.length() - 1); - items.emplace_back(it->second.asChar(), materialName.asChar()); - } - } - } + assignExistingMaterialItems(_item, itemPath, items); #endif } } // Top-level items return items; } +Ufe::ContextOps::Items MayaUsdContextOps::getBulkItems(const ItemPath& itemPath) const +{ + // Get the items from our base class and append ours to that list. + auto items = Parent::getBulkItems(itemPath); + + if (itemPath.empty()) { + items.emplace_back(Ufe::ContextItem::kSeparator); + +#ifdef UFE_V4_FEATURES_AVAILABLE + // Assign New Material: + items.emplace_back( + kAssignNewMaterialItem, kAssignNewMaterialLabel, Ufe::ContextItem::kHasChildren); + + // Only show this option if we actually have materials in the stage. + MStringArray materials; + MString script; + script.format( + "mayaUsdGetMaterialsInStage \"^1s\"", Ufe::PathString::string(_item->path()).c_str()); + MGlobal::executeCommand(script, materials); + if (materials.length() > 0) { + items.emplace_back( + kAssignExistingMaterialItem, + kAssignExistingMaterialLabel, + Ufe::ContextItem::kHasChildren); + } +#endif + items.emplace_back( + UnbindMaterialUndoableCommand::commandName, UnbindMaterialUndoableCommand::commandName); + } // top-level items + else { +#ifdef UFE_V4_FEATURES_AVAILABLE + if (itemPath[0] == kAssignNewMaterialItem) { + addNewMaterialItems(itemPath, items); + } else if (itemPath[0] == kAssignExistingMaterialItem) { + assignExistingMaterialItems(_item, itemPath, items); + } +#endif + } + + return items; +} + Ufe::UndoableCommand::Ptr MayaUsdContextOps::doOpCmd(const ItemPath& itemPath) { // Empty argument means no operation was specified, error. @@ -534,8 +597,11 @@ Ufe::UndoableCommand::Ptr MayaUsdContextOps::doOpCmd(const ItemPath& itemPath) return nullptr; } + if (isBulkEdit()) + return doBulkOpCmd(itemPath); + // First check if our base class handles this item. - auto cmd = UsdUfe::UsdContextOps::doOpCmd(itemPath); + auto cmd = Parent::doOpCmd(itemPath); if (cmd) return cmd; @@ -594,9 +660,9 @@ Ufe::UndoableCommand::Ptr MayaUsdContextOps::doOpCmd(const ItemPath& itemPath) return compoCmd; } } else if (itemPath[0] == kClearAllRefsOrPayloadsItem) { - if (fItem->path().empty()) + if (_item->path().empty()) return nullptr; - MString itemName = fItem->path().back().string().c_str(); + MString itemName = _item->path().back().string().c_str(); MString cmd; cmd.format( @@ -650,53 +716,107 @@ Ufe::UndoableCommand::Ptr MayaUsdContextOps::doOpCmd(const ItemPath& itemPath) } #endif else if (itemPath[0] == BindMaterialUndoableCommand::commandName) { - return std::make_shared(fItem->path(), SdfPath(itemPath[1])); + return std::make_shared(_item->path(), SdfPath(itemPath[1])); } else if (itemPath[0] == kBindMaterialToSelectionItem) { std::shared_ptr compositeCmd; if (auto globalSn = Ufe::GlobalSelection::get()) { for (auto&& selItem : *globalSn) { - if (BindMaterialUndoableCommand::CompatiblePrim(selItem)) { + if (sceneItemSupportsShading(selItem)) { if (!compositeCmd) { compositeCmd = std::make_shared(); } compositeCmd->append(std::make_shared( - selItem->path(), fItem->prim().GetPath())); + selItem->path(), _item->prim().GetPath())); } } } return compositeCmd; } else if (itemPath[0] == UnbindMaterialUndoableCommand::commandName) { - return std::make_shared(fItem->path()); + return std::make_shared(_item->path()); #ifdef UFE_V4_FEATURES_AVAILABLE } else if (itemPath.size() == 3u && itemPath[0] == kAssignNewMaterialItem) { - // Make a copy so that we don't change the user's original selection. - Ufe::Selection sceneItems(*Ufe::GlobalSelection::get()); - // As per UX' wishes, we add the item that was right-clicked, - // regardless of its selection state. - sceneItems.append(fItem); - if (sceneItems.size() > 0u) { - return std::make_shared( - UsdUndoAssignNewMaterialCommand::create(sceneItems, itemPath[2])); - } + // In single context item mode, only assign material to the context item. + return std::make_shared( + UsdUndoAssignNewMaterialCommand::create(_item, itemPath[2])); } else if (itemPath.size() == 3u && itemPath[0] == kAddNewMaterialItem) { return std::make_shared( - UsdUndoAddNewMaterialCommand::create(fItem, itemPath[2])); + UsdUndoAddNewMaterialCommand::create(_item, itemPath[2])); } else if (itemPath.size() == 3u && itemPath[0] == kAssignExistingMaterialItem) { - std::shared_ptr compositeCmd; - Ufe::Selection sceneItems(*Ufe::GlobalSelection::get()); - sceneItems.append(fItem); - for (auto& sceneItem : sceneItems) { - if (BindMaterialUndoableCommand::CompatiblePrim(sceneItem)) { - if (!compositeCmd) { - compositeCmd = std::make_shared(); + // In single context item mode, only assign material to the context item. + return std::make_shared(_item->path(), SdfPath(itemPath[2])); +#endif + } + return nullptr; +} + +Ufe::UndoableCommand::Ptr MayaUsdContextOps::doBulkOpCmd(const ItemPath& itemPath) +{ + // First check if our base class handles this item. + auto cmd = Parent::doBulkOpCmd(itemPath); + if (cmd) + return cmd; + + // List for the commands created (for CompositeUndoableCommand). If list + // is empty return nullptr instead so nothing will be executed. + std::list cmdList; + +#ifdef DEBUG + auto DEBUG_OUTPUT = [&cmdList](const Ufe::Selection& bulkItems) { + TF_STATUS( + "Performing bulk edit on %d prims (%d selected)", cmdList.size(), bulkItems.size()); + }; +#else +#define DEBUG_OUTPUT(x) (void)0 +#endif + + auto compositeCmdReturn = [&cmdList](const Ufe::Selection& bulkItems) { + DEBUG_OUTPUT(bulkItems); + return !cmdList.empty() ? std::make_shared(cmdList) + : nullptr; + }; + +#ifdef UFE_V4_FEATURES_AVAILABLE + if ((itemPath.size() == 3u) && (itemPath[0] == kAssignNewMaterialItem)) { + // In the bulk edit mode, we only apply the action to the selected + // items (not adding the item RMB if its outside the selection). + return std::make_shared( + UsdUndoAssignNewMaterialCommand::create(_bulkItems, itemPath[2])); + } else if (itemPath.size() == 3u && itemPath[0] == kAssignExistingMaterialItem) { + for (auto& selItem : _bulkItems) { + // The BindMaterialUndoableCommand will throw if given a prim type + // it cannot handle. Don't let any exception escape here. + try { + if (sceneItemSupportsShading(selItem)) { + cmdList.emplace_back(std::make_shared( + selItem->path(), SdfPath(itemPath[2]))); } - compositeCmd->append(std::make_shared( - sceneItem->path(), SdfPath(itemPath[2]))); + } catch (std::exception&) { + // Do nothing } } - return compositeCmd; + return compositeCmdReturn(_bulkItems); + } #endif + + if (itemPath[0] == UnbindMaterialUndoableCommand::commandName) { + for (auto& selItem : _bulkItems) { + // Only execute this menu item on items that have a direct binding relationship. + UsdSceneItem::Ptr usdItem = std::dynamic_pointer_cast(selItem); + if (usdItem) { + auto prim = usdItem->prim(); + if (prim.HasAPI()) { + UsdShadeMaterialBindingAPI bindingAPI(prim); + auto directBinding = bindingAPI.GetDirectBinding(); + if (directBinding.GetMaterial()) { + auto cmd = std::make_shared(selItem->path()); + cmdList.emplace_back(cmd); + } + } + } + } + return compositeCmdReturn(_bulkItems); } + return nullptr; } diff --git a/lib/mayaUsd/ufe/MayaUsdContextOps.h b/lib/mayaUsd/ufe/MayaUsdContextOps.h index abb60e617b..3cde4c04e4 100644 --- a/lib/mayaUsd/ufe/MayaUsdContextOps.h +++ b/lib/mayaUsd/ufe/MayaUsdContextOps.h @@ -53,6 +53,9 @@ class MAYAUSD_CORE_PUBLIC MayaUsdContextOps : public UsdUfe::UsdContextOps Items getItems(const ItemPath& itemPath) const override; Ufe::UndoableCommand::Ptr doOpCmd(const ItemPath& itemPath) override; + Items getBulkItems(const ItemPath& itemPath) const override; + Ufe::UndoableCommand::Ptr doBulkOpCmd(const ItemPath& itemPath) override; + UsdUfe::UsdContextOps::SchemaNameMap getSchemaPluginNiceNames() const override; }; // MayaUsdContextOps diff --git a/lib/mayaUsd/ufe/UsdUndoMaterialCommands.cpp b/lib/mayaUsd/ufe/UsdUndoMaterialCommands.cpp index cad990733d..923a19a6c0 100644 --- a/lib/mayaUsd/ufe/UsdUndoMaterialCommands.cpp +++ b/lib/mayaUsd/ufe/UsdUndoMaterialCommands.cpp @@ -150,6 +150,18 @@ bool _BindMaterialCompatiblePrim(const UsdPrim& usdPrim) return false; } +#ifdef UFE_V4_FEATURES_AVAILABLE +bool isDefPrim(const Ufe::SceneItem::Ptr& sceneItem) +{ + const auto canonicalName + = TfType::Find().FindDerivedByName(sceneItem->nodeType().c_str()); + if (canonicalName.IsUnknown()) { + return true; + } + return false; +} +#endif + } // namespace bool BindMaterialUndoableCommand::CompatiblePrim(const Ufe::SceneItem::Ptr& item) @@ -422,7 +434,7 @@ void UsdUndoAssignNewMaterialCommand::execute() } // There might be some unassignable items in the selection list. Skip and warn. // We know there is at least one assignable item found in the ContextOps resolver. - if (!BindMaterialUndoableCommand::CompatiblePrim(parentItem)) { + if (!BindMaterialUndoableCommand::CompatiblePrim(parentItem) || isDefPrim(parentItem)) { const std::string error = TfStringPrintf( "Assign new material: Skipping incompatible prim [%s] found in selection.", Ufe::PathString::string(parentItem->path()).c_str()); diff --git a/lib/usdUfe/ufe/UsdContextOps.cpp b/lib/usdUfe/ufe/UsdContextOps.cpp index 870c1d9e3b..dc58352b8d 100644 --- a/lib/usdUfe/ufe/UsdContextOps.cpp +++ b/lib/usdUfe/ufe/UsdContextOps.cpp @@ -17,6 +17,7 @@ #include "private/UfeNotifGuard.h" +#include #include #include #include @@ -30,8 +31,11 @@ #include #include +#include #include +#include + #include PXR_NAMESPACE_USING_DIRECTIVE @@ -53,13 +57,19 @@ static constexpr char kUSDUnloadLabel[] = "Unload"; static constexpr char kUSDVariantSetsItem[] = "Variant Sets"; static constexpr char kUSDVariantSetsLabel[] = "Variant Sets"; static constexpr char kUSDToggleVisibilityItem[] = "Toggle Visibility"; +static constexpr char kUSDMakeVisibleItem[] = "Make Visible"; static constexpr char kUSDMakeVisibleLabel[] = "Make Visible"; +static constexpr char kUSDMakeInvisibleItem[] = "Make Invisible"; static constexpr char kUSDMakeInvisibleLabel[] = "Make Invisible"; static constexpr char kUSDToggleActiveStateItem[] = "Toggle Active State"; +static constexpr char kUSDActivatePrimItem[] = "Activate Prim"; static constexpr char kUSDActivatePrimLabel[] = "Activate Prim"; +static constexpr char kUSDDeactivatePrimItem[] = "Deactivate Prim"; static constexpr char kUSDDeactivatePrimLabel[] = "Deactivate Prim"; static constexpr char kUSDToggleInstanceableStateItem[] = "Toggle Instanceable State"; -static constexpr char kUSDMarkAsInstancebaleLabel[] = "Mark as Instanceable"; +static constexpr char kUSDMarkAsInstanceabaleItem[] = "Mark as Instanceable"; +static constexpr char kUSDMarkAsInstanceabaleLabel[] = "Mark as Instanceable"; +static constexpr char kUSDUnmarkAsInstanceableItem[] = "Unmark as Instanceable"; static constexpr char kUSDUnmarkAsInstanceableLabel[] = "Unmark as Instanceable"; static constexpr char kUSDSetAsDefaultPrim[] = "Set as Default Prim"; static constexpr char kUSDClearDefaultPrim[] = "Clear Default Prim"; @@ -93,6 +103,10 @@ static const std::string kUSDSpherePrimImage { "out_USD_Sphere.png" }; static constexpr char kAllRegisteredTypesItem[] = "All Registered"; static constexpr char kAllRegisteredTypesLabel[] = "All Registered"; +static constexpr char kBulkEditItem[] = "BulkEdit"; +static constexpr char kBulkEditMixedTypeLabel[] = "%zu Prims Selected"; +static constexpr char kBulkEditSameTypeLabel[] = "%zu %s Prims Selected"; + std::vector> _computeLoadAndUnloadItems(const UsdPrim& prim) { @@ -230,8 +244,8 @@ std::vector UsdContextOps::schemaTypeGroups = {}; UsdContextOps::UsdContextOps(const UsdSceneItem::Ptr& item) : Ufe::ContextOps() - , fItem(item) { + setItem(item); } UsdContextOps::~UsdContextOps() { } @@ -242,15 +256,46 @@ UsdContextOps::Ptr UsdContextOps::create(const UsdSceneItem::Ptr& item) return std::make_shared(item); } -void UsdContextOps::setItem(const UsdSceneItem::Ptr& item) { fItem = item; } +void UsdContextOps::setItem(const UsdSceneItem::Ptr& item) +{ + _item = item; + + // We only support bulk editing USD prims (so not on the gateway item). + _bulkItems.clear(); + _bulkType.clear(); + if (item->runTimeId() == getUsdRunTimeId()) { + // Determine if this ContextOps should be in bulk edit mode. + if (auto globalSn = Ufe::GlobalSelection::get()) { + if (globalSn->contains(item->path())) { + // Only keep the selected items that match the runtime of the context item. + _bulkType = _item->nodeType(); + const auto usdId = _item->runTimeId(); + for (auto&& selItem : *globalSn) { + if (selItem->runTimeId() == usdId) { + _bulkItems.append(selItem); + if (selItem->nodeType() != _bulkType) + _bulkType.clear(); + } + } -const Ufe::Path& UsdContextOps::path() const { return fItem->path(); } + // In order to be in bulk edit mode we need at least two items. + // Our item plus one other. + if (_bulkItems.size() == 1) { + _bulkItems.clear(); + _bulkType.clear(); + } + } + } + } +} + +const Ufe::Path& UsdContextOps::path() const { return _item->path(); } //------------------------------------------------------------------------------ // Ufe::ContextOps overrides //------------------------------------------------------------------------------ -Ufe::SceneItem::Ptr UsdContextOps::sceneItem() const { return fItem; } +Ufe::SceneItem::Ptr UsdContextOps::sceneItem() const { return _item; } /*! Get the context ops items for the input item path. * @@ -278,9 +323,12 @@ Ufe::SceneItem::Ptr UsdContextOps::sceneItem() const { return fItem; } */ Ufe::ContextOps::Items UsdContextOps::getItems(const Ufe::ContextOps::ItemPath& itemPath) const { + if (isBulkEdit()) + return getBulkItems(itemPath); + Ufe::ContextOps::Items items; if (itemPath.empty()) { - if (!fIsAGatewayType) { + if (!_isAGatewayType) { // Working set management (load and unload): const auto itemLabelPairs = _computeLoadAndUnloadItems(prim()); for (const auto& itemLabelPair : itemLabelPairs) { @@ -330,8 +378,8 @@ Ufe::ContextOps::Items UsdContextOps::getItems(const Ufe::ContextOps::ItemPath& items.emplace_back( kUSDToggleInstanceableStateItem, prim().IsInstanceable() ? kUSDUnmarkAsInstanceableLabel - : kUSDMarkAsInstancebaleLabel); - } // !fIsAGatewayType + : kUSDMarkAsInstanceabaleLabel); + } // !_isAGatewayType // Top level item - Add New Prim (for all context op types). items.emplace_back(kUSDAddNewPrimItem, kUSDAddNewPrimLabel, Ufe::ContextItem::kHasChildren); @@ -417,6 +465,60 @@ Ufe::ContextOps::Items UsdContextOps::getItems(const Ufe::ContextOps::ItemPath& return items; } +// Adds the special Bulk Edit header as the first item. +void UsdContextOps::addBulkEditHeader(Ufe::ContextOps::Items& items) const +{ + assert(isBulkEdit()); + std::string bulkEditLabelStr; + if (_bulkType.empty()) { + bulkEditLabelStr = PXR_NS::TfStringPrintf(kBulkEditMixedTypeLabel, _bulkItems.size()); + } else { + bulkEditLabelStr + = PXR_NS::TfStringPrintf(kBulkEditSameTypeLabel, _bulkItems.size(), _bulkType.c_str()); + } + Ufe::ContextItem bulkEditItem(kBulkEditItem, bulkEditLabelStr); + bulkEditItem.enabled = Ufe::ContextItem::kDisabled; + + // Insert the header (and seperator) at the top of the menu. + items.emplace(items.begin(), Ufe::ContextItem::kSeparator); + items.emplace(items.begin(), bulkEditItem); +} + +/*! Called when the context ops is in bulk edit mode. + * + * This base class will build the following context menu: + * + * "{countOfPrimsSelected} {PrimType} Prims Selected" - disbled item has no action + * ----------------- + * Make Visible + * Make Invisible + * Activate Prim + * Deactivate Prim + * Mark as Instanceable + * Unmark as Instanceable + */ +Ufe::ContextOps::Items UsdContextOps::getBulkItems(const ItemPath& itemPath) const +{ + assert(isBulkEdit()); + Ufe::ContextOps::Items items; + if (itemPath.empty()) { + addBulkEditHeader(items); + + // Visibility: + items.emplace_back(kUSDMakeVisibleItem, kUSDMakeVisibleLabel); + items.emplace_back(kUSDMakeInvisibleItem, kUSDMakeInvisibleLabel); + + // Prim active state: + items.emplace_back(kUSDActivatePrimItem, kUSDActivatePrimLabel); + items.emplace_back(kUSDDeactivatePrimItem, kUSDDeactivatePrimLabel); + + // Instanceable: + items.emplace_back(kUSDMarkAsInstanceabaleItem, kUSDMarkAsInstanceabaleLabel); + items.emplace_back(kUSDUnmarkAsInstanceableItem, kUSDUnmarkAsInstanceableLabel); + } + return items; +} + Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) { // Empty argument means no operation was specified, error. @@ -425,6 +527,9 @@ Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) return nullptr; } + if (isBulkEdit()) + return doBulkOpCmd(itemPath); + if (itemPath[0u] == kUSDLoadItem || itemPath[0u] == kUSDLoadWithDescendantsItem) { const UsdLoadPolicy policy = (itemPath[0u] == kUSDLoadWithDescendantsItem) ? UsdLoadWithDescendants @@ -450,7 +555,7 @@ Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) else if (itemPath[0] == kUSDToggleVisibilityItem) { // Note: can use UsdObject3d::create() directly here since we know the item // supports it (because we added the menu item). - auto object3d = UsdObject3d::create(fItem); + auto object3d = UsdObject3d::create(_item); if (!TF_VERIFY(object3d)) return nullptr; // Don't use UsdObject3d::visibility() - it looks at the authored visibility @@ -476,9 +581,9 @@ Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) auto primType = itemPath[itemPath.size() - 1]; #ifdef UFE_V3_FEATURES_AVAILABLE return UsdUfe::UsdUndoSelectAfterCommand::create( - fItem, primType, primType); + _item, primType, primType); #else - return UsdUfe::UsdUndoAddNewPrimCommand::create(fItem, primType, primType); + return UsdUfe::UsdUndoAddNewPrimCommand::create(_item, primType, primType); #endif } else if (itemPath[0] == kUSDSetAsDefaultPrim) { return std::make_shared(prim()); @@ -488,6 +593,105 @@ Ufe::UndoableCommand::Ptr UsdContextOps::doOpCmd(const ItemPath& itemPath) return nullptr; } +Ufe::UndoableCommand::Ptr UsdContextOps::doBulkOpCmd(const ItemPath& itemPath) +{ + assert(isBulkEdit()); + + // List for the commands created (for CompositeUndoableCommand). If list + // is empty return nullptr instead so nothing will be executed. + std::list cmdList; + +#ifdef DEBUG + auto DEBUG_OUTPUT = [&cmdList](const Ufe::Selection& bulkItems) { + TF_STATUS( + "Performing bulk edit on %d prims (%d selected)", cmdList.size(), bulkItems.size()); + }; +#else +#define DEBUG_OUTPUT(x) (void)0 +#endif + + auto compositeCmdReturn = [&cmdList](const Ufe::Selection& bulkItems) { + DEBUG_OUTPUT(bulkItems); + return !cmdList.empty() ? std::make_shared(cmdList) + : nullptr; + }; + + // Prim Visibility: + const bool makeVisible = itemPath[0u] == kUSDMakeVisibleItem; + const bool makeInvisible = itemPath[0u] == kUSDMakeInvisibleItem; + if (makeVisible || makeInvisible) { + // We know that all the bulk items are in the Usd runtime. + auto object3dHndlr = UsdObject3dHandler::create(); + if (object3dHndlr) { + for (auto& selItem : _bulkItems) { + UsdSceneItem::Ptr usdItem = std::dynamic_pointer_cast(selItem); + if (usdItem) { + auto object3d = object3dHndlr->object3d(usdItem); + if (object3d) { + const auto imageable = UsdGeomImageable(usdItem->prim()); + const auto isVisible + = imageable.ComputeVisibility() != UsdGeomTokens->invisible; + Ufe::UndoableCommand::Ptr cmd; + try { + // The UsdUndoVisibleCommand constructor will throw + // if attribute editing is blocked. + if (isVisible && makeInvisible) { + cmd = object3d->setVisibleCmd(false); + cmdList.emplace_back(cmd); + } else if (!isVisible && makeVisible) { + cmd = object3d->setVisibleCmd(true); + cmdList.emplace_back(cmd); + } + } catch (std::exception&) { + // Do nothing + } + } + } + } + } + return compositeCmdReturn(_bulkItems); + } + + // Prim Active State: + const bool makeActive = itemPath[0u] == kUSDActivatePrimItem; + const bool makeInactive = itemPath[0u] == kUSDDeactivatePrimItem; + if (makeActive || makeInactive) { + for (auto& selItem : _bulkItems) { + UsdSceneItem::Ptr usdItem = std::dynamic_pointer_cast(selItem); + if (usdItem) { + auto prim = usdItem->prim(); + const bool primIsActive = prim.IsActive(); + if ((makeActive && !primIsActive) || (makeInactive && primIsActive)) { + auto cmd = std::make_shared(prim); + cmdList.emplace_back(cmd); + } + } + } + return compositeCmdReturn(_bulkItems); + } + + // Instanceable State: + const bool markInstanceable = itemPath[0u] == kUSDMarkAsInstanceabaleItem; + const bool unmarkInstanceable = itemPath[0u] == kUSDUnmarkAsInstanceableItem; + if (markInstanceable || unmarkInstanceable) { + for (auto& selItem : _bulkItems) { + UsdSceneItem::Ptr usdItem = std::dynamic_pointer_cast(selItem); + if (usdItem) { + auto prim = usdItem->prim(); + const bool primIsInstanceable = prim.IsInstanceable(); + if ((markInstanceable && !primIsInstanceable) + || (unmarkInstanceable && primIsInstanceable)) { + auto cmd = std::make_shared(prim); + cmdList.emplace_back(cmd); + } + } + } + return compositeCmdReturn(_bulkItems); + } + + return nullptr; +} + UsdContextOps::SchemaNameMap UsdContextOps::getSchemaPluginNiceNames() const { // clang-format off @@ -508,4 +712,41 @@ UsdContextOps::SchemaNameMap UsdContextOps::getSchemaPluginNiceNames() const return schemaPluginNiceNames; } +static_assert( + std::has_virtual_destructor::value, + "Destructor not virtual"); +static_assert( + std::is_base_of< + UsdBulkEditCompositeUndoableCommand::Parent, + UsdBulkEditCompositeUndoableCommand>::value, + "Verify base class"); + +void UsdBulkEditCompositeUndoableCommand::execute() +{ + // Same as base class in forward order. + // Iterate on our internal command list and only add to base class any commands + // which succeed (no error thrown). Thus we do not need to override undo/redo. + for (const auto& cmd : _cmds) { + if (cmd) { + try { + cmd->execute(); + Parent::append(cmd); + } catch (std::exception&) { + // Do nothing + } + } + } + + // Clear our internal list of commands since we added all the ones + // that succeeded to the base class list (for undo/redo). + _cmds.clear(); +} + +void UsdBulkEditCompositeUndoableCommand::addCommand(const Ptr& cmd) +{ + // Add the command to our own internal list. Later during execute + // we'll add the ones that succeed to the base class list. + _cmds.push_back(cmd); +} + } // namespace USDUFE_NS_DEF diff --git a/lib/usdUfe/ufe/UsdContextOps.h b/lib/usdUfe/ufe/UsdContextOps.h index 92439e7472..dc5cf32e21 100644 --- a/lib/usdUfe/ufe/UsdContextOps.h +++ b/lib/usdUfe/ufe/UsdContextOps.h @@ -22,6 +22,7 @@ #include #include +#include #include #include @@ -67,8 +68,8 @@ class USDUFE_PUBLIC UsdContextOps : public Ufe::ContextOps inline PXR_NS::UsdPrim prim() const { PXR_NAMESPACE_USING_DIRECTIVE - if (TF_VERIFY(fItem != nullptr)) - return fItem->prim(); + if (TF_VERIFY(_item != nullptr)) + return _item->prim(); else return PXR_NS::UsdPrim(); } @@ -76,15 +77,33 @@ class USDUFE_PUBLIC UsdContextOps : public Ufe::ContextOps // When we are created from a gateway node ContextOpsHandler we do not have the proper // UFE scene item. So it won't return the correct node type. Therefore we set // this flag directly. - void setIsAGatewayType(bool t) { fIsAGatewayType = t; } - bool isAGatewayType() const { return fIsAGatewayType; } + void setIsAGatewayType(bool t) { _isAGatewayType = t; } + bool isAGatewayType() const { return _isAGatewayType; } + + //! Returns true if this contextOps is in Bulk Edit mode. + //! Meaning there are multiple items selected and the operation will (potentially) + //! be ran on all of the them. + bool isBulkEdit() const { return !_bulkItems.empty(); } + + //! When in bulk edit mode returns the type of all the prims + //! if they are all of the same type. If mixed selection + //! then empty string is returned. + const std::string bulkEditType() const { return _bulkType; } // Ufe::ContextOps overrides Ufe::SceneItem::Ptr sceneItem() const override; Items getItems(const ItemPath& itemPath) const override; Ufe::UndoableCommand::Ptr doOpCmd(const ItemPath& itemPath) override; - // Helpers + // Bulk edit helpers: + + // Adds the special Bulk Edit header as the first item. + void addBulkEditHeader(Ufe::ContextOps::Items& items) const; + + // Will be called from getItems/doOpCmd respectively when in bulk edit mode. + // Can be overridden by derived class to add to the bulk items. + virtual Items getBulkItems(const ItemPath& itemPath) const; + virtual Ufe::UndoableCommand::Ptr doBulkOpCmd(const ItemPath& itemPath); // Called from getItems() method to replace the USD schema names with a // nice UI name (in the context menu). @@ -93,12 +112,41 @@ class USDUFE_PUBLIC UsdContextOps : public Ufe::ContextOps virtual SchemaNameMap getSchemaPluginNiceNames() const; protected: - UsdSceneItem::Ptr fItem; - bool fIsAGatewayType { false }; + UsdSceneItem::Ptr _item; + bool _isAGatewayType { false }; + std::string _bulkType; + Ufe::Selection _bulkItems; // A cache to keep the dynamic listing of plugin types to a minimum static std::vector schemaTypeGroups; }; // UsdContextOps +//! \brief Composite undoable command for Bulk Edit. +/*! + This class adds to the base Ufe::CompositeUndoableCommand by only keeping + commands that succeed. This is done because there can be edit restrictions + that will cause a command to fail, but we still want to execute the + remaining commands. + +*/ +class USDUFE_PUBLIC UsdBulkEditCompositeUndoableCommand : public Ufe::CompositeUndoableCommand +{ +public: + typedef Ufe::CompositeUndoableCommand Parent; + + UsdBulkEditCompositeUndoableCommand() = default; + + //! Add the command to the end of the list of commands. + void addCommand(const Ptr& cmd); + + // Overridden from Ufe::CompositeUndoableCommand + void execute() override; + +private: + // We keep our own list of command so that we can remove ones that error during + // execute. The base class list is private and only has const accessor. + CmdList _cmds; +}; + } // namespace USDUFE_NS_DEF diff --git a/test/lib/ufe/testContextOps.py b/test/lib/ufe/testContextOps.py index 47aac66d5f..4b01e30dca 100644 --- a/test/lib/ufe/testContextOps.py +++ b/test/lib/ufe/testContextOps.py @@ -152,6 +152,76 @@ def testGetItems(self): if c.checked: self.assertEqual(c.item, 'Ball_8') + def testGetBulkItems(self): + # The top-level context items are: + # Bulk Menu Header + # ----------------- + # Make Visible + # Make Invisible + # Activate Prim + # Deactivate Prim + # Mark as Instanceable + # Unmark as Instanceable + # ----------------- + # Assign New Material + # Assign Existing Material + # Unassign Material + + # The default setup selects a single prim (Ball_35) and creates context item + # for it. We want multiple selection to make a bulk context. + for ball in ['Ball_32', 'Ball_33', 'Ball_34']: + ballPath = ufe.Path([ + mayaUtils.createUfePathSegment("|transform1|proxyShape1"), + usdUtils.createUfePathSegment("/Room_set/Props/%s" % ball)]) + ballItem = ufe.Hierarchy.createItem(ballPath) + ufe.GlobalSelection.get().append(ballItem) + + # Re-create a ContextOps interface for it. + # Since the context item is in the selection, we get a bulk context. + self.contextOps = ufe.ContextOps.contextOps(self.ball35Item) + + contextItems = self.contextOps.getItems([]) + contextItemStrings = [c.item for c in contextItems] + + # Special header at top of menu. + self.assertIn('BulkEdit', contextItemStrings) + + # Not supported in bulk (from UsdContextOps). + self.assertNotIn('Load', contextItemStrings) + self.assertNotIn('Load with Descendants', contextItemStrings) + self.assertNotIn('Unload', contextItemStrings) + self.assertNotIn('Variant Sets', contextItemStrings) + self.assertNotIn('Add New Prim', contextItemStrings) + + # Two actions in bulk, instead of toggle. + self.assertIn('Make Visible', contextItemStrings) + self.assertIn('Make Invisible', contextItemStrings) + self.assertNotIn('Toggle Visibility', contextItemStrings) + + self.assertIn('Activate Prim', contextItemStrings) + self.assertIn('Deactivate Prim', contextItemStrings) + self.assertNotIn('Toggle Active State', contextItemStrings) + + self.assertIn('Mark as Instanceable', contextItemStrings) + self.assertIn('Unmark as Instanceable', contextItemStrings) + self.assertNotIn('Toggle Instanceable State', contextItemStrings) + + if ufeUtils.ufeFeatureSetVersion() >= 4: + self.assertIn('Assign New Material', contextItemStrings) + # Because there are no materials in the scene we don't have this item. + self.assertNotIn('Assign Existing Material', contextItemStrings) + self.assertIn('Unassign Material', contextItemStrings) + + # Not supported in bulk (from MayaUsdContextOps). + self.assertNotIn('Assign Material to Selection', contextItemStrings) + self.assertNotIn('USD Layer Editor', contextItemStrings) + self.assertNotIn('Edit As Maya Data', contextItemStrings) + self.assertNotIn('Duplicate As Maya Data', contextItemStrings) + self.assertNotIn('Add Maya Reference', contextItemStrings) + self.assertNotIn('AddReferenceOrPayload', contextItemStrings) + self.assertNotIn('ClearAllReferencesOrPayloads', contextItemStrings) + self.assertNotIn('ClearAllReferencesOrPayloads', contextItemStrings) + def testSwitchVariantInLayer(self): """ Test switching variant in layers: stronger, weaker, session. @@ -311,6 +381,129 @@ def shadingVariant(): cmds.undo() + def testDoBulkOp(self): + # The default setup selects a single prim (Ball_35) and creates context item + # for it. We want multiple selection to make a bulk context. + ballItems = {} + ballPrims = {} + for ball in [32, 33, 34]: + ballPath = ufe.Path([ + mayaUtils.createUfePathSegment("|transform1|proxyShape1"), + usdUtils.createUfePathSegment("/Room_set/Props/Ball_%d" % ball)]) + ballItem = ufe.Hierarchy.createItem(ballPath) + ballPrim = usdUtils.getPrimFromSceneItem(ballItem) + ufe.GlobalSelection.get().append(ballItem) + ballItems[ball] = ballItem + ballPrims[ball] = ballPrim + ballItems[35] = self.ball35Item + ballPrims[35] = self.ball35Prim + + # Re-create a ContextOps interface for it. + # Since the context item is in the selection, we get a bulk context. + self.contextOps = ufe.ContextOps.contextOps(self.ball35Item) + + # Change visility, undo/redo. + cmd = self.contextOps.doOpCmd(['Make Invisible']) + self.assertIsNotNone(cmd) + self.assertIsInstance(cmd, ufe.CompositeUndoableCommand) + + # Get visibility attr for each selected ball. + ballVisibility = {} + for ball in ballItems: + attrs = ufe.Attributes.attributes(ballItems[ball]) + self.assertIsNotNone(attrs) + visibility = attrs.attribute(UsdGeom.Tokens.visibility) + self.assertIsNotNone(visibility) + ballVisibility[ball] = visibility + + def verifyBulkVisibility(ballVisibility, visValue): + for ballVis in ballVisibility.values(): + self.assertEqual(ballVis.get(), visValue) + + # Initially all selected balls are inherited visibility. + verifyBulkVisibility(ballVisibility, UsdGeom.Tokens.inherited) + ufeCmd.execute(cmd) + verifyBulkVisibility(ballVisibility, UsdGeom.Tokens.invisible) + cmds.undo() + verifyBulkVisibility(ballVisibility, UsdGeom.Tokens.inherited) + cmds.redo() + verifyBulkVisibility(ballVisibility, UsdGeom.Tokens.invisible) + + # Make Visible + cmd = self.contextOps.doOpCmd(['Make Visible']) + self.assertIsNotNone(cmd) + self.assertIsInstance(cmd, ufe.CompositeUndoableCommand) + + # We left all the balls invisible from command above. + ufeCmd.execute(cmd) + verifyBulkVisibility(ballVisibility, UsdGeom.Tokens.inherited) + cmds.undo() + verifyBulkVisibility(ballVisibility, UsdGeom.Tokens.invisible) + cmds.redo() + verifyBulkVisibility(ballVisibility, UsdGeom.Tokens.inherited) + + # Deactivate Prim + cmd = self.contextOps.doOpCmd(['Deactivate Prim']) + self.assertIsNotNone(cmd) + self.assertIsInstance(cmd, ufe.CompositeUndoableCommand) + + def verifyBulkPrimState(ballPrims, state): + for ballPrim in ballPrims.values(): + self.assertEqual(ballPrim.IsActive(), state) + + # Initially, all selected balls should be Active. + verifyBulkPrimState(ballPrims, True) + ufeCmd.execute(cmd) + verifyBulkPrimState(ballPrims, False) + cmds.undo() + verifyBulkPrimState(ballPrims, True) + cmds.redo() + verifyBulkPrimState(ballPrims, False) + + # Activate Prim + cmd = self.contextOps.doOpCmd(['Activate Prim']) + self.assertIsNotNone(cmd) + self.assertIsInstance(cmd, ufe.CompositeUndoableCommand) + + # We left all the balls deactivated from command above. + ufeCmd.execute(cmd) + verifyBulkPrimState(ballPrims, True) + cmds.undo() + verifyBulkPrimState(ballPrims, False) + cmds.redo() + verifyBulkPrimState(ballPrims, True) + + # Mark as Instanceable + cmd = self.contextOps.doOpCmd(['Mark as Instanceable']) + self.assertIsNotNone(cmd) + self.assertIsInstance(cmd, ufe.CompositeUndoableCommand) + + def verifyBulkPrimInstState(ballPrims, state): + for ballPrim in ballPrims.values(): + self.assertEqual(ballPrim.IsInstanceable(), state) + + # Initially, all selected balls should be non-Instanceable. + verifyBulkPrimInstState(ballPrims, False) + ufeCmd.execute(cmd) + verifyBulkPrimInstState(ballPrims, True) + cmds.undo() + verifyBulkPrimInstState(ballPrims, False) + cmds.redo() + verifyBulkPrimInstState(ballPrims, True) + + # Unmark as Instanceable + cmd = self.contextOps.doOpCmd(['Unmark as Instanceable']) + self.assertIsNotNone(cmd) + self.assertIsInstance(cmd, ufe.CompositeUndoableCommand) + + # We left all the balls instanceable from command above. + ufeCmd.execute(cmd) + verifyBulkPrimInstState(ballPrims, False) + cmds.undo() + verifyBulkPrimInstState(ballPrims, True) + cmds.redo() + verifyBulkPrimInstState(ballPrims, False) + def testAddNewPrim(self): cmds.file(new=True, force=True) @@ -555,7 +748,7 @@ def testMaterialBinding(self): contextOps = ufe.ContextOps.contextOps(capsuleItem) cmd = contextOps.doOpCmd(['Assign Material', '/Material1']) - self.assertTrue(cmd) + self.assertIsNotNone(cmd) ufeCmd.execute(cmd) self.assertTrue(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) capsuleBindAPI = UsdShade.MaterialBindingAPI(capsulePrim) @@ -568,7 +761,7 @@ def testMaterialBinding(self): self.assertEqual(capsuleBindAPI.GetDirectBinding().GetMaterialPath(), Sdf.Path("/Material1")) cmd = contextOps.doOpCmd(['Unassign Material']) - self.assertTrue(cmd) + self.assertIsNotNone(cmd) ufeCmd.execute(cmd) self.assertTrue(capsuleBindAPI.GetDirectBinding().GetMaterialPath().isEmpty) @@ -726,12 +919,13 @@ def testMaterialCreationForMultipleObjects(self): spherePrim = usdUtils.getPrimFromSceneItem(sphereItem) self.assertFalse(spherePrim.HasAPI(UsdShade.MaterialBindingAPI)) - # Select two of our three objects. + # Select all three objects. + ufe.GlobalSelection.get().append(capsuleItem) ufe.GlobalSelection.get().append(cubeItem) ufe.GlobalSelection.get().append(sphereItem) - # Apply the new material on the unselected object. This object should also receive the new material binding, - # in addition to the two selected objects. + # Apply the new material one of the selected objects, so we are operating in + # bulk mode. All the selected objects should receive the new material binding. contextOps = ufe.ContextOps.contextOps(capsuleItem) # Create a new material and apply it to our cube, sphere and capsule objects. @@ -799,6 +993,7 @@ def checkItem(self, item, type, path): checkMaterial(self, rootHier, 4, 1, 1, 0, "UsdPreviewSurface", "", "surface") # Re-select our multiple objects so that we can repeat the test with another material. + ufe.GlobalSelection.get().append(capsuleItem) ufe.GlobalSelection.get().append(cubeItem) ufe.GlobalSelection.get().append(sphereItem) @@ -835,6 +1030,21 @@ def checkItem(self, item, type, path): ufeCmd.execute(cmdSS) checkMaterial(self, rootHier, 5, 1, 1, 0, "standard_surface", "mtlx", "out", "/test_scope") + # Unassign Material works on bulk + cmd = contextOps.doOpCmd(['Unassign Material']) + self.assertIsNotNone(cmd) + ufeCmd.execute(cmd) + + self.assertTrue(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) + capsuleBindAPI = UsdShade.MaterialBindingAPI(capsulePrim) + self.assertTrue(capsuleBindAPI.GetDirectBinding().GetMaterialPath().isEmpty) + self.assertTrue(cubePrim.HasAPI(UsdShade.MaterialBindingAPI)) + cubeBindAPI = UsdShade.MaterialBindingAPI(cubePrim) + self.assertTrue(cubeBindAPI.GetDirectBinding().GetMaterialPath().isEmpty) + self.assertTrue(spherePrim.HasAPI(UsdShade.MaterialBindingAPI)) + sphereBindAPI = UsdShade.MaterialBindingAPI(spherePrim) + self.assertTrue(sphereBindAPI.GetDirectBinding().GetMaterialPath().isEmpty) + @unittest.skipUnless(ufeUtils.ufeFeatureSetVersion() >= 4, 'Test only available in UFE v4 or greater') def testMaterialCreationScopeName(self): """This test verifies that materials get created in the correct scope.""" @@ -1156,7 +1366,7 @@ def testMaterialBindingWithNodeDefHandler(self): contextOps = ufe.ContextOps.contextOps(capsuleItem) cmd = contextOps.doOpCmd(['Assign Material', '/Material1']) - self.assertTrue(cmd) + self.assertIsNotNone(cmd) ufeCmd.execute(cmd) self.assertTrue(capsulePrim.HasAPI(UsdShade.MaterialBindingAPI)) capsuleBindAPI = UsdShade.MaterialBindingAPI(capsulePrim) @@ -1169,7 +1379,7 @@ def testMaterialBindingWithNodeDefHandler(self): self.assertEqual(capsuleBindAPI.GetDirectBinding().GetMaterialPath(), Sdf.Path("/Material1")) cmd = contextOps.doOpCmd(['Unassign Material']) - self.assertTrue(cmd) + self.assertIsNotNone(cmd) ufeCmd.execute(cmd) self.assertTrue(capsuleBindAPI.GetDirectBinding().GetMaterialPath().isEmpty) @@ -1219,7 +1429,7 @@ def noneHaveMaterial(self, items): contextOps = ufe.ContextOps.contextOps(rootHier.children()[2]) cmd = contextOps.doOpCmd(['Assign Material to Selection',]) - self.assertTrue(cmd) + self.assertIsNotNone(cmd) ufeCmd.execute(cmd) allHaveMaterial(self, [capsuleItem, sphereItem], "/Material1") cmds.undo() @@ -1230,7 +1440,7 @@ def noneHaveMaterial(self, items): # Test that undo restores previous material: contextOps = ufe.ContextOps.contextOps(rootHier.children()[3]) cmd = contextOps.doOpCmd(['Assign Material to Selection',]) - self.assertTrue(cmd) + self.assertIsNotNone(cmd) ufeCmd.execute(cmd) allHaveMaterial(self, [capsuleItem, sphereItem], "/Material2") cmds.undo()