Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
019edba
Fix discovery of tokens to accumulate current tokens through traversa…
mialana May 14, 2026
cc15276
Merge branch 'main' into support-token-editing
mialana May 14, 2026
3709caf
Implement token data as persistent map on ui node class
mialana May 15, 2026
18b0081
Write out edited token value and store affected inputs per token
mialana May 20, 2026
ee565f7
Fix for material render preview immediate update and resolve value st…
mialana May 20, 2026
090bf5c
Merge branch 'main' into support-token-editing
mialana May 20, 2026
b526ec7
Clean up comments and allow for tooltips per column in tokens table
mialana May 20, 2026
7e81ef7
Create help marker for tokens table, fill in all column tooltips
mialana May 20, 2026
b3cee30
Lint, supplementary comment
mialana May 20, 2026
04f9d02
Match private member function naming convention
mialana May 20, 2026
8d0767b
Move token mapping logic to an inline lambda to avoid repeated code
mialana May 20, 2026
33fe3a7
Handle tokens defined on custom node instances
mialana May 20, 2026
3644bd6
Move inline lambda token mapping handler to static member of UiToken …
mialana May 23, 2026
0a6f9e5
Merge branch 'main' into support-token-editing
jstone-lucasfilm May 26, 2026
7e9717e
Clean up minor code practice
mialana May 27, 2026
74b237a
Merge branch 'main' into support-token-editing
jstone-lucasfilm May 28, 2026
67130cd
Remove std::atomic usage due to no concurrency issues
mialana May 30, 2026
3bc659e
Merge branch 'support-token-editing' of github.com:mialana/FORK-Mater…
mialana May 30, 2026
360fcb5
Merge branch 'main' into support-token-editing
mialana May 30, 2026
2f41f79
Merge branch 'main' into support-token-editing
mialana Jun 12, 2026
21b7828
Merge branch 'main' into support-token-editing
jstone-lucasfilm Jun 13, 2026
6bb8add
Clear string stream correctly, add protection check in building affec…
mialana Jun 15, 2026
e2e2126
Merge branch 'support-token-editing' of github.com:mialana/FORK-Mater…
mialana Jun 15, 2026
400f793
Merge branch 'main' into support-token-editing
mialana Jun 15, 2026
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
104 changes: 72 additions & 32 deletions source/MaterialXGraphEditor/Graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,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 +1064,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 +1083,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 +3666,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 +3786,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
66 changes: 66 additions & 0 deletions source/MaterialXGraphEditor/UiNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,72 @@ mx::NodeGraphPtr UiNode::getNodeGraph() const
return _element ? _element->asA<mx::NodeGraph>() : nullptr;
}

void UiNode::buildUiTokenMap()
{
// Helper inline lambda function to avoid repeating code for mapping of tokens declared on element itself vs on element's corresponding nodedef
auto handleTokenMapping = [&](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
Comment thread
jstone-lucasfilm marked this conversation as resolved.
Outdated
_uiTokenMap.try_emplace(key, std::make_shared<UiToken>(token, sourceElem));
}
};

_uiTokenMap.clear(); // Assume we want clean slate

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

// If the node is a nodegraph, also check for tokens on corresponding nodedef
if (mx::ConstNodeGraphPtr nodegraph = currElem->asA<mx::NodeGraph>())
Comment thread
jstone-lucasfilm marked this conversation as resolved.
{
if (mx::NodeDefPtr nodedef = nodegraph->getNodeDef())
{
handleTokenMapping(nodedef, nodedef);
}
}
}
currElem = currElem->getParent();
}

// Traverse through inputs and determine which tokens their value depends on
for (const auto& input : getNode()->getActiveInputs())

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.

Apologies, as I should have caught this in my earlier review, but I believe we need a null check for this call to getNode, as it's very possible that the current element is not a node.

Since you're storing the return value of getNode in the currElem variable above, you might consider storing this value as node and then returning early if its value is null, as there is effectively no further work to do in this case.

For example, this might look like the following:

void UiNode::buildUiTokenMap()
{
    _uiTokenMap.clear();

    mx::NodePtr node = getNode();
    if (!node)
        return;

    mx::ElementPtr currElem = node;
    while (currElem)
    {
        ...
    }

    for (const auto& input : node->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);
Comment thread
jstone-lucasfilm marked this conversation as resolved.
Outdated
}
}
}
}
// return the uiNode connected with input name
UiNodePtr UiNode::getConnectedNode(const std::string& name)
{
Expand Down
69 changes: 69 additions & 0 deletions source/MaterialXGraphEditor/UiNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

#include <imgui_node_editor.h>

#include <atomic>
#include <sstream>

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

Expand Down Expand Up @@ -160,6 +163,65 @@ 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; };

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 };
Comment thread
jstone-lucasfilm marked this conversation as resolved.
Outdated

void buildAffectedInputsStream()
{
if (!_isAffectedInputsDirty.load())
return;
_affectedInputsStream.clear();
Comment thread
jstone-lucasfilm marked this conversation as resolved.
Outdated
for (size_t i = 0; i < _affectedInputs.size(); ++i)
{
_affectedInputsStream << _affectedInputs[i]->getName();
if (i < _affectedInputs.size() - 1)
_affectedInputsStream << ", ";
}
_isAffectedInputsDirty.store(false);
}
};

using UiTokenPtr = std::shared_ptr<UiToken>;

// The visual representation of a node in a graph.
class UiNode
{
Expand Down Expand Up @@ -248,6 +310,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 +344,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