Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 73 additions & 36 deletions source/MaterialXGraphEditor/Graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -697,10 +697,7 @@ void Graph::updateMaterials(mx::InputPtr input /* = nullptr */, mx::ValuePtr val
{
if (!input)
{
mx::ElementPtr elem = nullptr;
{
elem = _graphDoc->getDescendant(renderablePath);
}
const mx::ElementPtr elem = _graphDoc->getDescendant(renderablePath);
mx::TypedElementPtr typedElem = elem ? elem->asA<mx::TypedElement>() : nullptr;
_renderer->updateMaterials(typedElem);
}
Expand Down Expand Up @@ -1009,6 +1006,8 @@ void Graph::showPropertyEditorValue(UiNodePtr node, mx::InputPtr input, const mx
nodeInput->setValueString(temp);
nodeInput->setValue(temp, nodeInput->getType());
updateMaterials();

_currUiNode->buildUiTokenMap(); // Re-build token map
}
}
}
Expand Down Expand Up @@ -1062,16 +1061,16 @@ void Graph::setUiNodeInfo(UiNodePtr node, const std::string& type, const std::st
}
else
{
if (node->getNode())
if (mx::ConstNodePtr mxNode = node->getNode())
{
mx::NodeDefPtr nodeDef = node->getNode()->getNodeDef(node->getNode()->getName());
mx::NodeDefPtr nodeDef = mxNode->getNodeDef(mxNode->getName());
if (nodeDef)
{
for (mx::InputPtr input : nodeDef->getActiveInputs())
{
if (node->getNode()->getInput(input->getName()))
if (mxNode->getInput(input->getName()))
{
input = node->getNode()->getInput(input->getName());
input = mxNode->getInput(input->getName());
}
UiPinPtr inPin = std::make_shared<UiPin>(_state.nextUiId, node, ax::NodeEditor::PinKind::Input, input);
node->getInputPins().push_back(inPin);
Expand All @@ -1081,16 +1080,18 @@ void Graph::setUiNodeInfo(UiNodePtr node, const std::string& type, const std::st

for (mx::OutputPtr output : nodeDef->getActiveOutputs())
{
if (node->getNode()->getOutput(output->getName()))
if (mxNode->getOutput(output->getName()))
{
output = node->getNode()->getOutput(output->getName());
output = mxNode->getOutput(output->getName());
}
UiPinPtr outPin = std::make_shared<UiPin>(_state.nextUiId, node, ax::NodeEditor::PinKind::Output, output);
node->getOutputPins().push_back(outPin);
_state.pins.push_back(outPin);
++_state.nextUiId;
}
}

node->buildUiTokenMap(); // Build initial token map
}
else if (node->getInput())
{
Expand Down Expand Up @@ -3662,41 +3663,63 @@ void Graph::propertyEditor()

showPropertyEditorOutputConnections(_currUiNode);;
}

// Find tokens within currUiNode
mx::ConstNodePtr node = _currUiNode->getNode();
if (node != nullptr)

// Draw token table
if (const auto& currTokenMap = _currUiNode->getUiTokenMap(); !currTokenMap.empty())
{
mx::StringResolverPtr resolver = node->createStringResolver();
const mx::StringMap& tokens = resolver->getFilenameSubstitutions();
ImGui::Text("Tokens");
ImGui::SameLine();
drawHelpMarker("All tokens that are within scope of the selected node. Token values will be string-substituted into listed 'Affected Inputs'.");

int tokenCount = static_cast<int>(currTokenMap.size() + 1u); // Add 1 to account for header row
ImVec2 tableHeight(0.0f, TEXT_BASE_HEIGHT * std::min(SCROLL_LINE_COUNT, tokenCount));

if (!tokens.empty())
// Use `ImGuiTableFlags_SizingFixedFit` to set default column width to fit content
if (ImGui::BeginTable("tokens_node_table", 4, tableFlags | ImGuiTableFlags_SizingFixedFit, tableHeight))
{
ImGui::Text("Tokens");

ImVec2 tableSize(0.0f, TEXT_BASE_HEIGHT * std::min(SCROLL_LINE_COUNT, static_cast<int>(tokens.size())));
bool haveTable = ImGui::BeginTable("tokens_node_table", 2, tableFlags, tableSize);
if (haveTable)
ImGui::SetWindowFontScale(_fontScale);

ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Value");
ImGui::TableSetupColumn("Source Element");
ImGui::TableSetupColumn("Affected Inputs");

// Set tooltips for each of table's columns
constexpr std::array tableHeadersTooltips = { "", "Press <enter> to set token value.", "The graph element where the token is declared.", "Node inputs which reference the token." };
drawTableHeadersRowWithTooltips(tableHeadersTooltips);

for (const auto& [tokenName, tokenPtr] : currTokenMap)
{
ImGui::SetWindowFontScale(_fontScale);
ImGui::TableNextRow(); // Start new row
ImGui::PushID(&tokenName);

for (const auto& [token, value] : tokens)
{

ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::PushID(&token);
// Name
ImGui::TableNextColumn();
ImGui::Text("%s", tokenName.c_str());

ImGui::Text("%s", token.c_str());
ImGui::TableNextColumn();
ImGui::Text("%s", value.c_str());
// Value
ImGui::TableNextColumn();
std::string tokenValue = tokenPtr->getValue();

ImGui::PopID();
if (ImGui::InputText("##token_value", &tokenValue, ImGuiInputTextFlags_EnterReturnsTrue))
{
tokenPtr->setValue(tokenValue); // Write out new token value
updateMaterials(); // Trigger update of material
}

ImGui::EndTable();
ImGui::SetWindowFontScale(1.0f);

// Source Element
ImGui::TableNextColumn();
ImGui::Text("%s", tokenPtr->getSourceElementString().c_str());

// Affected Inputs
ImGui::TableNextColumn();
ImGui::Text("%s", tokenPtr->getAffectedInputsString().c_str());

ImGui::PopID();
}

ImGui::EndTable();
ImGui::SetWindowFontScale(1.0f); // Restore font scale
}
}

Expand Down Expand Up @@ -3760,6 +3783,20 @@ void Graph::showHelp() const
ImGui::BulletText("\"Node Info\" Will toggle showing node information.");
}
}
void Graph::drawHelpMarker(const char* content)
{
constexpr float WRAP_POSITION = 32.f; // Compile-time definition of text-wrap position

ImGui::TextDisabled(HELP_MARKER_TEXT); // Draw help marker
if (!ImGui::IsItemHovered())
return; // If help marker isn't hovered return early

ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * WRAP_POSITION);
ImGui::TextUnformatted(content);
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}

void Graph::addNodePopup(bool cursor)
{
Expand Down
30 changes: 29 additions & 1 deletion source/MaterialXGraphEditor/Graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,35 @@ class Graph

void showHelp() const;

// A compile-time constant member variable that corresponds to the function below. Defined in header as visibility is desirable here.
static constexpr char HELP_MARKER_TEXT[] = "(?)";
// Static helper function to draw a marker via ImGui which shows a tooltip when hovered
static void drawHelpMarker(const char* content);

// Static helper function to display tooltips for headers in an ImGui table
template <std::size_t N> static void drawTableHeadersRowWithTooltips(const std::array<const char*, N>& tooltips)
{
const int columnCount = ImGui::TableGetColumnCount();
if (columnCount == 0 || columnCount != N)
return; // Given array size should match number of columns in table

ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
for (int col = 0; col < columnCount; ++col)
{
if (!ImGui::TableSetColumnIndex(col))
continue; // Do not draw if column is not visible
ImGui::TableHeader(ImGui::TableGetColumnName(col)); // Header name

std::string colTooltip = tooltips[col];
if (!colTooltip.empty() && ImGui::IsItemHovered())
{
ImGui::BeginTooltip();
ImGui::TextUnformatted(tooltips[col]);
ImGui::EndTooltip();
}
}
}

private:
mx::StringVec _geomFilter;
mx::StringVec _mtlxFilter;
Expand Down Expand Up @@ -357,5 +386,4 @@ class Graph
// Options
bool _saveNodePositions;
};

#endif
59 changes: 59 additions & 0 deletions source/MaterialXGraphEditor/UiNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,65 @@ mx::NodeGraphPtr UiNode::getNodeGraph() const
return _element ? _element->asA<mx::NodeGraph>() : nullptr;
}

