Group Details Private

Global Moderators

Forum wide moderators

  • RE: Encountered a possible error in the example code in the docs for AssetDataBasesInterface.FindRepository

    Hey @randymills,

    good to hear that there seems to be no issue anymore and there is certainly no need to apologize. We first of all know how easily one can get side tracked in coding and secondly bug reporting is a service to the community and never a mistake even when it turns out to be false alarm. Better having a few false alarms than having bugs in the API or errors in the documentation! Thank your for taking the time.

    As to why this happened to you is hard to say. The Maxon API (all the stuff in the maxon package) is not as mature as the Classic API (all the stuff in the c4d package) is in Python. So, I would not rule out some weird corner case 3000 which is buggy. But what I would also not rule out is that you might have shot yourself in the foot with type hinting. You strike me as more experienced user but I have seen less experienced users brick a Cinema 4D instance with type hinting more than once.

    # Type hinting is the practice to type annotate code in Python. it is mostly decorative but
    # I and some other SDK members tend to write type hinted code as it make clearer examples.
    
    # Typed hinted statement which hints at the fact that result is of type int.
    result: int = 1 + 1
    # Functionally the same as (unless you use something like Pydantic).
    result = 1 + 1
    
    import maxon
    
    # But one can shoot oneself in the foot when getting confused by the type hinting 
    # syntax with a colon.
    
    # A correctly type hinted call that states that repo is of type #AssetRepositoryRef 
    repo: maxon.AssetRepositoryRef = maxon.Cast(maxon.AssetRepositoryRef, obj)
    
    # We made a little mistake and replaced the colon with an equal sign, maybe because
    # type hinting is new to us and the colon feels a bit unnatural to us. What we do here,
    # is overwrite  maxon.AssetRepositoryRef, i.e., it is now not the type  AssetRepositoryRef
    # anymore but whatever the return value of our expression was. I.e., we do a = b = 1
    repo = maxon.AssetRepositoryRef = maxon.Cast(maxon.AssetRepositoryRef, obj)
    
    # Our Cinema 4D instance is now permanently bricked until we restart Cinema 4D because
    # we "lost" the type #maxon.AssetRepositoryRef.
    

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK
  • RE: MCOMMAND_SELECTALL and MCOMMAND_SELECTINVERSE not working?

    Hi @datamilch,

    The c4d.utils.SendModelingCommand has an optional argument bc that contains settings that you pass to the command. Default value for bc is None, which is not the same as empty c4d.BaseContainer. Some commands tolerate bc=None, some require it to be present even if it's empty. The following lines work as expected:

    c4d.utils.SendModelingCommand(c4d.MCOMMAND_SELECTALL, [obj], c4d.MODELINGCOMMANDMODE_POINTSELECTION, c4d.BaseContainer(), doc)
    c4d.utils.SendModelingCommand(c4d.MCOMMAND_SELECTSHRINK, [obj], c4d.MODELINGCOMMANDMODE_POINTSELECTION, c4d.BaseContainer(), doc)
    

    Please also note, that in your code snippet you clone the object and perform some commands on it, but you never insert it back to document. Also note that some modeling commands require document context, hence in such cases the objects are additionally required to be inserted into a document.

    Cheers,
    Ilia

    posted in Cinema 4D SDK
  • RE: Use buttons in tags. Rope tag.

    Hi @SmetK,
    Please make sure you have made yourself familiar with our Support Procedures, especially the "How to Ask Question" part:

    Repeatable: Your claims or problem should be repeatable for us. In most cases this means posting executable code which demonstrates your problem

    Currently I can only guess the exact context you're asking this question in. Please note that attaching the project scene file that illustrates your setup is not prohibited, rather just the opposite - is highly appreciated.
    Regarding your question. If you were in the Python Script context with a normal spline + Rope tag, you could simulate pressing the Fix button in a normal way by sending c4d.MSG_DESCRIPTION_COMMAND (e.g. in a way Maxime showed here: Update button) :
    4cae0d67-7684-4669-ae98-6dcb490a1343-image.png

    import c4d
    def main() -> None:
        opSpline: c4d.PointObject = op
        if op is None:
            return print('No object found')
        ropeTag = opSpline.GetTag(c4d.Trope)
        if ropeTag is None:
            return print('No rope tag found')
     
        bs = opSpline.GetPointS()
        bs.Select(0)
     
        data: dict = { 'id': c4d.HAIR_SDYNAMICS_PBD_SET_FIXED }
        ropeTag.Message(c4d.MSG_DESCRIPTION_COMMAND, data)
     
    if __name__ == '__main__':
        main()
    

    However, assuming you'd like to have Rope tag on the Python generator, this doesn't seem to be possible. The aforementioned buttons only make effect, when the Rope tag is applied to the spline object (c4d.Ospline), otherwise nothing happens. You could potentially send the message to the Ospline cache object inside main() of the Python Generator, but this still wouldn't work as the Rope tag of the cache object is different from the one on the Python generator, and copying it is not allowed due to threaded nature of the python generator.

    Cheers,
    Ilia

    posted in Cinema 4D SDK
  • RE: Encountered a possible error in the example code in the docs for AssetDataBasesInterface.FindRepository

    Hey @randymills,

    Thank you for reaching out to us. I would not want to rule out that there is either a regression in our API or that I made a mistake when I documented this, but currently it does not look like that to me.

    Existing Code

    There is for example this thread where I do exactly what is shown in the snippet in the docs, cast an ObjectRef to an AssetRepositoryRef, in that case a sub type. The script still runs just fine for me (2024.5.1, Win 11), and when I inspect the relevant section, the values are also what I would expect them to be, i.e., this:

        obj: maxon.ObjectRef = maxon.AssetDataBasesInterface.FindRepository(url)
        if obj.IsNullValue():
            raise RuntimeError(f"Could not establish repository for: {url}.")
    
        # #FindRepository returns an ObjectRef, not an AssetRepositoryRef, we must cast it to its "full"
        # form. Specifically, to a reference to a maxon.WatchFolderAssetRepositoryInterface we just 
        # implemented. Yay, casting Python, the dream becomes true :P
        repo: maxon.WatchFolderAssetRepositoryRef = maxon.Cast(maxon.WatchFolderAssetRepositoryRef, obj)
        print(f"{obj = }")
        print(f"{repo = }")
    

    will print that:

    obj = maxon.ObjectRef object at 0x1bce66b2720 with the data: net.maxon.assets.repositorytype.watchfolder@0x000001BCAC5814C0<net.maxon.datatype.internedid> net.maxon.assetrepository.skipinsearch : <bool> false
    <net.maxon.datatype.internedid> net.maxon.assetrepository.exportonsaveproject : <bool> true
    repo = maxon.WatchFolderAssetRepositoryRef object at 0x1bda1573420 with the data: net.maxon.assets.repositorytype.watchfolder@0x000001BCAC5814C0<net.maxon.datatype.internedid> net.maxon.assetrepository.skipinsearch : <bool> false
    <net.maxon.datatype.internedid> net.maxon.assetrepository.exportonsaveproject : <bool> true
    

    General Problem that Interfaces and References are not Interchangeable

    On top of that comes that your code uses generally a bit flawed logic; which does not mean that there could not be a bug or an exception. Interfaces and references realize a managed memory architecture in our C++ API and are not interchangeable. The interface is the actual object held in memory, to which zero to many references can exist. The lifetime of an object is then managed by reference counting. This is very similar to what managed languages like Python, Java, C# do internally, it is just a bit more on the nose in our API. To briefly highlight the concept at the example of Python:

    # This causes Python to allocate the array [0, 1, 2, 3, 4] in its internal object table and then 
    # create a reference (the value of id(data)) which is assigned to data.
    data: list[int] = [0, 1, 2, 3, 4]
    # When we now assign #data to something else, we are just drawing another reference to the actual
    # data of #data, #x is the same object as #data.
    x: list[int] = data
    print(f"{id(data) == id(x) = }") # Will print True, another way to write this would be "data is x".
    
    # Another popular case to demonstrate this are strings, which are also reference managed in most managed
    # languages. This means each string is only uniquely held in memory. So, when we allocate "Maxon" twice, 
    # there is actually only one object in memory.
    a: str = "Maxon"
    b: str = "Maxon"
    # Although we provided here a string literal in both cases, Python catches us duplicating data and
    # only holds the object once in its object table.
    print(f"{id(a) == id(b) = }") # Will print True.
    
    # This is a bit deceptive, we are actually not modifying here the string which is referenced by #b,
    # because strings are immutable in Python due to the "only stored once logic" shown above. When we
    # would modify the actual object, we would also change the value of #a. So, Python does actually copy
    # the object referenced by #b and then modifies that copy and assigns a reference to that to #b.
    b += "Foo"
    print(f"{id(a) == id(b) = }") # Will print False, a and b are not the same object anymore.
    
    id(data) == id(x) = True
    id(a) == id(b) = True
    id(a) == id(b) = False
    

    Interfaces are similar to the hidden entities living in Python's object table, and references are somewhat equivalent to the frontend data instances a user deals with in Python. We have a manual for Python and C++ about the subject for our API. The C++ manual is slightly better, but both unfortunately do their best to obfuscate the subject with a very technical approach rather than explaining it in laymans terms.

    When we really boil all that down, then interfaces are "classes" and references are instances of that classes. That is more an analogy than an actual fact but conveys the major idea that an instances of a class are always meant to be dealt with in the form of references to some interface object.

    About your Issue

    Because of all that, it strikes me as a bit unlikely that you must cast down a reference to an interface (maxon.AssetDataBasesInterface.FindRepository returns an ObjectRef, not an ObjectInterface, i.e., a reference to some data, not the actual data). Because our managed memory approach does expose that actual raw data (all the SomethingInterface types), you can also technically use the raw object, as all the logic of that type is implemented there. But doing that is very uncommon and you also lack the logic which is provided by references like for example IsEmpty(), IsNullValue(), or GetType() which is not unimportant. References derive from maxon.Data where the these methods provided by references are implemented, interfaces derive from maxon.ObjectInterface and implement the actual functionality of some type, e.g., FindAssets for AssetRepositoryInterface. The methods of an interface then "shine through" on its reference, because the reference is also in inheritance relation to its interface.

    When you need more assistance here, or want this generally to be clarified, please share your code. There could be a bug or an inconsistency in our API, or I simply documented something incorrectly. But for me it currently looks more like that something in your code is going wrong.

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK
  • RE: How do I sort the plug-in menu?

    Hey @treezw,

    As it turns out, there is also a special syntax with which you can do that, but it requires you registering a dummy hook (with its own ID) for each separator.

    Cheers,
    Ferdinand

    Result

    b047158c-d1b3-4f56-8da1-37ee6312288c-image.png

    Code
    """Realizes a simple plugin collection with multiple command plugins.
    """
    
    import c4d
    
    class FooCommandData(c4d.plugins.CommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064229
        PLUGIN_NAME: str = "Foo Command"
    
        def Execute(self, doc):
            print(f"Executing {self.PLUGIN_NAME}")
            return True
        
    class BarCommandData(FooCommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064230
        PLUGIN_NAME: str = "Bar Command"
        
    class BazCommandData(FooCommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064231
        PLUGIN_NAME: str = "Baz Command"
    
    class SeparatorCommandData(c4d.plugins.CommandData):
        """Realizes a dummy command plugin used by separators in menus.
    
        Please use a command data plugin as a dummy plugin, as there are not unlimited slots for other
        plugin types in Python (a user can have only 50 of each NodeData derived plugin type installed).
    
        We can have each separator use this type but they all need to have a unique ID.
        """
        SEPARATOR_ID_1: int = 1064234
        SEPARATOR_ID_2: int = 1064235
    
        def Execute(self, doc): return True
    
    def RegisterPlugins() -> None:
        """Registers the plugins of the plugin collection.
        """
        # To sort plugins in a custom order, we must use the "$#00" syntax. We can pass here a two-digit 
        # number for the index of the plugin in the menu. The logic we use here is that when #hook is a
        # an #int, we register there a dummy plugin for a separator.
        for i, hook in enumerate((FooCommandData, 
                                  SeparatorCommandData.SEPARATOR_ID_1, 
                                  BazCommandData, 
                                  SeparatorCommandData.SEPARATOR_ID_2, 
                                  BarCommandData)):
            
            # Separators can be realized with the special syntax "$#00--", but we need a dummy plugin 
            # for each separator. We can have each separator use the DummyCommandData type but they all
            # need to have a unique plugin ID. If you want to use separators, please use a command data 
            # plugin use a command data plugin as the dummy plugin, as there are not unlimited slots for 
            # other plugin types.
    
            if isinstance(hook, int):
                label = f"#${str(i).zfill(2)}--"                   # E.g., "#$01--" will be a separator at index 1
                pid: int = hook
                hook = SeparatorCommandData
            else:
                label = f"#${str(i).zfill(2)}{hook.PLUGIN_NAME}"  # E.g., "#$00Foo" will be "Foo" at index 0
                pid: int = hook.PLUGIN_ID
    
            print (f"{label = }")
            if not c4d.plugins.RegisterCommandPlugin(pid, label, 0, None ,None, hook()):
                raise RuntimeError(f"Failed to register {hook} plugin.")
    
    if __name__ == "__main__":
        RegisterPlugins()
    
    posted in Cinema 4D SDK
  • RE: How do I sort the plug-in menu?

    Hey, separators are not possible in this menu, as it is dynamically built. When you want separators, you must build your own menu.

    posted in Cinema 4D SDK
  • RE: How do I sort the plug-in menu?

    Hello @treezw,

    Thank you for reaching out to us and thank you for providing stripped down example code, we really appreciate that. What @Dunhou said is right, you can build entirely custom menus entries as shown in that thread he linked to.

    But I think you want to operate within the "Extensions" menu, right? I would not say that we discourage adding new top level menu entries in general, but the default should be that plugins are only placed in the "Extensions" menu. I am not quite sure how your screen shots came to pass, as they do not line up with the demo code you gave us. But in general, all entries are alpha-numerically sorted in the Extensions and its sub-menus. So, I would not call this 'very bad' sorting, it is just how Cinema does things. When we have for example the Listing A shown below, Cinema 4D will not care that we register the commands in the order Foo, Baz, Bar, it will put them in alphanumerical order into the menu, i.e., do this:

    base.png

    This assumes that there is a {SOMEPATH}\plugins\MyPluginCollection\myPluginCollection.pyp with the code shown in Listing A and that the user has linked {SOMEPATH}\plugins as a plugin path in his or her plugin collection. The sub menu entry MyPluginCollection then comes from the folder of the same name.

    But we can disable this automated sorting with the #$xy{SOMELABEL} syntax, where xy is the two digit index of where the plugin hook should be displayed in its extensions menu. This is being shown in Listing B and will then result in this, where we force a non-alphanumerical order:

    sorted.png

    One can also stack multiple folders in a plugin package to build complex menus in the extensions menu. E.g., this, where {SOMEPATH}\py is a plugins directory linked in a Cinema 4D instance:

    28df0feb-ce85-4342-9938-d765b07c935f-image.png

    Will result in this:

    e53f2962-bc45-49c0-916e-cb79de4bc153-image.png

    There is of course some limit to which it makes sense to organize plugins in this manner. But in general, you should avoid opening new top level menu entries unless your plugin offers a s substantial feature set. This is not a technical limitation, there is no technical harm in doing that, but a user experience restriction. We want Cinema 4D to have a consistent and clean look. When every plugin creates its top-level menu entry, the menu bar of a user can become quite confusing or even overflow (become wider than one screen width). If your plugin suite mandates its own menu entry is up to you to decide but we would encourage asking yourself if that is really beneficial for your user experience.

    Cheers,
    Ferdinand

    Code

    Listing A
    """Realizes a simple plugin collection with multiple command plugins.
    """
    
    import c4d
    
    class FooCommandData(c4d.plugins.CommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064229
        PLUGIN_NAME: str = "Foo Command"
    
        def Execute(self, doc):
            print(f"Executing {self.PLUGIN_NAME}")
            return True
        
    class BarCommandData(FooCommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064230
        PLUGIN_NAME: str = "Bar Command"
        
    class BazCommandData(FooCommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064231
        PLUGIN_NAME: str = "Baz Command"
    
    
    def RegisterPlugins() -> None:
        """Registers the plugins of the plugin collection.
        """
        # When we place this file inside a folder named "myPluginCollection" in the plugins directory,
        # linked as a plugin folder in Cinema, we will wind up with the following menu structure:
        #
        # Extensions
        #   |- ...
        #   |- myPluginCollection
        #       |- Bar Command
        #       |- Baz Command
        #       |- Foo Command
        #
        # This is because Cinema will sort by default all entries in the Extensions alphanumerically.
        for i, hook in (FooCommandData, BazCommandData, BarCommandData):
            if not c4d.plugins.RegisterCommandPlugin(
                hook.PLUGIN_ID, hook.PLUGIN_NAME, 0, None ,None, hook()):
                raise RuntimeError(f"Failed to register {hook.PLUGIN_NAME} plugin.")
    
    if __name__ == "__main__":
        RegisterPlugins()
    
    Listing B
    """Realizes a simple plugin collection with multiple command plugins with a custom menu order.
    """
    
    import c4d
    
    class FooCommandData(c4d.plugins.CommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064229
        PLUGIN_NAME: str = "Foo Command"
    
        def Execute(self, doc):
            print(f"Executing {self.PLUGIN_NAME}")
            return True
        
    class BarCommandData(FooCommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064230
        PLUGIN_NAME: str = "Bar Command"
        
    class BazCommandData(FooCommandData):
        """Realizes a dummy command plugin.
        """
        PLUGIN_ID: int = 1064231
        PLUGIN_NAME: str = "Baz Command"
    
    
    def RegisterPlugins() -> None:
        """Registers the plugins of the plugin collection.
        """
        # To sort plugins in a custom order, we must use the "$#00" syntax for the plugin label. The plugin 
        # will not display that prefix in its name, but with the `xy` part in the prefix we can set an position
        # index for the menu of the plugin. E.g., "#$05Blah Command" means that Cinema 4D will try to put the
        # "Blah Command" at the 5th position in that menu. 
        for i, hook in enumerate((FooCommandData, BazCommandData, BarCommandData)):
            # E.g, "#$00Foo Command". It is important to use the zfill method to ensure that the number
            # has always two digits. We cannot do any sorting beyond 100 plugins within the same menu.
            label: str = f"#${str(i).zfill(2)}{hook.PLUGIN_NAME}"
            if not c4d.plugins.RegisterCommandPlugin(
                hook.PLUGIN_ID, label, 0, None ,None, hook()):
                raise RuntimeError(f"Failed to register {hook.PLUGIN_NAME} plugin.")
    
    if __name__ == "__main__":
        RegisterPlugins()
    
    posted in Cinema 4D SDK
  • RE: Retrieving Images from Cinema 4D's Picture Viewer for Importing into Photoshop

    Hello,

    Thank you for reaching out to us. @Dunhou is right, the Picture Viewer is sealed, which means that you can put images into it (with c4d.bitmaps.ShowBitmap or c4d.documents.LoadFile) but you cannot get images or information out of it. This generally applies to all UIs, we do not expose UIs, but the data structures behind them. So, there is for example no Object or Material Manager API interface, their functionalities are exposed via the scene graph. For the Picture Viewer there is no data structure exposed.

    Cheers,
    Ferdinand

    posted in Cinema 4D SDK
  • RE: Changing the angle of spline-aligned objects

    Good to hear that you found your solution!

    posted in Cinema 4D SDK
  • RE: Changing the angle of spline-aligned objects

    Hey @SmetK,

    Thank you for reaching out to us. Please provide your code in future postings as pointed out before. Screenshots are not a substitute for code. I have provided a brief example of what you are trying to do.

    Cheers,
    Ferdinand

    Result

    4cf1b10d-933e-43e8-b679-3054cf1a615a-image.png

    Code

    """Constructs cone objects on the selected spline object at five different points and rotates them 
    by 90 degrees on their principal X and Z axes.
    """
    
    import c4d
    
    doc: c4d.documents.BaseDocument  # The currently active document.
    op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
    
    # Ninety degrees in radians and two pre-built matrices for rotating 90 degrees around the X and Z 
    # axes. The Y axis is not too useful here since a cone is symmetrical along its Y axis.
    RAD_90_DEG : float = c4d.utils.DegToRad(90)
    TRANSFORM_90_X : c4d.Matrix = c4d.utils.MatrixRotX(RAD_90_DEG)
    TRANSFORM_90_Z : c4d.Matrix = c4d.utils.MatrixRotZ(RAD_90_DEG)
    
    def GetCone(doc: c4d.documents.BaseDocument) -> c4d.BaseObject:
        """Creates a new cone object.
        """
        cone: c4d.BaseObject = c4d.BaseObject(c4d.Ocone)
        cone[c4d.PRIM_CONE_HEIGHT] = 40
        cone[c4d.PRIM_CONE_BRAD] = 10
        doc.InsertObject(cone)
    
        return cone
    
    def main() -> None:
        """Called by Cinema 4D when the script is being executed.
        """
        # Ensure that there is an editable spline object selected.
        if not isinstance(op, c4d.SplineObject):
            return
        
        # Create a new SplineHelp object and initialize it with the selected spline object. Then iterate
        # over five offsets (0, 0.25, 0.5, 0.75, 1) and get the matrix at each offset. A spline does not
        # have unambiguously a matrix for each offset since a normal and a tangent of a point only define
        # two out of the three degrees of freedom. The matrices which are constructed here are in respect
        # to the #upvector we pass to the SplineHelp object.
        sh: c4d.utils.SplineHelp = c4d.utils.SplineHelp()
        sh.InitSplineWithUpVector(op, upvector=c4d.Vector(0, 1, 0))
    
        for offset in (0, 0.25, 0.5, 0.75, 1):
            mg: c4d.Matrix = sh.GetMatrix(offset)
    
            # Now construct two cones on each point and rotate them 90 degrees around the X and Z axes.
            # The order in matrix multiplication is important, because other than for example the natural
            # or real numbers, matrix multiplication is not commutative. So X * Y is not always equal to
            # Y * X in matrix multiplication. In this case we want to multiply the spline point matrix
            # by our additional rotation matrices. We already operate in world coordinates, with #mg,
            # so we do not need to multiply by the matrix of the spline object itself.
            coneX: c4d.BaseObject = GetCone(doc)
            coneZ: c4d.BaseObject = GetCone(doc)
            coneX.SetMg(mg * TRANSFORM_90_X)
            coneZ.SetMg(mg * TRANSFORM_90_Z)
            coneX.SetName(coneX.GetName() + "X")
            coneZ.SetName(coneZ.GetName() + "Z")
    
        c4d.EventAdd()
    
    
    if __name__ == '__main__':
        main()
    
    posted in Cinema 4D SDK