• TreeView for ObjectData plugin

    python r23
    2
    0 Votes
    2 Posts
    476 Views
    M
    Hi Mike this is unfortunately not possible, even in C++ for more info please look at Info for ITEMTREE/customgui_itemtree. Cheers, Maxime.
  • Purpose of different preference folders

    3
    0 Votes
    3 Posts
    886 Views
    A
    Hi @ferdinand, Thank you for the quick reply. This answers all my questions! Cheers, Arttu
  • 0 Votes
    4 Posts
    739 Views
    ferdinandF
    Hey @mogh, just to clarify: I found that bit of the documentation before you pointed it out, but in my mind I could not link "needs a document" to "the object has to be present" ... Well, the documentation clearly states it: doc (Optional[c4d.documents.BaseDocument]) – The document for the operation. Should be set if possible. Must be set for MCOMMAND_JOIN, MCOMMAND_MAKEEDITABLE, MCOMMAND_CURRENTSTATETOOBJECT and MCOMMAND_SPLINE_PROJECT. If you set the document, the objects which you pass to this function have to be in the same document. So pay attention that you use one send_modeling_command per document for objects. But that could be more visible. I'll see if I can rework the function documentation a bit to make that important information more prominent. Cheers, Ferdinand
  • This topic is deleted!

    1
    0 Votes
    1 Posts
    26 Views
    No one has replied
  • List All Nodes With No Filter?

    python 2023
    3
    0 Votes
    3 Posts
    639 Views
    B
    RE: Yes, GraphModelHelper.ListAllNodes requires at least one attribute to be set in the data dictionary. Gotcha. Thanks for the clarification. Anyway, the suggested alternative (i.e. write a separate iterator) still works as expected. Same as before.
  • Trying to programmatically create vertex color tag

    2
    0 Votes
    2 Posts
    695 Views
    ferdinandF
    Hello @zauhar, thank you for reaching out to us. Your code does look mostly fine and is working for me. The line op = Default makes little sense and you are of course missing things like imports, a context guard and a main function. Not all of them are absolutely necessary, but it is strongly advised to use them all, especially for novice users. Find a code example below. Vertex colors are also not automatically rendered as the diffuse color in Cinema 4D, one must use a VertexMap shader for that. Please consult the user manual or end-user support for end-user questions, we cannot deliver such support here. Cheers, Ferdinand The result for two polygon objects, the color of a vertex depends on its position in global space. [image: 1676539052471-0e9b3117-e70a-4495-8001-cbe0a6b0a2e4-image.png] Code """Creates vertex-color tag on the currently selected polygon object and makes it the active tag. Must be run as a Script Manager scrip with a polygon object selected. The color of each vertex will depend on its position in global space. """ import c4d import typing GRADIENT_MIN: float = -200.0 # The lower boundary of the gradient. GRADIENT_MAX: float = 200.0 # The upper boundary of the gradient. op: typing.Optional[c4d.BaseObject] # The active object, can be `None`. def main() -> None: """Runs the example. """ # Check that there is indeed a selected object and that it is a polygon object. if not isinstance(op, c4d.PolygonObject): raise TypeError("Please select a polygon object.") # Get its point count and allocate a vertex color tag with that count. count: int = op.GetPointCount() tag: c4d.VertexColorTag = c4d.VertexColorTag(count) if not isinstance(tag, c4d.VertexColorTag): raise MemoryError("Could not allocate tag.") # We are going to make it a little bit more exciting and write a gradient based on the global # coordinate y-component of a vertex. # Set the tag to defining colors only once per vertex instead of having N colors per vertex, # where N is the number of polygons attached to it. tag.SetPerPointMode(True) # Get the global matrix of the polygon object and all its points in global space. mg: c4d.Matrix = op.GetMg() globalPoints: list[c4d.Vector] = [mg * p for p in op.GetAllPoints()] # Define a callable with which we can get the gradient color of a point over its index. GetGradientColor = lambda i : c4d.Vector4d( c4d.utils.RangeMap(globalPoints[i].y, GRADIENT_MIN, GRADIENT_MAX, 0, 1, True), 0, 0, 1) # Get the data pointer of the tag and start writing data. dataW: object = tag.GetDataAddressW() for i in range(count): c4d.VertexColorTag.SetPoint(dataW, None, None, i, GetGradientColor(i)) # Insert the tag into the selected object, make it the active tag, and push an update event. op.InsertTag(tag) tag.SetBit(c4d.BIT_ACTIVE) c4d.EventAdd() if __name__ == "__main__": main()
  • Control whether a redshift shader node port is collapsed or not.

    2023 c++
    4
    2
    0 Votes
    4 Posts
    705 Views
    ferdinandF
    Hello @till-niese, No, that is not possible for public API users. As I tried to indicate before, you are operating here on the data model of the graph with GraphModelInterface. There is also a GraphModelPresenterInterface which is the presenter and provides some GUI logic as selecting nodes and wires, but that interface is non-public. The toggle state of a widget is buried deep in the presenter and not even exposed over the interface. There are also the non-public attributes about which I talked before. I went ahead and wrote some code trying to emulate access in the public API [1], but unsurprisingly, there is nothing to be found in the public graphs, the code will not print anything. It is simply not possible to do what you want to do here, and it is quite common for our APIs to not expose GUI functionalities. In the case of the Node API, the complexities of the MVP application model are added on top of that. There can be multiple presenters coordinating multiple views on a singular model. Or in less fancy: One can have multiple node editors opened on the same graph. The toggle state of a port bundle/group is shared between all editors at the moment. And aside from a principal stance of the feasibility of exposing GUIs, there might be substantial problems with exposing the toggle state despite the external perception that is just "this one thing"; I do not know for sure since I am not familiar with the implementation details here, but I would not be surprised. In the Asset API we had a similar case with people wanting to know "the selected asset" and our answer then being "that is an ambiguous question because there can be more than one Asset Browser". In the Asset API the solution is to open your own Asset Browser where you can then get that state. There is AFAIK currently no alternative solution as such for your problem, e.g., a node command which would toggle the folding state of a port. We could think about adding such command, but I doubt that I will find many supporters for this idea. Cheers, Ferdinand [1] // graph is a maxon::NodesGraphModelRef/GraphModelRef instance // rustTexNode is a RS Texture GraphNode in #graph. maxon::GraphNode root = graph.GetRoot(); maxon::GraphNode portBundle = rustTexNode.GetInputs().FindChild( maxon::Id("com.redshift3d.redshift4c4d.nodes.core.texturesampler.tex0")) iferr_return; // Iterate over candidates where this widget data is internally to be found, a graph root, a true // node, and a port (bundle). for (const maxon::GraphNode& node : { root, rustTexNode, portBundle }) { // Iterate over some attribute identifiers where GUI widget data is stored internally. for (const maxon::String& attr : { "widgetDataBlackBox"_s, "widgetDataBlackBoxSM"_s, "widgetDataBlackBoxOut"_s }) { maxon::InternedId attrId; attrId.Init("widgetDataBlackBoxOut") iferr_return; // Get the widget data stored at the maxon attribute #attr and iterate over its entries, the // data will always be empty/null. const maxon::DataDictionary attrData = node.GetValue<maxon::DataDictionary>( attrId).GetValueOrNull() iferr_return; for (const auto& entry : attrData) { const maxon::Data key = entry.first.GetCopy() iferr_return; const maxon::Data value = entry.second.GetCopy() iferr_return; ApplicationOutput("node: @, attr: @, key: @, value: @, type: @", node, attr, key, value, value.GetType()); } } }
  • How to Add Child Shaders to a Fusion Shader?

    python r20
    6
    1
    0 Votes
    6 Posts
    2k Views
    H
    Hello @ferdinand, when trying out your function to traverse shaders I noticed that for two materials in the scene it yields for the first material all the shaders of itself but also all the shaders of the second material. For second material it works like expected and yields only the shaders of itself. After fiddling around a bit with some other code of you from this post I think I ended up with a function that is yielding all the shaders from a material iteratively. Find the code below. Maybe this will help others. Cheers, Sebastian def iter_shaders(node): """Credit belongs to Ferdinand from the Plugincafe. I added only the part with the material and First Shader checking. Yields all descendants of ``node`` in a truly iterative fashion. The passed node itself is yielded as the first node and the node graph is being traversed in depth first fashion. This will not fail even on the most complex scenes due to truly hierarchical iteration. The lookup table to do this, is here solved with a dictionary which yields favorable look-up times in especially larger scenes but results in a more convoluted code. The look-up could also be solved with a list and then searching in the form ``if node in lookupTable`` in it, resulting in cleaner code but worse runtime metrics due to the difference in lookup times between list and dict collections. """ if not node: return # The lookup dictionary and a terminal node which is required due to the # fact that this is truly iterative, and we otherwise would leak into the # ancestors and siblings of the input node. The terminal node could be # set to a different node, for example ``node.GetUp()`` to also include # siblings of the passed in node. visisted = {} terminator = node while node: if isinstance(node, c4d.Material) and not node.GetFirstShader(): break if isinstance(node, c4d.Material) and node.GetFirstShader(): node = node.GetFirstShader() # C4DAtom is not natively hashable, i.e., cannot be stored as a key # in a dict, so we have to hash them by their unique id. node_uuid = node.FindUniqueID(c4d.MAXON_CREATOR_ID) if not node_uuid: raise RuntimeError("Could not retrieve UUID for {}.".format(node)) # Yield the node when it has not been encountered before. if not visisted.get(bytes(node_uuid)): yield node visisted[bytes(node_uuid)] = True # Attempt to get the first child of the node and hash it. child = node.GetDown() if child: child_uuid = child.FindUniqueID(c4d.MAXON_CREATOR_ID) if not child_uuid: raise RuntimeError("Could not retrieve UUID for {}.".format(child)) # Walk the graph in a depth first fashion. if child and not visisted.get(bytes(child_uuid)): node = child elif node == terminator: break elif node.GetNext(): node = node.GetNext() else: node = node.GetUp()
  • Relative File Path Not Recognize by Plug-in?

    2023 python
    5
    0 Votes
    5 Posts
    1k Views
    ferdinandF
    Hello @bentraje, Yes, the Python variant of GeGetPluginPath is not that useful as it just calls the C++ implementation which will then return the Python module path because the Python module is the C++ plugin which is calling GeGetPluginPath in that case. I will make the function description a bit more clear about that and also add a little example snippet for the module attribute __file__. And as what I would consider a fun fact: The __file__ attribute of modules is one of the many things where the language Python took inspiration from C/C++. In C++, __FILE__ is the macro which refers to the source file it is referenced in. I used this for example recently in the C++ Color Management examples to load an ICC file which is located next to a source file. But since users are usually not in the habit of compiling their C++ code themselves, or if they do, then want to move the resulting binary to a different place, __FILE__ is much less useful than __file__ as C++ there is a difference between the source and the executable. Cheers, Ferdinand
  • Run a GUI Dialog AFTER C4D Launches not BEFORE?

    python 2023
    7
    1
    0 Votes
    7 Posts
    1k Views
    B
    @ferdinand Thanks for the response. With the illustration code you provided, I misunderstood the documentation. Thanks for the clarification. Basically, I thought the PluginMessage/RegisterPlugin functions should be method (i.e. they should be under the class of the plugin data, command data etc). They actually live outside the classes. It now works as expected. Thanks!
  • InExclude detect 'Remove All' context menu command

    python
    3
    1
    0 Votes
    3 Posts
    549 Views
    mikeudinM
    Thank you @ferdinand ! Will chek it!
  • Change Bodypaint Active Channel Color

    python
    3
    1
    0 Votes
    3 Posts
    505 Views
    A
    Hi @m_adam. Thanks for the information! I needed this feature to generate UV texture, where polygon selection tags colorizes the texture with different colors. And since "Fill Layer, Fill Polygons and Outline Polygons" commands uses color from current "Channel Color" I needed option to change the color with a script. [image: 1675761996302-g89cotievo.png] c4d.CallCommand(170150) # Fill Layer c4d.CallCommand(170151) # Fill Polygons c4d.CallCommand(170152) # Outline Polygons But if this is not possible, one workaround that come to mind is to use "UV to Mesh" Scene Nodes Deformer/Capsule and render actual mesh with aligned camera. Cheers, Arttu
  • Snap to grid while MouseDrag()

    2023 python
    2
    0 Votes
    2 Posts
    424 Views
    ferdinandF
    Hello @pim, Thank you for reaching out to us. It depends a bit on what you expect to happen here. There is the snapping module of Cinema 4D but you cannot actually get values out of it, i.e., you cannot compute the snapped value of x with it, you can only define the snap settings with it. But when I understand your correctly, you just want to quantize some mouse inputs for a plugin and for that you must indeed compute the values yourself. It is best to also draw a snapping location onto the screen, so that user can see where the actual input is in relation to the mouse. How the snapping works in detail depends on what you want to do exactly when quantizing your plane drawing. The snapping module might become relevant here, because with it you can retrieve the working planes, which might be useful when placing planes. Cheers, Ferdinand
  • Plugin opens on Mac not correctly

    2023 python macos
    13
    1
    0 Votes
    13 Posts
    2k Views
    ferdinandF
    Hello @pim, In addition to my answer via mail, I will also answer here, as this might be interesting for the rest of the community. Cheers, Ferdinand So, the question was here "Why does my tree view not open with the right size?". The easy answer to this is that: You did neither set a minimum size for the dialog in GeDialog.Open(). Nor one for the tree view itself via GeDialog.AddCustomGui(). Both in conjunction did result in your dialog collapsing down to zero height. What can I do? Not much, the TreeViewCustomGui is not designed to scale to the size of its content. The underlying question is what you expect to happen here. a. Just have the tree view have some fixed minimum size, regardless of its content. b. Have the tree view initialize automatically to size, i.e., when the view has 10 items upon opening, it should have exactly 10 items height. When it is (a.) what you want, then this is easily doable with the minimum size passed to GeDialog.AddCustomGui(). When it is (b.), then you are more or less out of luck, as a tree view cannot scale automatically to the size of its content. You can adjust the minimum size dynamically based on the content which is going to be placed in the tree view, but when the content changes, you will have to flush your layout in order to be able to set a new minimum size. On a practical level it is also not so desirable to have a tree view scale like this, as this minimum height is not well defined. Should it be all items, or just all top level items, i.e., fully collapsed or fully expanded (which is implied by your example as all nodes start out as expanded). Let's say we choose fully collapsed. What happens when you have so many root nodes that the tree view will not fit on screen when making space for all root nodes. Example Result The dialog is set to have a minimum height which matches the total number of nodes in it. [image: 1675704054022-58d6b1aa-de83-484f-9d48-d6a0d327a98f-image.png] Code I had to cut here a bit, but the relevant parts are: class TreeNode: # ... def __len__(self): """(f_hoppe): Counts all descendants of this node, including the node itself. Implemented fully recursively. Should be implemented iteratively for production due to stack overflows and Python's recursion limit preventing them. Or the data should be acquired when textures are collected. """ count: int = 1 for child in self.children: count += len(child) class ListView(c4d.gui.TreeViewFunctions): COLUMN_COUNT: int = 1 MIN_LINE_HEIGHT: int = 24 MIN_WIDTH: int = 500 def __init__(self): # The root nodes of the tree view. self._rootNodes: list[TreeNode] = [] def __len__(self): """(f_hoppe): Returns the number of tree nodes in the instance. """ return sum([len(node) for node in self._rootNodes]) def GetMinSize(self) -> tuple[int, int]: """(f_hoppe): Returns the minimum GUI size for the data of this ListView instance. """ # But all these classic API pixel values are quite wonky anyways and the tree view does # many custom things. So we must do some ugly magic number pushing. Subtracting nine units # from the actual height of each row gave me sort of the best results, but the tree view # GUI does not scale linearly in height with the number of rows. Meaning that what looks # good for 5 items might not look good for 50 items. return (ListView.MIN_WIDTH, (ListView.MIN_LINE_HEIGHT - 9) * len(self)) def GetColumnWidth(self, root: TreeNode, userdata: None, obj: TreeNode, col: int, area: c4d.gui.GeUserArea): """(f_hoppe): This cannot be a constant value, as we will otherwise clip data. """ return area.DrawGetTextWidth(obj.textureName) + 24 def GetLineHeight(self, root, userdata, obj, col, area): """(f_hoppe): Used constant value. """ return ListView.MIN_LINE_HEIGHT # ... class SimpleDialog (c4d.gui.GeDialog): ID_TRV_TEXTURES: int = 1000 def __init__(self) -> None: self._treeView: c4d.gui.TreeViewCustomGui = None # This builds the tree node data so that self._listView._rootNodes holds the top level # node(s) for the tree managed by this ListView instance. self._listView: ListView = self.GetTree() super().__init__() def CreateLayout(self) -> bool: """(f_hoppe): I substantially rewrote this. """ # Because we already initialized the ListView data, we can use it to compute the minimum # size of the gadget. w, h = self._listView.GetMinSize() print(f"{self._listView.GetMinSize() = }, {len(self._listView) = }") # Use these values to define a default size so that it shows all columns. What you want to # be done here is sort of not intended by the TreView GUI, although admittedly desirable. # The closest thing we can do is set the minimum size for the gadget, so that all lines # will fit into it. This will then ofc also have the side effect that the GUI cannot be # scaled down beyond this point. You might want to implement ListView.GetMinSize() in a # different manner, so that it does not take into account all nodes and instead just the # top level nodes. self._treeView = self.AddCustomGui( id=SimpleDialog.ID_TRV_TEXTURES, pluginid=c4d.CUSTOMGUI_TREEVIEW, name="", flags=c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, minw=w, minh=h, customdata=SimpleDialog.SETTINGS_TREEVIEW) if not isinstance(self._treeView, c4d.gui.TreeViewCustomGui): raise MemoryError(f"Could not allocate tree view.") # If you want to do this at runtime, i.e., load a new texture path, you would have to # call GeDialog.LayoutChanged() on the layout group which contains the tree view, add # the tree view again (with new min size values), and then call GeDialog.LayoutChanged() # on the group. It might be easier to just live with a fixed minimum size just as # (300, 300) return True # ...
  • How do you collapse complex dependies in order?

    2023 python
    2
    0 Votes
    2 Posts
    538 Views
    ferdinandF
    Hello @fss, Thank you for reaching out to us. The Cinema 4D classic API has no dependency graph for its scene elements. If you want such information, you must gather it yourself. This is however a non-trivial task. You also have been asking this same question multiple times both here on the forum and via mail, with both Manuel and I giving you multiple times the same answer. To "collapse" things, you must use 'Current State to Object (CSTO)' and then join the results, as first reducing things to their current cache state (CSTO) will remove the dependencies between things. You can/could also do this manually, just as the joining operation, but it is then up to you to develop that. Please understand that we will not answer the same question over and over again. We enjoy and encourage discussions with users, but as stated in our forum guidelines: We cannot provide support for [...] code design that is in direct violation of Cinema's technical requirements [...] Find below an example. Cheers, Ferdinand Result for the fairly complex Mograph asset Example Scenes\Disciplines\Motion Graphics\01 Scenes\Funny Face.c4d: [image: 1675690420936-connect_and_delete.gif] Code '''Example for mimicking the "Connect & Delete" command in Python. Must be run from the Script Manager with the root objects selected whose local hierarchies should be collapsed. ''' import c4d import typing def Collapse(objects: list[c4d.BaseObject]) -> None: """Collapses all items in #objects as individual root nodes into singular objects. This function mimics the behaviour of the builtin (but unexposed) "Connect & Delete" command by first running the "CSTO" and then "JOIN" command. With setups complex enough, this can still fail due to the non-existent dependency graph of the classic API (when one does CSTO things in the wrong order). In 99.9% of the cases this will not be the case, but one should get the inputs with #GETACTIVEOBJECTFLAGS_SELECTIONORDER as I did below to give the user more control. (or alternatively do not batch operate). """ if len(objects) < 1: raise RuntimeError() doc: c4d.documents.BaseDocument = objects[0].GetDocument() doc.StartUndo() # CSTO all local hierarchies in #objects and replace these root nodes with their collapsed # counter parts. result = c4d.utils.SendModelingCommand(c4d.MCOMMAND_CURRENTSTATETOOBJECT, objects, c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc, c4d.MODELINGCOMMANDFLAGS_NONE) if not result or len(result) != len(objects): raise RuntimeError() for old, new in zip(objects, result): parent, pred = old.GetUp(), old.GetPred() doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, old) old.Remove() doc.InsertObject(new, parent, pred) doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, new) # Join the CSTO results root by root object, and then replace the CSTO results with the final # collapsed result. JOIN is a bit weird when it comes to transforms, so we must store the # transform of the to be joined object, then zero it out, and finally apply it to the joined # result again. for obj in result: mg: c4d.Matrix = obj.GetMg() obj.SetMg(c4d.Matrix()) joined = c4d.utils.SendModelingCommand(c4d.MCOMMAND_JOIN, [obj], c4d.MODELINGCOMMANDMODE_ALL, c4d.BaseContainer(), doc, c4d.MODELINGCOMMANDFLAGS_NONE) if not joined: raise RuntimeError() parent, pred = obj.GetUp(), obj.GetPred() doc.AddUndo(c4d.UNDOTYPE_DELETEOBJ, obj) obj.Remove() new: c4d.BaseObject = joined[0] new.SetMg(mg) doc.InsertObject(new, parent, pred) doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, new) doc.EndUndo() c4d.EventAdd() doc: c4d.documents.BaseDocument # The active document op: typing.Optional[c4d.BaseObject] # The active object, can be None. def main() -> None: """Runs the #Collapse() function on all currently selected objects as root nodes. """ selection: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER) if len(selection) < 1: print("Please select at least one root object.") else: Collapse(selection) if __name__ == "__main__": main()
  • Keyframing the source file on an ImageTexture shader

    r25
    3
    0 Votes
    3 Posts
    638 Views
    mocolocoM
    Hi, A simple addition on CTrack and DescId as I was also faced to this a time ago. You can consult the @ferdinand's explanations and exemples on CTrack to the following post : https://developers.maxon.net/forum/topic/14315/solved-how-to-setup-a-ctrack-on-tag-plugin-ui-slider-with-extended-details/8 Cheers, Christophe
  • Struggling on SetParameter for OLight

    python s26
    4
    0 Votes
    4 Posts
    1k Views
    mocolocoM
    Hi @m_adam, Thanks a lot for the flags, it does work as expected now. Are c4d.DESCFLAGS_GET_NONE and c4d.DESCFLAGS_GET_0 similars? Cheers, Christophe.
  • Get material / texture resolution.

    2023 python
    3
    1
    0 Votes
    3 Posts
    578 Views
    P
    Thanks for the good explanation and the example. Regards, Pim
  • Back on "message after tag delete" post

    python s26 sdk
    5
    0 Votes
    5 Posts
    1k Views
    mocolocoM
    Hi @ferdinand, Thanks a lot one more time for all the detailed examples and informations. I finally opt to a data container of the node, mostly due to the fact that the hooks are volatile and need to be set all the time. I also ran some tests with globals without having encounter issues, but indeed you need to be careful when handling this approach. Cheers, Christophe
  • How to make Icon buttons and Shortcut in python plugin

    python
    2
    0 Votes
    2 Posts
    506 Views
    ManuelM
    Hi, Welcome to the forum, you do not need to be sorry, we all ask basic question. There are differences between creating a script and plugins. Script is something you create inside the Script Manager. You can save them in a file and this file extension is .py. Plugins are something you must create in an external editor and save the file with the extension .pyp or .pypv (for encrypted files) with a certain plugin structure There are differents way to create an icon and a shortcut. icon for script: as state here, you can just use the command in the Script Manager to load a file that will be used as the icon for that script. If you save the script or already saved it, the picture will be saved in the same directory with the same name as the script. save the picture you want to use for a script in the same directory than your script with the same filename. icon for plugins: Plugins are registered using register commands like RegisterCommandPlugin, most of those commands allow you to pass as an argument the bitmap that will be used as the icon for that plugin. Shortcuts: To create shortcuts you must use the function AddShortcut you can check this thread where Maxime answer the question in the second part of his thread. Finally, you might want to create a palette to store your different icons and add the possibility to load the palette. Ferdinand answer this question in this thread. Cheers, Manuel