Skip to content
2 changes: 2 additions & 0 deletions lib/mayaUsd/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ target_sources(${PROJECT_NAME}
targetLayer.cpp
traverseLayer.cpp
undoHelperCommand.cpp
utilComponentCreator.cpp
util.cpp
utilDictionary.cpp
utilFileSystem.cpp
Expand Down Expand Up @@ -65,6 +66,7 @@ set(HEADERS
traverseLayer.h
trieVisitor.h
undoHelperCommand.h
utilComponentCreator.h
util.h
utilDictionary.h
utilFileSystem.h
Expand Down
164 changes: 164 additions & 0 deletions lib/mayaUsd/utils/utilComponentCreator.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// Copyright 2025 Autodesk
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#include <mayaUsd/utils/utilComponentCreator.h>

#include <pxr/base/tf/diagnostic.h>
#include <pxr/pxr.h>

#include <maya/MGlobal.h>
#include <maya/MString.h>

#include <vector>

PXR_NAMESPACE_USING_DIRECTIVE

namespace MAYAUSD_NS_DEF {
namespace ComponentUtils {

std::vector<std::string> getAdskUsdComponentLayersToSave(const std::string& proxyPath)
{
// Ask via python what layers need to be saved for the component.
// With the maya api we can only return a string, so we concat the ids.
MString getLayersFromComponent;
getLayersFromComponent.format(
"def usd_component_creator_get_layers_to_save():\n"
" import mayaUsd\n"
" import mayaUsd.ufe\n"
" from usd_component_creator_plugin import MayaComponentManager\n"
" stage = mayaUsd.ufe.getStage('^1s')\n"
" if stage is None:\n"
" return ''\n"
" ids = MayaComponentManager.GetInstance().GetSaveInfo(stage)\n"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this fail? if so maybe we need to add validation on the evaluation and reporting that error occured.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(answer offline) the only thing that can fail is if the CC isnt present or the path isnt a component path - but this function documents expecting a valid component path - and this is made sure of before calling into this one. So any such error would be reported from "above"

" result = ''\n"
" first = True\n"
" for id in ids:\n"
" if not first:\n"
" result += '\\n'\n"
" result += id\n"
" first = False\n"
" return result\n",
proxyPath.c_str());

if (MGlobal::executePythonCommand(getLayersFromComponent)) {
auto resultString = MGlobal::executePythonCommandStringResult(
"usd_component_creator_get_layers_to_save()");
MStringArray layerIds;
resultString.split('\n', layerIds);
std::vector<std::string> toSave;
for (const auto& id : layerIds) {
toSave.push_back(id.asUTF8());
}
return toSave;
}
return {};
}

bool isAdskUsdComponent(const std::string& proxyShapePath)
{
MString defineIsComponentCmd;
defineIsComponentCmd.format(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for now, but I think we should eventually refactor all those Python functions into a "helper" sub-module of usd_component_creator and avoid redefining and reparsing these functions all the time. (Merged with Anthon's)

"def usd_component_creator_is_proxy_shape_a_component():\n"
" from pxr import Sdf, Usd, UsdUtils\n"
" import mayaUsd\n"
" import mayaUsd.ufe\n"
" try:\n"
" from AdskUsdComponentCreator import ComponentDescription\n"
" except ImportError:\n"
" return -1\n"
" proxyStage = mayaUsd.ufe.getStage('^1s')\n"
" component_description = ComponentDescription.CreateFromStageMetadata(proxyStage)\n"
" if component_description:\n"
" return 1\n"
" else:\n"
" return 0",
proxyShapePath.c_str());

int isStageAComponent = 0;
MStatus success;
if (MS::kSuccess
== (success = MGlobal::executePythonCommand(defineIsComponentCmd, false, false))) {
MString runIsComponentCmd = "usd_component_creator_is_proxy_shape_a_component()";
success = MGlobal::executePythonCommand(runIsComponentCmd, isStageAComponent);
}

if (success != MS::kSuccess) {
TF_RUNTIME_ERROR(
"Error occurred when testing stage '%s' for component.", proxyShapePath.c_str());
}

return isStageAComponent == 1;
}

void saveAdskUsdComponent(const std::string& proxyPath)
{
MString saveComponent;
saveComponent.format(
"from pxr import Sdf, Usd, UsdUtils\n"
"import mayaUsd\n"
"import mayaUsd.ufe\n"
"from usd_component_creator_plugin import MayaComponentManager\n"
"proxyStage = mayaUsd.ufe.getStage('^1s')\n"
"MayaComponentManager.GetInstance().SaveComponent(proxyStage)",
proxyPath.c_str());

if (!MGlobal::executePythonCommand(saveComponent)) {
TF_RUNTIME_ERROR("Error while saving USD component '%s'", proxyPath.c_str());
}
}

std::string previewSaveAdskUsdComponent(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code moved from what Anton merged. I just changed double quotes to quotes in the inlined python

const std::string& saveLocation,
const std::string& componentName,
const std::string& proxyPath)
{
MString defMoveComponentPreviewCmd;
defMoveComponentPreviewCmd.format(
"def usd_component_creator_move_component_preview():\n"
" import json\n"
" from pxr import Sdf, Usd, UsdUtils\n"
" import mayaUsd\n"
" import mayaUsd.ufe\n"
" try:\n"
" from AdskUsdComponentCreator import ComponentDescription, "
"PreviewMoveComponentHierarchy\n"
" except ImportError:\n"
" return None\n"
" proxyStage = mayaUsd.ufe.getStage('^1s')\n"
" component_description = "
" ComponentDescription.CreateFromStageMetadata(proxyStage)\n"
" if component_description:\n"
" move_comp_preview = PreviewMoveComponentHierarchy(component_description, '^2s', "
"'^3s')\n"
" return json.dumps(move_comp_preview)\n"
" else:\n"
" return \"\"",
proxyPath.c_str(),
saveLocation.c_str(),
componentName.c_str());

if (MS::kSuccess == MGlobal::executePythonCommand(defMoveComponentPreviewCmd)) {
MString result;
MString runComponentMovePreviewCmd = "usd_component_creator_move_component_preview()";
if (MS::kSuccess == MGlobal::executePythonCommand(runComponentMovePreviewCmd, result)) {
return result.asChar();
}
}
return {};
}

} // namespace ComponentUtils
} // namespace MAYAUSD_NS_DEF
62 changes: 62 additions & 0 deletions lib/mayaUsd/utils/utilComponentCreator.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// Copyright 2025 Autodesk
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#ifndef MAYAUSD_UTILS_UTILCOMPONENTCREATOR_H
#define MAYAUSD_UTILS_UTILCOMPONENTCREATOR_H

#include "mayaUsd/mayaUsd.h"

#include <mayaUsd/base/api.h>

#include <string>
#include <vector>

namespace MAYAUSD_NS_DEF {
namespace ComponentUtils {

/*! \brief Returns whether the proxy shape at the given path identifies an Autodesk USD Component.
*/
MAYAUSD_CORE_PUBLIC
bool isAdskUsdComponent(const std::string& proxyPath);

/*! \brief Returns the ids of the USD layers that should be saved for the Autodesk USD Component.
*
* \note Expects \p proxyPath to be a valid component path.
*/
MAYAUSD_CORE_PUBLIC
std::vector<std::string> getAdskUsdComponentLayersToSave(const std::string& proxyPath);

/*! \brief Saves the Autodesk USD Component identified by \p proxyPath.
*
* \note Expects \p proxyPath to be a valid component path.
*/
MAYAUSD_CORE_PUBLIC
void saveAdskUsdComponent(const std::string& proxyPath);

/*! \brief Previews the structure of the Autodesk USD Component identified by \p proxyPath,
* when saved at the given location with the given name.
* \return Returns the expected component hierarchy, formatted in json.
*/
MAYAUSD_CORE_PUBLIC
std::string previewSaveAdskUsdComponent(
const std::string& saveLocation,
const std::string& componentName,
const std::string& proxyPath);

} // namespace ComponentUtils
} // namespace MAYAUSD_NS_DEF

#endif // MAYAUSD_UTILS_UTILCOMPONENTCREATOR_H
17 changes: 17 additions & 0 deletions lib/mayaUsd/utils/utilSerialization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
#include <pxr/usd/sdf/usdaFileFormat.h>
#include <pxr/usd/sdf/usdcFileFormat.h>
#endif
#include "utilComponentCreator.h"

#include <pxr/usd/usdGeom/tokens.h>

#include <maya/MGlobal.h>
Expand Down Expand Up @@ -621,6 +623,21 @@ void getLayersToSaveFromProxy(const std::string& proxyPath, StageLayersToSave& l
return;
}

// Special case for components created by the component creator. Non-local layers,
// non-active layers, and non-dirty but to be renamed layers, can be impacted when saving a
// component. Only the component creator knows how to save a component properly.
if (ComponentUtils::isAdskUsdComponent(proxyPath)) {
const auto layerIds = ComponentUtils::getAdskUsdComponentLayersToSave(proxyPath);
for (const auto& ccLayerId : layerIds) {
auto ccLayer = pxr::SdfLayer::FindOrOpen(ccLayerId);
if (!ccLayer || ccLayer->IsAnonymous()) {
continue;
}
layersInfo._dirtyFileBackedLayers.push_back(ccLayer);
}
return;
}

auto root = stage->GetRootLayer();
populateChildren(
proxyPath, stage, root, nullptr, layersInfo._anonLayers, layersInfo._dirtyFileBackedLayers);
Expand Down
97 changes: 35 additions & 62 deletions lib/usd/ui/layerEditor/componentSaveDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
#include "generatedIconButton.h"
#include "qtUtils.h"

#include <mayaUsd/utils/utilComponentCreator.h>

#include <maya/MGlobal.h>
#include <maya/MString.h>

Expand Down Expand Up @@ -378,70 +380,41 @@ void ComponentSaveDialog::updateTreeView()
std::string saveLocation(_locationEdit->text().toStdString());
std::string componentName(_nameEdit->text().toStdString());

MString defMoveComponentPreviewCmd;
defMoveComponentPreviewCmd.format(
"def usd_component_creator_move_component_preview():\n"
" import json\n"
" from pxr import Sdf, Usd, UsdUtils\n"
" import mayaUsd\n"
" import mayaUsd.ufe\n"
" try:\n"
" from AdskUsdComponentCreator import ComponentDescription, "
"PreviewMoveComponentHierarchy\n"
" except ImportError:\n"
" return None\n"
" proxyStage = mayaUsd.ufe.getStage(\"^1s\")\n"
" component_description = "
" ComponentDescription.CreateFromStageMetadata(proxyStage)\n"
" if component_description:\n"
" move_comp_preview = PreviewMoveComponentHierarchy(component_description, \"^2s\", "
"\"^3s\")\n"
" return json.dumps(move_comp_preview)\n"
" else:\n"
" return \"\"",
_proxyShapePath.c_str(),
saveLocation.c_str(),
componentName.c_str());

if (MS::kSuccess == MGlobal::executePythonCommand(defMoveComponentPreviewCmd)) {
MString result;
MString runComponentMovePreviewCmd = "usd_component_creator_move_component_preview()";
if (MS::kSuccess == MGlobal::executePythonCommand(runComponentMovePreviewCmd, result)) {
QString jsonStr = QString::fromStdWString(result.asWChar());

// Clear existing tree first
_treeWidget->clear();

QStackedWidget* stackedWidget
= qobject_cast<QStackedWidget*>(_treeScrollArea->widget());

QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError);
QJsonObject jsonObj;
bool hasData = false;

if (parseError.error == QJsonParseError::NoError && doc.isObject() && !doc.isEmpty()) {
jsonObj = doc.object();
hasData = !jsonObj.isEmpty();
}

if (hasData) {
// Populate tree view with JSON data and show tree
populateTreeView(jsonObj);
if (stackedWidget) {
stackedWidget->setCurrentIndex(0);
}
} else {
// Show "Nothing to preview" message
if (stackedWidget) {
stackedWidget->setCurrentIndex(1);
}
}

// Update last component name
_lastComponentName = _nameEdit->text();
const auto result = MayaUsd::ComponentUtils::previewSaveAdskUsdComponent(
saveLocation, componentName, _proxyShapePath.c_str());

QString jsonStr = QString::fromStdString(result);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes starting here.


// Clear existing tree first
_treeWidget->clear();

QStackedWidget* stackedWidget = qobject_cast<QStackedWidget*>(_treeScrollArea->widget());

QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError);
QJsonObject jsonObj;
bool hasData = false;

if (parseError.error == QJsonParseError::NoError && doc.isObject() && !doc.isEmpty()) {
jsonObj = doc.object();
hasData = !jsonObj.isEmpty();
}

if (hasData) {
// Populate tree view with JSON data and show tree
populateTreeView(jsonObj);
if (stackedWidget) {
stackedWidget->setCurrentIndex(0);
}
} else {
// Show "Nothing to preview" message
if (stackedWidget) {
stackedWidget->setCurrentIndex(1);
}
}

// Update last component name
_lastComponentName = _nameEdit->text();
}

void ComponentSaveDialog::onShowMore()
Expand Down
Loading