void UiNode::buildUiTokenMap()
{
_uiTokenMap.clear(); // Assume we want clean slate

mx::ElementPtr currElem = getNode();
while (currElem)
{
if (mx::ConstInterfaceElementPtr interfaceElem = currElem->asA<mx::InterfaceElement>())
{
UiToken::applyTokenMapping(&_uiTokenMap, interfaceElem, currElem);

// If the node is a nodegraph, check for tokens on corresponding nodedef
if (mx::ConstNodeGraphPtr nodegraph = currElem->asA<mx::NodeGraph>())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe this logic will need to also handle node instances as the corresponding nodedef could also have exposed tokens. I don't think there is an example such as nodedef so may need to make one. Perhaps a simple example is a nodedef implemented as a image node + a token node, where the image node's filename and the token input are exposed as interface inputs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure! Do these new additions cover the cases you are thinking of?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good. Thanks.

{
if (mx::NodeDefPtr nodedef = nodegraph->getNodeDef())
UiToken::applyTokenMapping(&_uiTokenMap, nodedef, nodedef);
}

// If the node is a custom node instance, check for tokens on corresponding nodedef
if (mx::NodePtr node = currElem->asA<mx::Node>())
{
if (mx::NodeDefPtr nodedef = node->getNodeDef())
UiToken::applyTokenMapping(&_uiTokenMap, nodedef, nodedef);
}
}
currElem = currElem->getParent();
}

// Traverse through inputs and determine which tokens their value depends on
for (const auto& input : getNode()->getActiveInputs())
{
if (input->getType() != "filename")
continue;

mx::StringResolverPtr inputResolver = input->createStringResolver();
const mx::StringMap& inputTokens = inputResolver->getFilenameSubstitutions();

mx::StringMap inputTokensRenormalized;
for (const auto& entry : inputTokens)
{
// Store tokens without excess delimiters
inputTokensRenormalized[entry.first] = entry.first.substr(1, entry.first.size() - 2);
}

std::string inputValue = input->getValueString();
if (inputValue.empty() && input->hasInterfaceName())
inputValue = input->getInterfaceInput()->getValueString(); // Get value from referenced interface

for (const auto& entry : inputTokens)
{
if (inputValue.find(entry.first) != std::string::npos)
{
// Append to affected inputs of corresponding entry in token map
_uiTokenMap[inputTokensRenormalized[entry.first]]->addAffectedInput(input);
}
}
}
}

