How to add parameter groups to Nodes dynamically?
-
dynamic_node_impl.cpp from the example.nodes project shows how to add simple int/string/color parameters (input ports) to a Node dynamically.
maxon::Id portId; portId.Init(part) iferr_return; // Add port for part. maxon::nodes::MutablePort port = root.GetInputs().AddPort(portId) iferr_return; // Make a dependency connection from input to output. This tells the system which // ports are needed for the shader evaluation of the output port. port.Connect(output, maxon::Wires(maxon::Wires::DEPENDENCY)) iferr_return; if (part[0] == 'i') { // Create a port of type Int. port.SetType<maxon::Int>() iferr_return; // Directly set the name. port.SetValue(maxon::NODE::BASE::NAME, part) iferr_return; } ...
Is it possible to add UI groups as well and set them as parent groups of these ports?
-
Hey @peter_horvath,
just for verboseness, you are talking about a parameter group such as the "Base" group shown below on the Redshift Standard Material node?
- You are not talking about "tabs", e.g., "Outputs", which are technically also groups, right?
- Does the group you want to reference already exist?
On the data description level groups are referenced as
IntenernedId
's, specifically overmaxon::DESCRIPTION::UI::BASE::GROUPID
. Groups are also not part of the data model you are operating there with, you cannot find a node for a group in a graph (I am sure you are aware, just being verbose).I had quick look at our code base and:
- I do not see any alias for
DESCRIPTION::UI::BASE::GROUPID
on the node side of things. - When we deal with this internally, we query the data description database for a given node to read/modify the description. This will not work for what you want to do here (at least I think so).
Sometimes you can just blindly set an attribute on a node and the presenter part picks then up on that (presenter as in MVP). You can 'hack' the folding state of a port bundle like this for example. But I do not see anything similar in place here. The core problem is that you are limited to the model domain of MVP in the public API.
Will ask the node team if there is a way.
Cheers,
Ferdinand -
Hey Ferdinand,
Thank you for your prompt answer. Yes, I'm talking about parameter groups, like the Base or Reflection group in the Redshift Material and the groups do not exist yet. The goal here is to create these parameter groups and the parameters within them dynamically based on an integer input that defines the number of groups.
Thanks,
Peter -
Hey @peter_horvath,
so, I had a look at your problem, and while I came to a solution there are still some question marks for me.
First of all, modifying the
DataDescription
's for the node template viaDataDescriptionDatabaseInterface::LoadDescription
did not work for me. That is the workflow I saw in our codebase and talked about in my last posting. Loading the description will simply fail for me, @m_adam claims that this should work and that he does it all the time but, yeah, for me it simply does not work. Maybe you have to do the magic thing "x" for this to work, I don't know.The route I took, is
DataDescriptionDefintion
which is sort of an extra layer around data descriptions. There is also a database for these, but other than for the descriptions, we can just pin this data to the root node and something in the backend picks then up on that. Creating the groups and using them is then only a relatively trivial task, I added the functionAddGroupDescription
for that. There some minor bits and bobs, but using this function then looks like this:// (FH): The three relevant groups, the inputs group, and our two custom groups, forming the hierarchy: // inputs -> foo -> bar. static const maxon::InternedId inputsGrpId = maxon::NODE::BASE::GROUP_INPUTS; static const maxon::InternedId fooGrpId = maxon::InternedId::Create( "net.maxonexample.class.dynamicnode.group.foo") iferr_return; static const maxon::InternedId barGrpId = maxon::InternedId::Create( "net.maxonexample.class.dynamicnode.group.bar") iferr_return; // (FH): Add group descriptions for the custom groups. maxon::DataDescriptionDefinition uiDefintions, stringDefinitions; AddGroupDescription(uiDefintions, stringDefinitions, fooGrpId, inputsGrpId, "foo"_s) iferr_return; AddGroupDescription(uiDefintions, stringDefinitions, barGrpId, fooGrpId, "bar"_s) iferr_return; // (FH): For the string definitions we have to do some additional work, as strings in nodes/assets are stored in // language specific containers. We are writing the English string here which is the default/fallback language. // With maxon::Resource you have more control about languages to pick. maxon::LazyLanguageStringDataDescriptionDefinition languageDict = root.GetValue< maxon::LazyLanguageStringDataDescriptionDefinition>( maxon::nodes::NodeDescriptionStringLazy).GetValueOrDefault() iferr_return; languageDict.Set(maxon::LANGUAGE_ENGLISH_ID, stringDefinitions) iferr_return; // (FH): Set our group descriptions on the root node of the graph. root.SetValue(maxon::nodes::NodeDescriptionUi, uiDefintions) iferr_return; root.SetValue(maxon::nodes::NodeDescriptionStringLazy, languageDict) iferr_return; // ... // (FH): Make use on some dynamically allocated port of our new group ID. maxon::DataDictionary ui; ui.Set(maxon::DESCRIPTION::UI::BASE::GROUPID, barGrpId) iferr_return;
This somewhat works, but has the problem that it rearranges the groups (the inputs group is now in front of the the preview group). I have not started debugging this, by for example moving my code more towards the end of
InstantiateImpl
(although it seems a bit unlikely that this would help). Internally we use similar code, but I had to rip out some internal stuff to make it work for public API users. But this should be doable.I also asked Ole, one of the developers of all this, who showed me a third way, but then told me that my approach would be better. I would simply ask you to try a little bit around with the code we gave you, and when you are really stuck to come back. We are here very much in the depths of Nodes API, and I have to limit a bit the amount of support I spend on such a niche topic.
Cheers,
FerdinandMy full code, my modfications are prefixed with (FH):
#include "maxon/datadescription_ui.h" #include "maxon/datadescription_string.h" #include "maxon/datadescription_nodes.h" #include "maxon/lazylanguagestringdatadescription.h" #include "space/customnode-customnodespace_descriptions.h" #include "nodesystemclass_impl.h" #include "maxon/stringresource.h" #include "maxon/nodetemplate.h" #include "maxon/datadescription_data.h" #include "maxon/datadescriptiondatabase.h" namespace maxonsdk { /// (FH): Adds a group description to the passed data description definition. /// @param[in, out] ui The ui data description definition to add the group to. /// @param[in, out] strings The string data description definition to add the group to. /// @param[in] groupId The identifier of the group. /// @param[in] parentId The identifier of the parent group. /// @param[in] label The label of the group. static maxon::Result<void> AddGroupDescription( maxon::DataDescriptionDefinition& ui, maxon::DataDescriptionDefinition& strings, const maxon::InternedId& groupId, const maxon::InternedId& parentId, const String& label) { iferr_scope; maxon::DataDictionary uiData; uiData.Set(maxon::DESCRIPTION::BASE::IDENTIFIER, groupId) iferr_return; uiData.Set(maxon::DESCRIPTION::BASE::COMMAND, maxon::DESCRIPTION::BASE::COMMAND.ENUM_GROUP) iferr_return; uiData.Set(maxon::DESCRIPTION::UI::BASE::SHOWGROUPINPORTLIST, false) iferr_return; uiData.Set(maxon::DESCRIPTION::UI::BASE::GROUPDEFAULTOPEN, true) iferr_return; uiData.Set(maxon::DESCRIPTION::UI::BASE::DEFAULTCLASSIFICATION, maxon::DESCRIPTION::UI::BASE::DEFAULTCLASSIFICATION.ENUM_INPUT) iferr_return; if (parentId.IsPopulated()) { uiData.Set(maxon::DESCRIPTION::UI::BASE::GROUPID, parentId) iferr_return; } ui.AddEntry(uiData) iferr_return; maxon::DataDictionary stringData; stringData.Set(maxon::DESCRIPTION::BASE::IDENTIFIER, groupId) iferr_return; stringData.Set(maxon::DESCRIPTION::BASE::COMMAND, maxon::DESCRIPTION::BASE::COMMAND.ENUM_GROUP) iferr_return; stringData.Set(maxon::DESCRIPTION::STRING::BASE::TRANSLATEDSTRING, label) iferr_return; strings.AddEntry(stringData) iferr_return; return maxon::OK; } // This example implements a node with a template parameter port. // The user can drive that port with a String argument which is parsed as a comma-separated list of names. // For each name a port is created dynamically. If the name starts with 'i' its type is Int, // for 's' the type is String, otherwise Float. class DynamicNode : public maxon::Component<DynamicNode, maxon::nodes::NodeTemplateInterface> { MAXON_COMPONENT(NORMAL, maxon::nodes::NodeTemplateBaseClass); public: MAXON_METHOD maxon::Result<maxon::Bool> SupportsImpl(const maxon::nodes::NodeSystemClass& cls) const { // Make sure that this node only shows up in the example node space. return cls.GetClass() == NodeSystemClassExample::GetClass(); } MAXON_METHOD maxon::Result<maxon::nodes::NodeSystem> InstantiateImpl( const maxon::nodes::InstantiationTrace& parent, const maxon::nodes::TemplateArguments& args) const { iferr_scope; maxon::nodes::MutableRoot root = parent.CreateNodeSystem() iferr_return; // (FH): The three relevant groups, the inputs group, and our two custom groups, forming the hierarchy: // inputs -> foo -> bar. static const maxon::InternedId inputsGrpId = maxon::NODE::BASE::GROUP_INPUTS; static const maxon::InternedId fooGrpId = maxon::InternedId::Create( "net.maxonexample.class.dynamicnode.group.foo") iferr_return; static const maxon::InternedId barGrpId = maxon::InternedId::Create( "net.maxonexample.class.dynamicnode.group.bar") iferr_return; // (FH): Add group descriptions for the custom groups. maxon::DataDescriptionDefinition uiDefintions, stringDefinitions; AddGroupDescription(uiDefintions, stringDefinitions, fooGrpId, inputsGrpId, "foo"_s) iferr_return; AddGroupDescription(uiDefintions, stringDefinitions, barGrpId, fooGrpId, "bar"_s) iferr_return; // (FH): For the string definitions we have to do some additional work, as strings in nodes/assets are stored in // language specific containers. We are writing the English string here which is the default/fallback language. // With maxon::Resource you have more control about languages to pick. maxon::LazyLanguageStringDataDescriptionDefinition languageDict = root.GetValue< maxon::LazyLanguageStringDataDescriptionDefinition>( maxon::nodes::NodeDescriptionStringLazy).GetValueOrDefault() iferr_return; languageDict.Set(maxon::LANGUAGE_ENGLISH_ID, stringDefinitions) iferr_return; // (FH): Set our group descriptions on the root node of the graph. root.SetValue(maxon::nodes::NodeDescriptionUi, uiDefintions) iferr_return; root.SetValue(maxon::nodes::NodeDescriptionStringLazy, languageDict) iferr_return; // Create an output port. maxon::nodes::MutablePort output = root.GetOutputs().AddPort(maxonexample::NODE::DYNAMICNODE::RESULT) iferr_return; output.SetType<maxon::Color>() iferr_return; // Mark the output port as dynamic. This means that its value is computed by shader evaluation. output.SetValue(maxon::nodes::Dynamic, true) iferr_return; // Add the template parameter port. maxon::nodes::MutablePort codePort = root.GetInputs().AddPort(maxonexample::NODE::DYNAMICNODE::CODE) iferr_return; codePort.SetType<maxon::String>() iferr_return; codePort.SetValue(maxon::nodes::TemplateParameter, true) iferr_return; // Get the template argument for the code. const String& code = args.GetValueArgument<String>(codePort).GetValueOrDefault(); Int order = 0; // Parse code, here we just split the code into its comma-separated parts and create a port for each part. code.Split(","_s, true, [&root, &order, &output] (const maxon::String& part) -> maxon::Result<maxon::Bool> { iferr_scope; if (part.IsPopulated()) { maxon::Id portId; portId.Init(part) iferr_return; // Add port for part. maxon::nodes::MutablePort port = root.GetInputs().AddPort(portId) iferr_return; // Make a dependency connection from input to output. This tells the system which // ports are needed for the shader evaluation of the output port. port.Connect(output, maxon::Wires(maxon::Wires::DEPENDENCY)) iferr_return; if (part[0] == 'i') { // Create a port of type Int. port.SetType<maxon::Int>() iferr_return; // Directly set the name. port.SetValue(maxon::NODE::BASE::NAME, part) iferr_return; } else if (part[0] == 's') { // Create a port of type String. port.SetType<maxon::String>() iferr_return; port.SetValue(maxon::NODE::BASE::NAME, part) iferr_return; } else if (part[0] == 'c') { // Create a port of type Color. Also make it expect a constant. port.SetType<maxon::Color>() iferr_return; port.SetValue(maxon::NODE::BASE::NAME, part) iferr_return; port.SetValue(maxon::nodes::ConstantParameter, true) iferr_return; } else { // Default case: Use Float. port.SetType<maxon::Float>() iferr_return; // This shows how to set up UI properties dynamically, in this case we configure a slider. maxon::DataDictionary data; data.Set(maxon::DESCRIPTION::DATA::BASE::DATATYPE, maxon::GetDataType<maxon::AFloat>().GetId()) iferr_return; port.SetValue(maxon::nodes::PortDescriptionData, std::move(data)) iferr_return; maxon::DataDictionary ui; // (FH): Make use of our new group ID. ui.Set(maxon::DESCRIPTION::UI::BASE::GROUPID, barGrpId) iferr_return; ui.Set(maxon::DESCRIPTION::UI::BASE::GUITYPEID, maxon::Id("net.maxon.ui.number")) iferr_return; ui.Set(maxon::DESCRIPTION::UI::NET::MAXON::UI::NUMBER::SLIDER, true) iferr_return; ui.Set(maxon::DESCRIPTION::UI::BASE::ADDMINMAX::LIMITVALUE, true) iferr_return; ui.Set(maxon::DESCRIPTION::UI::BASE::ADDMINMAX::MINVALUE, maxon::Data(maxon::Float(0.0_f))) iferr_return; ui.Set(maxon::DESCRIPTION::UI::BASE::ADDMINMAX::MAXVALUE, maxon::Data(maxon::Float(10.0_f))) iferr_return; ui.Set(maxon::DESCRIPTION::UI::BASE::ADDMINMAX::STEPVALUE, maxon::Data(maxon::Float(0.1_f))) iferr_return; port.SetValue(maxon::nodes::PortDescriptionUi, std::move(ui)) iferr_return; // This shows how to use localization for dynamically created ports. maxon::LazyLanguageDictionary languageDict; maxon::DataDictionary english; english.Set(maxon::DESCRIPTION::STRING::BASE::TRANSLATEDSTRING, "Localized port name for "_s + part) iferr_return; languageDict.Set(maxon::LANGUAGE_ENGLISH_ID, english) iferr_return; port.SetValue(maxon::nodes::PortDescriptionStringLazy, std::move(languageDict)) iferr_return; } port.SetValue(maxon::nodes::ReqOrderIndex, ++order) iferr_return; } return true; }) iferr_return; root.SetTemplate(self, args) iferr_return; return root.EndModification(); } }; MAXON_COMPONENT_CLASS_REGISTER(DynamicNode, "net.maxonexample.class.dynamicnode"); MAXON_DECLARATION_REGISTER(maxon::nodes::BuiltinNodes, maxonexample::NODE::DYNAMICNODE::GetId()) { return DynamicNode::GetClass().Create().SetId({objectId, maxon::Id()}, 0, maxon::AssetInterface::GetBuiltinRepository()); } } // namespace maxonsdk
Ole's code which is yet another approach than DataDescription or DataDescriptionDefinition:
const maxon::CString& groupId = "net.maxonexample.mydynamicgroup"_cs; maxon::InternedId iid; iid.Init(groupId) iferr_return; ui.Set(maxon::DESCRIPTION::UI::BASE::GROUPID, iid) iferr_return; iid.Init(FormatCString("[@]@", groupId, maxon::DESCRIPTION::UI::BASE::GROUPTITLEBAR.GetId())) iferr_return; ui.Set(iid, true) iferr_return; iid.Init(FormatCString("[@]@", groupId, maxon::DESCRIPTION::UI::BASE::SHOWGROUPINPORTLIST.GetId())) iferr_return; ui.Set(iid, true) iferr_return; iid.Init(FormatCString("[@]@", groupId, maxon::DESCRIPTION::UI::BASE::GROUPDEFAULTOPEN.GetId())) iferr_return; ui.Set(iid, true) iferr_return; port.SetValue(maxon::nodes::PortDescriptionUi, std::move(ui)) iferr_return; // This shows how to use localization for dynamically created ports. maxon::LazyLanguageDictionary languageDict; maxon::DataDictionary english; english.Set(maxon::DESCRIPTION::STRING::BASE::TRANSLATEDSTRING, "Localized port name for "_s + part) iferr_return; iid.Init(FormatCString("[@]@", groupId, maxon::DESCRIPTION::STRING::BASE::TRANSLATEDSTRING.GetId())) iferr_return; english.Set(iid, "Dynamic Group"_s) iferr_return; languageDict.Set(maxon::LANGUAGE_ENGLISH_ID, english) iferr_return;
His comment was (translated):
"All float ports are assigned to a dynamically created group. The trick is that you set the group properties on the port itself, using the special id "[groupid]property". The group is thus (redundantly) defined on each port that belongs to it.
This was his result, but it looks like that his approach has a similar problem, as the dynamic group is also in front of everything (in his approach we do not explicitly define the hierarchy of the groups). I would suggest you to try out both approaches and see if you can make it work.
-
Thank you for taking time on this. Definitely promising. I'll give it a spin to see if I can reach the desired results with any of the mentioned approaches.