// return the uiNode connected with input name
UiNodePtr UiNode::getConnectedNode(const std::string& name)
{
Expand Down
82 changes: 82 additions & 0 deletions source/MaterialXGraphEditor/UiNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@

#include <imgui_node_editor.h>

#include <atomic>
#include <sstream>

namespace mx = MaterialX;
namespace ed = ax::NodeEditor;

class UiNode;
class UiPin;
class UiToken;

using UiNodePtr = std::shared_ptr<UiNode>;
using UiPinPtr = std::shared_ptr<UiPin>;
using UiTokenPtr = std::shared_ptr<UiToken>;

// An edge between two UiNodes, storing the two nodes and connecting input.
class UiEdge
Expand Down Expand Up @@ -160,6 +165,76 @@ class UiPin
bool _connected;
};

class UiToken
{
public:
UiToken(const mx::TokenPtr& token, const mx::ElementPtr& elem) : _tokenPtr(token), _sourceElement(elem) { }

std::string getValue() const { return _tokenPtr->getValueString(); }
void setValue(const std::string& val) const
{
_tokenPtr->setValueString(val);
}

std::string getSourceElementString() const
{
std::string _sourceElementName = _sourceElement->getName();
return _sourceElementName.empty() ? "<DOCUMENT>" : _sourceElementName;
}

void addAffectedInput(const mx::InputPtr& input)
{
_affectedInputs.push_back(input);
_isAffectedInputsDirty.store(true);
}

std::string getAffectedInputsString()
{
if (_isAffectedInputsDirty.load())
buildAffectedInputsStream();
return _affectedInputsStream.str();
}

const std::vector<mx::InputPtr>& getAffectedInputs() const { return _affectedInputs; };

// Handle update of given map pointer by iterating through active tokens of an interface element
static void applyTokenMapping(std::unordered_map<std::string, UiTokenPtr>* uiTokenMapPtr, const mx::ConstInterfaceElementPtr& interfaceElem, mx::ElementPtr sourceElem)
{
std::vector<mx::TokenPtr> tokens = interfaceElem->getActiveTokens();
for (auto token : tokens)
{
std::string key = token->getName();

// Insert into map, but do not allow parent values to override child values
uiTokenMapPtr->try_emplace(key, std::make_shared<UiToken>(token, sourceElem));
}
}

private:
const mx::TokenPtr _tokenPtr;
const mx::ElementPtr _sourceElement;

std::vector<mx::InputPtr> _affectedInputs{};
std::ostringstream _affectedInputsStream{};

// Track whether changes were made to inputs in order to re-build stream accordingly
std::atomic<bool> _isAffectedInputsDirty{ true };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps the introduction of std::atomic to this code is not needed? Assuming this code will only be called from the single-threaded UI of the Graph Editor, I wouldn't imagine we need to bring new thread-safe constructs into this logic.


void buildAffectedInputsStream()
{
if (!_isAffectedInputsDirty.load())
return;
_affectedInputsStream.clear();
for (size_t i = 0; i < _affectedInputs.size(); ++i)
{
_affectedInputsStream << _affectedInputs[i]->getName();
if (i < _affectedInputs.size() - 1)
_affectedInputsStream << ", ";
}
_isAffectedInputsDirty.store(false);
}
};

// The visual representation of a node in a graph.
class UiNode
{
Expand Down Expand Up @@ -248,6 +323,11 @@ class UiNode
std::vector<UiPinPtr>& getOutputPins() { return _outputPins; }
const std::vector<UiPinPtr>& getOutputPins() const { return _outputPins; }

const std::unordered_map<std::string, UiTokenPtr>& getUiTokenMap() const { return _uiTokenMap; }

// Build a map of relevant UI info for tokens in scope of this node. Should be called lazily (i.e. only when needed)
void buildUiTokenMap();

// Edge collection accessors
std::vector<UiEdge>& getEdges() { return _edges; }
const std::vector<UiEdge>& getEdges() const { return _edges; }
Expand Down Expand Up @@ -277,6 +357,8 @@ class UiNode
std::vector<UiPinPtr> _outputPins;
std::vector<UiEdge> _edges;

std::unordered_map<std::string, UiTokenPtr> _uiTokenMap;

bool _showAllInputs;
bool _showOutputsInEditor;
};
Expand Down
Loading