Navigation

    • Register
    • Login
    • Search
    1. Home
    2. ferdinand
    3. Best
    • Profile
    • More
      • Following
      • Followers
      • Topics
      • Posts
      • Best
      • Groups

    Best posts made by ferdinand

    RE: API for new behavior of opnening Windows in Layout

    Hello @holgerbiebrach,

    please excuse the wait. So, this is possible in Python and quite easy to do. This new behavior is just the old dialog folding which has been reworked a little bit. I have provided a simple example at the end of the posting. There is one problem regarding title bars which is sort of an obstacle for plugin developers which want to distribute their plugins, it is explained in the example below.

    I hope this helps and cheers,
    Ferdinand

    The result:
    3453535.gif
    The code:

    """Example for a command plugin with a foldable dialog as provided with the
    Asset Browser or Coordinate Manger in Cinema 4D R25.
    
    The core of this is just the old GeDialog folding mechanic which has been
    changed slightly with R25 as it will now also hide the title bar of a folded
    dialog, i.e., the dialog will be hidden completely.
    
    The structure shown here mimics relatively closely what the Coordinate Manger
    does. There is however one caveat: Even our internal implementations do not
    hide the title bar of a dialog when unfolded. Instead, this is done via 
    layouts, i.e., by clicking onto the ≡ icon of the dialog and unchecking the
    "Show Window Title" option and then saving such layout. If you would want
    to provide a plugin which exactly mimics one of the folding managers, you
    would have to either ask your users to take these steps or provide a layout.
    
    Which is not ideal, but I currently do not see a sane way to hide the title
    bar of a dialog. What you could do, is open the dialog as an async popup which 
    would hide the title bar. But that would also remove the ability to dock the 
    dialog. You could then invoke `GeDialog.AddGadegt(c4d.DIALOG_PIN, SOME_ID)`to 
    manually add a pin back to your dialog, so that you can dock it. But that is 
    not how it is done internally by us, as we simply rely on layouts for that.
    """
    
    import c4d
    
    
    class ExampleDialog (c4d.gui.GeDialog):
        """Example dialog that does nothing.
    
        The dialog itself has nothing to do with the implementation of the
        folding.
        """
        ID_GADGETS_START = 1000
        ID_GADGET_GROUP = 0
        ID_GADGET_LABEL = 1
        ID_GADGET_TEXT = 2
    
        GADGET_STRIDE = 10
        GADEGT_COUNT = 5
    
        def CreateLayout(self) -> bool:
            """Creates dummy gadgets.
            """
            self.SetTitle("ExampleDialog")
            flags = c4d.BFH_SCALEFIT
    
            for i in range(self.GADEGT_COUNT):
                gid = self.ID_GADGETS_START + i * self.GADGET_STRIDE
                name = f"Item {i}"
    
                self.GroupBegin(gid + self.ID_GADGET_GROUP, flags, cols=2)
                self.GroupBorderSpace(5, 5, 5, 5)
                self.GroupSpace(2, 2)
                self.AddStaticText(gid + self.ID_GADGET_LABEL, flags, name=name)
                self.AddEditText(gid + self.ID_GADGET_TEXT, flags)
                self.GroupEnd()
            return True
    
    
    class FoldingManagerCommand (c4d.plugins.CommandData):
        """Provides the implementation for a command with a foldable dialog.
        """
        ID_PLUGIN = 1058525
        REF_DIALOG = None
    
        @property
        def Dialog(self) -> ExampleDialog:
            """Returns a class bound ExampleDialog instance.
            """
            if FoldingManagerCommand.REF_DIALOG is None:
                FoldingManagerCommand.REF_DIALOG = ExampleDialog()
    
            return FoldingManagerCommand.REF_DIALOG
    
        def Execute(self, doc: c4d.documents.BaseDocument) -> bool:
            """Folds or unfolds the dialog.
    
            The core of the folding logic as employed by the Asset Browser
            or the Coordinate manager in R25.
            """
            # Get the class bound dialog reference.
            dlg = self.Dialog
            # Fold the dialog, i.e., hide it if it is open and unfolded. In C++
            # you would also want to test for the dialog being visible with
            # GeDialog::IsVisible, but we cannot do that in Python.
            if dlg.IsOpen() and not dlg.GetFolding():
                dlg.SetFolding(True)
            # Open or unfold the dialog. The trick here is that calling
            # GeDialog::Open will also unfold the dialog.
            else:
                dlg.Open(c4d.DLG_TYPE_ASYNC, FoldingManagerCommand.ID_PLUGIN)
    
            return True
    
        def RestoreLayout(self, secret: any) -> bool:
            """Restores the dialog on layout changes.
            """
            return self.Dialog.Restore(FoldingManagerCommand.ID_PLUGIN, secret)
    
        def GetState(self, doc: c4d.documents.BaseDocument) -> int:
            """Sets the command icon state of the plugin.
    
            This is not required, but makes it a bit nicer, as it will indicate
            in the command icon when the dialog is folded and when not.
            """
            dlg = self.Dialog
            result = c4d.CMD_ENABLED
            if dlg.IsOpen() and not dlg.GetFolding():
                result |= c4d.CMD_VALUE
    
            return result
    
    
    def RegisterFoldingManagerCommand() -> bool:
        """Registers the example.
        """
        return c4d.plugins.RegisterCommandPlugin(
            id=FoldingManagerCommand.ID_PLUGIN,
            str="FoldingManagerCommand",
            info=c4d.PLUGINFLAG_SMALLNODE,
            icon=None,
            help="FoldingManagerCommand",
            dat=FoldingManagerCommand())
    
    
    if __name__ == '__main__':
        if not RegisterFoldingManagerCommand():
            raise RuntimeError(
                f"Failed to register {FoldingManagerCommand} plugin.")
    
    posted in Cinema 4D SDK •
    RE: Welcome Mr. Hoppe

    Hi,

    thanks for the kind words both from Maxon and the community. I am looking forward to my upcoming adventures with the SDK Team and Cinema community.

    Cheers,
    Ferdinand

    posted in Maxon Announcements •
    RE: Reading proper decimal values on lower numbers?

    Hi,

    that your script is not working has not anything to do with pseudo decimals, but the fact that you are treating numbers as strings (which is generally a bad idea) in a not very careful manner. When you truncate the string representation of a number which is represented in scientific notation (with an exponent), then you also truncate that exponent and therefor change the value of the number.

    To truncate a float you can either take the floor of my_float * 10 ** digits and then divide by 10 ** digits again or use the keyword round.

    data = [0.03659665587738824,
            0.00018878623163019122,
            1.1076812650509394e-03,
            1.3882258325566638e-06]
    
    for n in data:
        rounded = round(n, 4)
        floored = int(n * 10000) / 10000
        print(n, rounded, floored)
    
    0.03659665587738824 0.0366 0.0365
    0.00018878623163019122 0.0002 0.0001
    0.0011076812650509394 0.0011 0.0011
    1.3882258325566637e-06 0.0 0.0
    [Finished in 0.1s]
    

    Cheers
    zipit

    posted in General Talk •
    RE: Modified Pop Up Menu

    Hi,

    as @Cairyn said the problem is unreachable code. I also just saw now that you did assign the same ID to all your buttons in your CreateLayout(). Ressource and dialog element IDs should be unique. I would generally recommend to define your dialogs using a resource, but here is an example on how to do it in code.

    BUTTON_BASE_ID = 1000
    BUTTON_NAMES = ["Button1", "Button2", "Button3", "Button4", "Button5"]
    BUTTON_DATA = {BUTTON_BASE_ID + i: name for i, name in enumerate(BUTTON_NAMES)}
    
    class MyDialog(gui.GeDialog):
    
        def CreateLayout(self):
            """
            """
            self.GroupBegin(id=1013, flags=c4d.BFH_SCALEFIT, cols=5, rows=4)
            for element_id, element_name in BUTTON_DATA.items():
                self.AddButton(element_id, c4d.BFV_MASK, initw=100, 
                               name=element_name)
            self.GroupEnd()
            return True
    
        def Command(self, id, msg):
            """
            """
            if id == BUTTON_BASE_ID:
                print "First button has been clicked"
            elif id == BUTTON_BASE_ID + 1:
                print "Second button has been clicked"
            # ...
            if id in BUTTON_DATA.keys(): # or just if id in BUTTON_DATA
                self.Close()
            return True
    
    posted in Cinema 4D SDK •
    RE: GetAllTextures from materials only

    Hi,

    sorry for all the confusion. You have to pass actual instances of objects. The following code does what you want (and this time I actually tried it myself ;)).

    import c4d
    
    def main():
        """
        """
        bc = doc.GetAllTextures(ar=doc.GetMaterials())
        for cid, value in bc:
            print cid, value
    
    if __name__=='__main__':
       main()
    

    Cheers,
    zipit

    posted in Cinema 4D SDK •
    RE: Object materials won't show up in final render

    Hi,

    you use GetActiveDocument() in a NodeData environment. You cannot do this, since nodes are also executed when their document is not the active document (while rendering for example - documents get cloned for rendering).

    Cheers
    zipit

    posted in Cinema 4D SDK •
    RE: More Examples for GeUserArea?

    Hi,

    you have to invoke AddUserArea and then attach an instance of your implemented type to it. Something like this:

    my_user_area = MyUserAreaType()
    self.AddUserArea(1000,*other_arguments)
    self.AttachUserArea(my_user_area, 1000)
    

    I have attached an example which does some things you are trying to do (rows of things, highlighting stuff, etc.). The gadget is meant to display a list of boolean values and the code is over five years old. I had a rather funny idea of what good Python should look like then and my attempts of documentation were also rather questionable. I just wrapped the gadget into a quick example dialog you could run as a script. I did not maintain the code, so there might be newer and better ways to do things now.

    Also a warning: GUI stuff is usually a lot of work and very little reward IMHO.

    Cheers
    zipit

    import c4d
    import math
    import random
    
    from c4d import gui
    
    # Pattern Gadget
    IDC_SELECTLOOP_CELLSIZE = [32, 32]
    IDC_SELECTLOOP_GADGET_MINW = 400
    IDC_SELECTLOOP_GADGET_MINH = 32
    
    class ExampleDialog(gui.GeDialog):
        """
        """
        def CreateLayout(self):
            """
            """
    
            self.Pattern = c4d.BaseContainer()
            for i in range(10):
                self.Pattern[i] = random.choice([True, False])
            self.PatternSize = len(self.Pattern)
    
            self.gadget = Patterngadget(host=self)
            self.AddUserArea(1000, c4d.BFH_FIT, 400, 32)
            self.AttachUserArea(self.gadget, 1000)
            return True
    
    class Patterngadget(gui.GeUserArea):
        """
        A gui gadget to modify and display boolean patterns.
        """
    
        def __init__(self, host):
            """
            :param host: The hosting BaseToolData instance
            """
            self.Host = host
            self.BorderWidth = None
            self.CellPerColumn = None
            self.CellWidht = IDC_SELECTLOOP_CELLSIZE[0]
            self.CellHeight = IDC_SELECTLOOP_CELLSIZE[1]
            self.Columns = None
            self.Height = None
            self.Width = None
            self.MinHeight = IDC_SELECTLOOP_GADGET_MINH
            self.MinWidht = IDC_SELECTLOOP_GADGET_MINW
            self.MouseX = None
            self.MouseY = None
    
        """------------------------------------------------------------------------
            Overridden methods
            --------------------------------------------------------------------"""
    
        def Init(self):
            """
            Init the gadget.
            :return : Bool
            """
            self._get_colors()
            return True
    
        def GetMinSize(self):
            """
            Resize the gadget
            :return : int, int
            """
            return int(self.MinWidht), int(self.MinHeight)
    
        def Sized(self, w, h):
            """
            Get the gadgets height and width
            """
            self.Height, self.Width = int(h), int(w)
            self._fit_gadget()
    
        def Message(self, msg, result):
            """
            Fetch and store mouse over events
            :return : bool
            """
            if msg.GetId() == c4d.BFM_GETCURSORINFO:
                base = self.Local2Screen()
                if base:
                    self.MouseX = msg.GetLong(c4d.BFM_DRAG_SCREENX) - base['x']
                    self.MouseY = msg.GetLong(c4d.BFM_DRAG_SCREENY) - base['y']
                    self.Redraw()
                    self.SetTimer(1000)
            return gui.GeUserArea.Message(self, msg, result)
    
        def InputEvent(self, msg):
            """
            Fetch and store mouse clicks
            :return : bool
            """
            if not isinstance(msg, c4d.BaseContainer):
                return True
            if msg.GetLong(c4d.BFM_INPUT_DEVICE) == c4d.BFM_INPUT_MOUSE:
                if msg.GetLong(c4d.BFM_INPUT_CHANNEL) == c4d.BFM_INPUT_MOUSELEFT:
                    base = self.Local2Global()
                    if base:
                        x = msg.GetLong(c4d.BFM_INPUT_X) - base['x']
                        y = msg.GetLong(c4d.BFM_INPUT_Y) - base['y']
                        pid = self._get_id(x, y)
                        if pid <= self.Host.PatternSize:
                            self.Host.Pattern[pid] = not self.Host.Pattern[pid]
                            self.Redraw()
            return True
    
        def Timer(self, msg):
            """
            Timer loop to catch OnMouseExit
            """
            base = self.Local2Global()
            bc = c4d.BaseContainer()
            res = gui.GetInputState(c4d.BFM_INPUT_MOUSE,
                                    c4d.BFM_INPUT_MOUSELEFT, bc)
            mx = bc.GetLong(c4d.BFM_INPUT_X) - base['x']
            my = bc.GetLong(c4d.BFM_INPUT_Y) - base['y']
            if res:
                if not (mx >= 0 and mx <= self.Width and
                        my >= 0 and my <= self.Height):
                    self.SetTimer(0)
                    self.Redraw()
    
        def DrawMsg(self, x1, y1, x2, y2, msg):
            """
            Draws the gadget
            """
            # double buffering
            self.OffScreenOn(x1, y1, x2, y2)
            # background & border
            self.DrawSetPen(self.ColBackground)
            self.DrawRectangle(x1, y1, x2, y2)
            if self.BorderWidth:
                self.DrawBorder(c4d.BORDER_THIN_IN, x1, y1,
                                self.BorderWidth + 2, y2 - 1)
            # draw pattern
            for pid, state in self.Host.Pattern:
                x, y = self._get_rect(pid)
                self._draw_cell(x, y, state, self._is_focus(x, y))
    
        """------------------------------------------------------------------------
            Public methods
            --------------------------------------------------------------------"""
    
        def Update(self, cid=None):
            """
            Update the gadget.
            :param cid: A pattern id to toggle.
            """
            if cid and cid < self.Host.PatternSize:
                self.Host.Pattern[cid] = not self.Host.Pattern[cid]
            self._fit_gadget()
            self.Redraw()
    
        """------------------------------------------------------------------------
            Private methods
            --------------------------------------------------------------------"""
    
        def _get_colors(self, force=False):
            """
            Set the drawing colors.
            :return : Bool
            """
            self.ColScale = 1.0 / 255.0
            if self.IsEnabled() or force:
                self.ColBackground = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellActive = c4d.GetViewColor(
                    c4d.VIEWCOLOR_ACTIVEPOINT) * 0.9
                self.ColCellFocus = self._get_color_vector(c4d.COLOR_BGFOCUS)
                self.ColCellInactive = self._get_color_vector(c4d.COLOR_BGEDIT)
                self.ColEdgeDark = self._get_color_vector(c4d.COLOR_EDGEDK)
                self.ColEdgeLight = self._get_color_vector(c4d.COLOR_EDGELT)
            else:
                self.ColBackground = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellActive = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellFocus = self._get_color_vector(c4d.COLOR_BG)
                self.ColCellInactive = self._get_color_vector(c4d.COLOR_BG)
                self.ColEdgeDark = self._get_color_vector(c4d.COLOR_EDGEDK)
                self.ColEdgeLight = self._get_color_vector(c4d.COLOR_EDGELT)
            return True
    
        def _get_cell_pen(self, state, _is_focus):
            """
            Get the color for cell depending on its state.
            :param state   : The state
            :param _is_focus : If the cell is hoovered.
            :return        : c4d.Vector()
            """
            if state:
                pen = self.ColCellActive
            else:
                pen = self.ColCellInactive
            if self.IsEnabled() and _is_focus:
                return (pen + c4d.Vector(2)) * 1/3
            else:
                return pen
    
        def _draw_cell(self, x, y, state, _is_focus):
            """
            Draws a gadget cell.
            :param x:       local x
            :param y:       local y
            :param state:   On/Off
            :param _is_focus: MouseOver state
            """
            # left and top bright border
            self.DrawSetPen(self.ColEdgeLight)
            self.DrawLine(x, y, x + self.CellWidht, y)
            self.DrawLine(x, y, x, y + self.CellHeight)
            # bottom and right dark border
            self.DrawSetPen(self.ColEdgeDark)
            self.DrawLine(x, y + self.CellHeight - 1, x +
                          self.CellWidht - 1, y + self.CellHeight - 1)
            self.DrawLine(x + self.CellWidht - 1, y, x +
                          self.CellWidht - 1, y + self.CellHeight - 1)
            # cell content
            self.DrawSetPen(self._get_cell_pen(state, _is_focus))
            self.DrawRectangle(x + 1, y + 1, x + self.CellWidht -
                               2, y + self.CellHeight - 2)
    
        def _get_rect(self, pid, offset=1):
            """
            Get the drawing rect for an array id.
            :param pid    : the pattern id
            :param offset : the pixel border offset
            :return       : int, int
            """
            pid = int(pid)
            col = pid / self.CellPerColumn
            head = pid % self.CellPerColumn
            return self.CellWidht * head + offset, self.CellHeight * col + offset
    
        def _get_id(self, x, y):
            """
            Get the array id for a coord within the gadget.
            :param x : local x
            :param y : local y
            :return  : int
            """
            col = (y - 1) / self.CellHeight
            head = (x - 1) / self.CellWidht
            return col * self.CellPerColumn + head
    
        def _is_focus(self, x, y):
            """
            Test if the cell coords are under the cursor.
            :param x : local x
            :param y : local y
            :return  : bool
            """
            if (self.MouseX >= x and self.MouseX <= x + self.CellWidht and
                    self.MouseY >= y and self.MouseY <= y + self.CellHeight):
                self.MouseX = c4d.NOTOK
                self.MouseY = c4d.NOTOK
                return True
            else:
                return False
    
        def _fit_gadget(self):
            """
            Fit the gadget size to the the array
            """
            oldHeight = self.MinHeight
            self.CellPerColumn = int((self.Width - 2) / self.CellWidht)
            self.Columns = math.ceil(
                self.Host.PatternSize / self.CellPerColumn) + 1
            self.MinHeight = int(IDC_SELECTLOOP_GADGET_MINH * self.Columns) + 3
            self.MinWidht = int(IDC_SELECTLOOP_GADGET_MINW)
            self.BorderWidth = self.CellWidht * self.CellPerColumn
            if oldHeight != self.MinHeight:
                self.LayoutChanged()
    
        def _get_color_vector(self, cid):
            """
            Get a color vector from a color ID.
            :param cid : The color ID
            :return    : c4d.Vector()
            """
            dic = self.GetColorRGB(cid)
            if dic:
                return c4d.Vector(float(dic['r']) * self.ColScale,
                                  float(dic['g']) * self.ColScale,
                                  float(dic['b']) * self.ColScale)
            else:
                return c4d.Vector()
    
    if __name__ == "__main__":
        dlg = ExampleDialog()
        dlg.Open(c4d.DLG_TYPE_ASYNC, defaultw=400, defaulth=400)
    
    posted in Cinema 4D SDK •
    RE: Python Generator Mimicking Cloner That Can Modify Individual Cloner Parameters (Except PSR)?

    Hi @bentraje,

    thank you for reaching out to us. As @Cairyn already said, there is little which prevents you from writing your own cloner. Depending on the complexity of the cloner, this can however be quite complex. Just interpolating between two positions or placing objects in a grid array is not too hard. But recreating all the complexity of a MoGraph cloner will be. It is also not necessary. Here are two more straight forward solutions:

    1. As I said in the last thread (the one you did link), MoGraph is perfectly capable of driving arbitrary parameters of an object. I overlooked in the old thread that you did ask what I meant with ""drive the attributes of an object or its material"", so the file cube_bend_mograph.c4d shows you how you can drive the strength parameter of a bend object with MoGraph. The difference I then made was that while you cannot drive such attributes directly via the particle (arrays), but you can let MoGraphs interpolation system take care of it. Please keep in mind that I am everything but an expert for MoGraph. There are probably more elegant solutions for all this, but all this is out of scope for this forum. Please contact technical support or refer to Cineversity for questions about MoGraph.
    2. One problem of MoGraph is that driving more than one arbitrary parameter of an object is a bit tedious. Since MoGraph's sole principle is interpolation and it can only interpolate on a single axis (between parameters), you will then have to start interpolating between two cloners to drive a second, third, fourth, etc. parameter. Here it would make sense to write a custom solution to have less complicated setups. Rather than rewriting a cloner, it would be however more sensible to just modify the cache of a cloner. This will only work when the cloner is in the "Instance Mode" "Instance", as otherwise the cache will only contain the instance wrappers (a bit confusing the naming here ^^). You could do this all inside a GVO, including building the cloner and its cache, but I did provide here a solution which relies on linking a cloner whose cache one wishes to modify. The idea is then simple, walk over all bend objects in the cache and modify their bend strength. You can find a scene file and the code below.

    Cheers,
    Ferdinand

    file: cube_bend_python.c4d
    code:

    """Example for modifying the cache of a node and returning it as the output
    of a generator.
    
    This specific example drives the bend strength of bend objects contained in
    a Mograph cloner object. The example is designed for a Python generator 
    object with a specific set of user data values. Please use the provided c4d
    file if possible.
    
    Note:
        This example makes use of the function `CacheIterator()` for cache 
        iteration which has been proposed on other threads for the task of walking
        a cache, looking for specific nodes. One can pass in one or multiple type
        symbols for the node types to be retrieved from the cache. I did not 
        unpack the topic of caches here any further.
    
        We are aware that robust cache walking can be a complex subject and 
        already did discuss adding such functionality to the SDK toolset in the
        future, but for now users have to do that on their own.
    
    As discussed in:
        plugincafe.maxon.net/topic/13275/
    """
    
    import c4d
    
    # The cookie cutter cache iterator template, can be treated as a black-box,
    # as it has little to do with the threads subject.
    def CacheIterator(op, types=None):
        """An iterator for the elements of a BaseObject cache.
    
        Handles both "normal" and deformed caches and has the capability to 
        filter by node type.
    
        Args:
            op (c4d.BaseObject): The node to walk the cache for.
            types (Union[list, tuple, int, None], optional): A collection of type
             IDs from one of which a yielded node has to be derived from. Will
             yield all node types if None. Defaults to None.
    
        Yields:
            c4d.BaseObject: A cache element of op.
    
        Raises:
            TypeError: On argument type violations.
        """
        if not isinstance(op, c4d.BaseObject):
            msg = "Expected a BaseObject or derived class, got: {0}"
            raise TypeError(msg.format(op.__class__.__name__))
    
        if isinstance(types, int):
            types = (types, )
        if not isinstance(types, (tuple, list, type(None))):
            msg = "Expected a tuple, list or None, got: {0}"
            raise TypeError(msg.format(types.__class__.__name__))
    
        # Try to retrieve the deformed cache of op.
        temp = op.GetDeformCache()
        if temp is not None:
            for obj in CacheIterator(temp, types):
                yield obj
    
        # Try to retrieve the cache of op.
        temp = op.GetCache()
        if temp is not None:
            for obj in CacheIterator(temp, types):
                yield obj
    
        # If op is not a control object.
        if not op.GetBit(c4d.BIT_CONTROLOBJECT):
            # Yield op if it is derived from one of the passed type symbols.
            if types is None or any([op.IsInstanceOf(t) for t in types]):
                yield op
    
        # Walk the hierarchy of the cache.
        temp = op.GetDown()
        while temp:
            for obj in CacheIterator(temp, types):
                yield obj
            temp = temp.GetNext()
    
    
    def main():
        """
        """
        # The user data.
        node = op[c4d.ID_USERDATA, 1]
        angle = op[c4d.ID_USERDATA, 2]
        fieldList = op[c4d.ID_USERDATA, 3]
    
        # Lazy parameter validation ;)
        if None in (node, angle, fieldList):
            raise AttributeError("Non-existent or non-populated user data.")
    
        # Get the cache of the node and clone it (so that we have ownership).
        cache = node.GetDeformCache() or node.GetCache()
        if cache is None:
            return c4d.BaseObject(c4d.Onull)
        clone = cache.GetClone(c4d.COPYFLAGS_NONE)
    
        # Iterate over all bend objects in the cache ...
        for bend in CacheIterator(clone, c4d.Obend):
            # ..., sample the field list for the bend object position, ...
            fieldInput = c4d.modules.mograph.FieldInput([bend.GetMg().off], 1)
            fieldOutput = fieldList.SampleListSimple(op, fieldInput,
                                                     c4d.FIELDSAMPLE_FLAG_VALUE)
            if (not isinstance(fieldOutput, c4d.modules.mograph.FieldOutput) or
                    fieldOutput.GetCount() < 1):
                raise RuntimeError("Error sampling field input.")
            # ... and set the bend strength with that field weight as a multiple 
            # of the angle defined in the user data.
            bend[c4d.DEFORMOBJECT_STRENGTH] = angle * fieldOutput.GetValue(0)
    
        # Return the clone's cache.
        return clone
    
    posted in Cinema 4D SDK •
    RE: Implementing a watermark on render

    Hello @shetal,

    thank you for reaching out to us. The reformulation of your question and the conformance with the forum guidelines on tagging is also much appreciated.

    About your question: As stated in the forum guidelines, we cannot provide full solutions for questions, but provide answers for specific questions. Which is why I will not show here any example code, the first step would have to be made by you. I will instead roughly line out the purpose of and workflow around VideoPostData, which I assume is what you are looking for anyway.

    VideoPostData is derived from NodeData, the base class to implement a node for a classic API scene graph. Node means here 'something that lives inside a document and is an addressable entity', examples for such nodes are materials, shaders, objects, tags, ..., and as such 'video post' node. As mentioned in its class description, VideoPostData is a versatile plugin interface which can be used to intervene a rendering process in multiple ways. The most tangible place for VideoPostData in the app is the render settings where video post plugins can be added as effects for a rendering process as shown below with the built-in water mark video post node.

    12dc3981-c9af-4e3b-80a3-a1e5dabc2a42-image.png

    VideoPostData is an effect, meaning that you cannot use it to invoke a rendering process and on its own it also cannot forcibly add itself to a rendering and must be included manually with the RenderData, the render settings of a rendering. However, a user could make a render setting which includes such watermark effect part of his or her default render settings. One could also implement another plugin interface, SceneHookData, to automatically add such effect to every active document. We would not encourage that though, as this could be confusing or infuriating for users. Finally, such VideoPostData plugin would be visible by default like all NodeData plugins, i.e., it would appear as something in menus that the user can add and interact with. To prevent this if desired, one would have to register the plugin with the flag PLUGINFLAG_HIDE suppress it from popping up in the 'Effect ...' button menu. I cannot tell you with certainty if it is possible to hide programmatically added effect nodes from the users view in the effect list of a render settings. There are some flags which can be used to hide instances of nodes, but I would have to test myself if this also applies in this list, it is more likely that this will not be possible.

    To implement a VideoPostData plugin interface, one can override multiple methods and take different approaches, the most commonly used one is to override VideoPostData::Execute(Link) which will be called multiple times for each rendered frame. The method follows a flag/message logic which is commonly used in Cinema 4D's classic API, where one gets passed in a flag which signalizes in which context the method is being called. Here the context is at which state of the rendering this call is being made, and the chain is:

    • VIDEOPOSTCALL::FRAMESEQUENCE - Series of images starts.
    • VIDEOPOSTCALL::FRAME - Image render starts.
    • VIDEOPOSTCALL::SUBFRAME - Sub-frame starts.
    • VIDEOPOSTCALL::RENDER - Render precalculation.
    • VIDEOPOSTCALL::INNER - Render precalculation.
    • VIDEOPOSTCALL::INNER - Immediately after rendering.
    • VIDEOPOSTCALL::RENDER - Immediately after shader cleanup.
    • VIDEOPOSTCALL::SUBFRAME - Sub-frame rendering done.
    • VIDEOPOSTCALL::FRAME - Frame rendering done.
    • VIDEOPOSTCALL::FRAMESEQUENCE - Complete rendering process finished.

    These flags are accompanied by information if the flags denotes the opening or closing of that 'step' in the rendering process. A developer often then restricts its plugin functionality to a certain flag. I.e., in your case you only want to execute some code when the closing VIDEOPOSTCALL::FRAME is being passed, i.e., after a single frame and all its sub-frames have been rendered. Execute() also passes in a pointer to a VideoPostStruct(Link) which carries information about the ongoing rendering. One of its fields is render, a pointer to a Render(Link). This data structure represents a rendering with multiple buffers and provides the method GetBuffer() which returns a pointer to VPBuffer buffer. In your case you would want to retrieve the RGBA buffer for the rendering by requesting the VPBUFFER_RGBA buffer (Link) with GetBuffer().

    This buffer is then finally the pixel buffer, the bitmap data you want to modify. The buffer is being read and written in a line wise fashion with VPBuffer::GetLine() and ::SetLine(). Here you would have to superimpose your watermark information onto the frame. I would do this in a shader like fashion, i.e., write a function which I can query for a texture coordinate for every pixel/fragment in every line and it will then return an RBGA value which I could then combine with the RGBA information which is in the buffer at that coordinate. The details on that depend on what you want to do, e.g.,

    • Should the watermark be tiled across the frame or just live in a 'corner'?
    • Should it contain alpha information?
    • Can the user influence it, or is it just a png file on disk?
    • etc...

    and the answers to that are mostly algorithmic and not directly connected to our API which limits the amount of support we can provide for them. If this all sounds very confusing to you, it might be helpful to look at our video post examples I did post in the previous thread, e.g., vpreconstructimage.cpp, as this will probably make things less daunting.

    If you decide that you do not want to take this route for technical or complexity reasons, you could write a SceneHookData plugin which listens via NodeData::Message for MSG_MULTI_RENDERNOTIFICATION(Link), a message family which is being sent in the context of a rendering. There you would have to evaluate the start field in the RenderNotificationData(Link) accompanying the message, to determine if the call is for the start or end of a rendering. Then you could grab the rendering output file(s) on disk with the help of the render settings from disk and 'manually' superimpose your watermark information. This will come with the drawback that you might have to deal with compressed video files like mpeg or Avi and all the image formats. Some complexity in that can be hidden away with our BaseBitmap type I did mention in my last posting, but not all of it. There is also the fact that you might run into problems when this plugin runs on a render server, where you cannot easily obtain write or even read access to files of the render output.

    I hope this give you some desired guidance,
    Ferdinand

    posted in Cinema 4D SDK •
    Discovering Channel Identifiers of a Substance Shader

    Dear community,

    The following code example demonstrates how to discover the channel identifiers of the "Channel" parameter of a Substance shader, so that the channel can be changed programmatically for a substance asset unknown at the time of writing the script.

    9b05b0e3-39d3-471b-855f-4c0e1e3dcdea-image.png

    This question reached us via mail, but since answering it requires no confidential data, we are sharing the solution here. The "trick" is to traverse the description of the shader, as these identifiers depend on the substance.

    Cheers,
    Ferdinand

    The result (the example script will randomly select a channel, but with the data provided, channels can also be selected by their name or a substring match as for example "diffuse"):

    substance_channels.gif

    The code:

    """Example for discovering the channels of a Substance shader.
    
    The solution is a bit hacky by traversing the description of the shader but should work.
    """
    
    import c4d
    import random
    
    def GetSubstanceChannels(shader: c4d.BaseShader) -> dict[int:str]:
        """Returns all channels of the substance loaded into #shader as a dictionary of id-label pairs.
        """
        if not isinstance(shader, c4d.BaseShader) or (shader.GetType() != c4d.Xsubstance):
            raise TypeError(f"{shader} is not a substance shader.")
    
        # Get the data for the "Channel" dropdown element from the description of the shader.
        description = shader.GetDescription(c4d.DESCFLAGS_DESC_NONE)
        channelData = description.GetParameter(c4d.SUBSTANCESHADER_CHANNEL)
    
        # Get the elements in the drop down menu.
        elements = channelData[c4d.DESC_CYCLE]
        if not isinstance(elements, c4d.BaseContainer):
            raise RuntimeError(f"Could not access Channel parameter description in {shader}.")
        
        # Pack the data into a dictionary and return it.
        return {id: label for id, label in elements}
    
    def main(doc: c4d.documents.BaseDocument):
        """
        """
        # Get the active material.
        material = doc.GetActiveMaterial()
        if not isinstance(material, c4d.BaseMaterial):
            raise RuntimeError("Please select a material.")
    
        # Get the substance shader loaded into the color channel of the material.
        shader = material[c4d.MATERIAL_COLOR_SHADER]
        channelData = GetSubstanceChannels(shader)
    
        for id, label in channelData.items():
            print (f"id: {id}, label: {label}")
    
        # To select a specific channel, one would have to do a string comparison here to find keywords as
        # "Color" or "Metal" in the channel label. I am just randomly selecting a channel instead.
        channelId = random.choice(tuple(channelData.keys()))
        channelLabel = channelData[channelId]
        print (f"Setting substance to channel '{channelLabel}({channelId})'")
        shader[c4d.SUBSTANCESHADER_CHANNEL] = channelId
    
        c4d.EventAdd()
    
    if __name__=='__main__':
        main(doc)
    
    posted in Cinema 4D SDK •
    RE: Python Plugin GUI Tutorial

    Hello @joel,

    Thank you for reaching out to us. Yeah, the GUI examples for Python are in a bit rough state. I would recommend having a look at the C++ Docs, as they cover more ground.

    • C ++ Resource File Manual.
    • A very basic Python dialog example

    There are in principal two ways to define GUIs in Cinema 4D, dialogs and descriptions (see the C++ Manual for details). Dialogs are primarily used for things like CommandData plugins, i.e., when you need a separate window. NodeData plugins (an object, material, shader, tag, etc.) use description resources to define their GUIs and are displayed in the Attribute Manger.

    I should really find the time to write a new GUI manual both for C++ and Python, we are aware that it is urgent, but I never got to it yet, because it will be quite some undertaking. But I have written a little example for your specific case, in the hopes that it will help you. It goes over some basic and more advanced techniques:

    • How to bind a CommandData plugin and a GeDialog together.
    • What the major overwritable methods do.
    • How GeDialog.CreateLayout works.
    • Using custom GUIs
    • Implementing a (very simple) data model for a dialog.

    Cheers,
    Ferdinand

    The result:
    gui_link.gif

    The code:

    """Implements a CommandData plugin with dialog with a dynamic GUI, using multiple CUSTOMGUI_LINKBOX
    gadgets.
    
    Save this file as "someName.pyp" and target Cinema 4D's plugin directory list to the directory
    containing the file "someName.pyp". The plugin will appear as "Dialog Manager Command" in the
    "Extensions" menu.
    """
    
    import c4d
    import typing
    
    
    class MyDialog(c4d.gui.GeDialog):
        """Implements a dialog with link box gadgets which can be dynamically added and removed at
        runtime.
    
        This also demonstrates how one can put an abstraction layer / data model (or however one wants
        to call such thing) on top of a couple of gadgets, here the link box GUIs.
        """
        # The gadget IDs of the dialog.
    
        # The three groups.
        ID_GRP_MAIN: int = 1000
        ID_GRP_ELEMENTS: int = 1001
        ID_GRP_BUTTONS: int = 1002
    
        # The three buttons at the bottom.
        ID_BTN_ADD: int = 2000
        ID_BTN_REMOVE: int = 2001
        ID_BTN_PRINT: int = 2002
    
        # The dynamic elements. They start at 3000 and then go NAME, LINK, NAME, LINK, ...
        ID_ELEMENTS_START: int = 3000
        ID_ELEMENT_NAME: int = 0
        ID_ELEMENT_LINK: int = 1
    
        # A default layout flag for GUI gadgets and a default gadget spacing.
        DEFAULT_FLAGS: int = c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT
        DEFAULT_SPACE: tuple[int] = (5, 5, 5, 5)
    
        # A settings container for a LinkBoxGui instance, these are all default settings, so we could
        # pass the empty BaseContainer instead with the same effect. But here you can tweak the settings
        # of a custom GUI. Since we want all link boxes to look same, this is done as a class constant.
        LINKBOX_SETTINGS: c4d.BaseContainer = c4d.BaseContainer()
        LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_HIDE_ICON, False)
        LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_LAYERMODE, False)
        LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_NO_PICKER, False)
        LINKBOX_SETTINGS.SetBool(c4d.LINKBOX_NODE_MODE, False)
    
        def __init__(self, items: list[c4d.BaseList2D] = []) -> None:
            """Initializes a MyDialog instance.
    
            Args:
                items (list[c4d.BaseList2D]): The items to init the dialog with.
            """
            super().__init__()
    
            self._items: list[c4d.BaseList2D] = []  # The items linked in the dialog.
            self._doc: typing.Optional[c4d.documents.BaseDocument] = None  # The document of the dialog.
            self._hasCreateLayout: bool = False  # If CrateLayout() has run for the dialog or not.
    
            # Bind the dialog to the passed items.
            self.Items = items
    
        # Our data model, we expose _items as a property, so that we can read and write items from
        # the outside. For basic type gadgets, e.g., string, bool, int, float, etc., there are
        # convenience methods attached to GeDialog like Get/SetString. But there is no GetLink() method.
        # So one must do one of two things:
        #
        #   1. Store all custom GUI gadgets in a list and manually interact with them.
        #   2. Put a little abstraction layer on top of things as I did here.
        #
        # Calling myDialogInstance.Items will always yield all items in the order as shown in the GUI,
        # and calling my myDialogInstance.Items = [a, b, c] will then show them items [a, b, c] in three
        # link boxes in the dialog. No method is really intrinsically better, but I prefer it like this.
        @property
        def Items(self) -> list[c4d.BaseList2D]:
            """gets all items linked in the link boxes.
            """
            return self._items
    
        @Items.setter
        def Items(self, value: list[c4d.BaseList2D]) -> None:
            """Sets all items linked in link boxes.
            """
            if not isinstance(value, list):
                raise TypeError(f"Items: {value}")
    
            # Set the items and get the associated document from the first item.
            self._items = value
            self._doc = value[0].GetDocument() if len(self._items) > 0 else None
    
            # Update the GUI when this setter is being called after CreateLayout() has already run.
            if self._hasCreateLayout:
                self.PopulateDynamicGroup(isUpdate=True)
    
        def InitValues(self) -> bool:
            """Called by Cinema 4D once CreateLayout() has ran.
    
            Not needed in this case.
            """
            return super().InitValues()
    
        def CreateLayout(self) -> bool:
            """Called once by Cinema 4D when a dialog opens to populate the dialog with gadgets.
    
            But one is not bound to adding only items from this method, a dialog can be repopulated
            dynamically.
            """
            self._hasCreateLayout = True
            self.SetTitle("Dialog Manager Command")
    
            # The outmost layout group of the dialog. It has one column and we will only place other
            # groups in it. Items are placed like this:
            #
            #   Main {
            #       a,
            #       b,
            #       c,
            #       ...
            #   }
            #
            self.GroupBegin(id=self.ID_GRP_MAIN, flags=c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=1)
            # Set the group spacing of ID_GRP_MAIN to (5, 5, 5, 5)
            self.GroupBorderSpace(*self.DEFAULT_SPACE)
    
            # An layout group inside #ID_GRP_MAIN, it has two columns and we will place pairs of
            # labels and link boxes in it. The layout is now:
            #
            #   Main {
            #       Elements {
            #           a, b,
            #           c, d,
            #           ... }
            #       b,
            #       c,
            #       ...
            #   }
            #
            self.GroupBegin(id=self.ID_GRP_ELEMENTS, flags=c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, cols=2)
            # Set the group spacing of ID_GRP_ELEMENTS to (5, 5, 5, 5).
            self.GroupBorderSpace(*self.DEFAULT_SPACE)
            # Call our PopulateDynamicGroup() method, here with isUpdate=False, so that group
            # ID_GRP_ELEMENTS won't be flushed the first time its is being built. Doing this is the same
            # as moving all the code from PopulateDynamicGroup() to the line below.
            self.PopulateDynamicGroup(isUpdate=False)
            self.GroupEnd()  # ID_GRP_ELEMENTS
    
            # A second layout group inside ID_GRP_MAIN, its has three columns and will place our buttons
            # in it. The layout is now:
            #
            #   Main {
            #       Elements {
            #           a, b,
            #           c, d,
            #           ... }
            #       Buttons {
            #           a, b, c,
            #           e, f, g,
            #           ...},
            #       c,
            #       ...
            #   }
            #
            self.GroupBegin(id=self.ID_GRP_BUTTONS, flags=c4d.BFH_SCALEFIT, cols=3)
            self.GroupBorderSpace(*self.DEFAULT_SPACE)
            # The three buttons.
            self.AddButton(id=self.ID_BTN_ADD, flags=c4d.BFH_SCALEFIT, name="Add Item")
            self.AddButton(id=self.ID_BTN_REMOVE, flags=c4d.BFH_SCALEFIT, name="Remove Last Item")
            self.AddButton(id=self.ID_BTN_PRINT, flags=c4d.BFH_SCALEFIT, name="Print Items")
            self.GroupEnd()  # ID_GRP_BUTTONS
    
            self.GroupEnd()  # ID_GRP_MAIN
    
            return super().CreateLayout()
    
        def PopulateDynamicGroup(self, isUpdate: bool = False):
            """Builds the dynamic part of the GUI.
    
            This is a custom method that is not a member of GeDialog.
    
            Args:
                isUpdate (bool, optional): If this is an GUI update event. Defaults to False.
    
            Raises:
                MemoryError: On gadget allocation failure.
                RuntimeError: On linking objects failure.
            """
            # When this is an update event, i.e., the group #ID_GRP_ELEMENTS has been populated before,
            # flush the items in the group and set the gadget insertion pointer of the this dialog to
            # the start of #ID_GRP_ELEMENTS. Everything else done in CreateLayout(), the groups, the
            # buttons, the spacings, remains intact.
            if isUpdate:
                self.LayoutFlushGroup(self.ID_GRP_ELEMENTS)
    
            # For each item in self._items ...
            for i, item in enumerate(self.Items):
                # Define the current starting id: 3000, 3002, 3004, 3006, ...
                offset: int = self.ID_ELEMENTS_START + (i * 2)
    
                # Add a static text element containing the class name of #item or "Empty" when the
                # item is None.
                self.AddStaticText(id=offset + self.ID_ELEMENT_NAME,
                                   flags=c4d.BFH_LEFT,
                                   name=item.__class__.__name__ if item else "Empty")
    
                # Add a link box GUI, a custom GUI is added by its gadget ID, its plugin ID, here
                # CUSTOMGUI_LINKBOX, and additionally a settings container, here the constant
                # self.LINKBOX_SETTINGS.
                gui: c4d.gui.LinkBoxGui = self.AddCustomGui(
                    id=offset + self.ID_ELEMENT_LINK,
                    pluginid=c4d.CUSTOMGUI_LINKBOX,
                    name="",
                    flags=c4d.BFH_SCALEFIT,
                    minw=0,
                    minh=0,
                    customdata=self.LINKBOX_SETTINGS)
                if not isinstance(gui, c4d.gui.LinkBoxGui):
                    raise MemoryError("Could not allocate custom GUI.")
    
                # When item is not a BaseList2D, i.e., None, we do not have to set the link.
                if not isinstance(item, c4d.BaseList2D):
                    continue
    
                # Otherwise try to link #item in the link box GUI.
                if not gui.SetLink(item):
                    raise RuntimeError("Failed to set node link from data.")
    
            if isUpdate:
                self.LayoutChanged(self.ID_GRP_ELEMENTS)
    
        def AddEmptyItem(self) -> None:
            """Adds a new empty item to the data model and updates the GUI.
    
            This is a custom method that is not a member of GeDialog.
            """
            self._items.append(None)
            self.PopulateDynamicGroup(isUpdate=True)
    
        def RemoveLastItem(self) -> None:
            """Removes the last item from the data model and updates the GUI.
    
            This is a custom method that is not a member of GeDialog.
            """
            if len(self._items) > 0:
                self._items.pop()
                self.PopulateDynamicGroup(isUpdate=True)
    
        def UpdateItem(self, cid: int):
            """Updates an item in list of link boxes.
    
            This is a custom method that is not a member of GeDialog.
    
            Args:
                cid (int): The gadget ID for which this event was fired (guaranteed to be a link box
                    GUI gadget ID unless I screwed up somewhere :D).
    
            """
            # The index of the link box and therefore index in self._items, e.g., the 0, 1, 2, 3, ...
            # link box GUI / item.
            index: int = int((cid - self.ID_ELEMENTS_START) * 0.5)
    
            # Get the LinkBoxGui associated with the ID #cid.
            gui: c4d.gui.LinkBoxGui = self.FindCustomGui(id=cid, pluginid=c4d.CUSTOMGUI_LINKBOX)
            if not isinstance(gui, c4d.gui.LinkBoxGui):
                raise RuntimeError(f"Could not access link box GUI for gadget id: {cid}")
    
            # Retrieve the item in the link box gui. This can return None, but in this case we are
            # okay with that, as we actually want to reflect in our data model self._items when
            # link box is empty. The second argument to GetLink() is a type filter. We pass here
            # #Tbaselist2d to indicate that we are interested in anything that is a BaseList2D. When
            # would pass Obase (any object), and the user linked a material, the method would return
            # None. If we would pass Ocube, only cube objects would be retrieved.
            item: typing.Optional[c4d.BaseList2D] = gui.GetLink(self._doc, c4d.Tbaselist2d)
    
            # Write the item into our data model and update the GUI.
            self.Items[index] = item
            self.PopulateDynamicGroup(isUpdate=True)
    
        def PrintItems(self) -> None:
            """Prints all items held by the dialog to the console.
    
            This is a custom method that is not a member of GeDialog.
            """
            for item in self.Items:
                print(item)
    
        def Command(self, cid: int, msg: c4d.BaseContainer) -> bool:
            """Called by Cinema 4D when the user interacts with a gadget.
    
            Args:
                cid (int): The id of the gadget which has been interacted with.
                msg (c4d.BaseContainer): The command data, not used here.
    
            Returns:
                bool: Success of the command.
            """
            # You could also put a lot of logic into this method, but for an example it might be better
            # to separate out the actual logic into methods to make things more clear.
    
            # The "Add Item" button has been clicked.
            if cid == self.ID_BTN_ADD:
                self.AddEmptyItem()
            # The "Remove Item" button has been clicked.
            elif cid == self.ID_BTN_REMOVE:
                self.RemoveLastItem()
            # The "Print Items" button has been clicked.
            elif cid == self.ID_BTN_PRINT:
                self.PrintItems()
            # One of the link boxes has received an interaction.
            elif (cid >= self.ID_ELEMENTS_START and (cid - self.ID_ELEMENTS_START) % 2 == self.ID_ELEMENT_LINK):
                self.UpdateItem(cid)
    
            return super().Command(cid, msg)
    
        def CoreMessage(self, cid: int, msg: c4d.BaseContainer) -> bool:
            """Called by Cinema 4D when a core event occurs.
    
            You could use this to automatically update the dialog when the selection state in the 
            document has changed. I did not flesh this one out.
            """
            # When "something" has happened in the, e.g., the selection has changed ...
            # if cid == c4d.EVMSG_CHANGE:
            # items: list[c4d.BaseList2D] = (
            #     self._doc.GetSelection() + self._doc.GetActiveMaterials() if self._doc else [])
    
            # newItems: list[c4d.BaseList2D] = list(n for n in items if n not in self._items)
            # self.Items = self.Items + newItems
    
            return super().CoreMessage(cid, msg)
    
    
    class DialogManagerCommand (c4d.plugins.CommandData):
        """Provides an implementation for a command data plugin with a foldable dialog.
    
        This will appear as the entry "Dialog Manager Command" in the extensions menu.
        """
        ID_PLUGIN: int = 1060264  # The plugin ID of the command plugin.
        REF_DIALOG: typing.Optional[MyDialog] = None  # The dialog hosted by the plugin.
    
        def GetDialog(self, doc: typing.Optional[c4d.documents.BaseDocument] = None) -> MyDialog:
            """Returns a class bound MyDialog instance.
    
            Args:
                doc (typing.Optional[c4d.documents.BaseDocument], optional): The active document. 
                    Defaults to None.
    
            This is a custom method that is not a member of CommandData.
            """
            # Get the union of all selected objects, tags, and materials in #doc or define the empty
            # list when doc is None. Doing it in this form is necessary, because GetState() will call
            # this method before Execute() and we only want to populate the dialog when the user invokes
            # the command.
            items: list[c4d.BaseList2D] = doc.GetSelection() + doc.GetActiveMaterials() if doc else []
    
            # Instantiate a new dialog when there is none.
            if self.REF_DIALOG is None:
                self.REF_DIALOG = MyDialog(items)
            # Update the dialog state when the current document selection state is different. This will
            # kick in when the user selects items, opens the dialog, closes the dialog, and changes the
            # selection. This very much a question of what you want, and one could omit doing this or
            # do it differently.
            elif doc is not None and self.REF_DIALOG.Items != items:
                self.REF_DIALOG.Items = items
    
            # Return the dialog instance.
            return self.REF_DIALOG
    
        def Execute(self, doc: c4d.documents.BaseDocument) -> bool:
            """Folds or unfolds the dialog.
            """
            # Get the dialog bound to this command data plugin type.
            dlg: MyDialog = self.GetDialog(doc)
            # Fold the dialog, i.e., hide it if it is open and unfolded.
            if dlg.IsOpen() and not dlg.GetFolding():
                dlg.SetFolding(True)
            # Open or unfold the dialog.
            else:
                dlg.Open(c4d.DLG_TYPE_ASYNC, self.ID_PLUGIN)
    
            return True
    
        def RestoreLayout(self, secret: any) -> bool:
            """Restores the dialog on layout changes.
            """
            return self.GetDialog().Restore(self.ID_PLUGIN, secret)
    
        def GetState(self, doc: c4d.documents.BaseDocument) -> int:
            """Sets the command icon state of the plugin.
    
            With this you can tint the command icon blue when the dialog is open or grey it out when
            some condition is not met (not done here). You could for example disable the plugin when
            there is nothing selected in a scene, when document is not in polygon editing mode, etc.
            """
            # The icon is never greyed out, the button can always be clicked.
            result: int = c4d.CMD_ENABLED
    
            # Tint the icon blue when the dialog is already open.
            dlg: MyDialog = self.GetDialog()
            if dlg.IsOpen() and not dlg.GetFolding():
                result |= c4d.CMD_VALUE
    
            return result
    
    
    def RegisterDialogManagerCommand() -> bool:
        """Registers the example.
        """
        # Load one of the builtin icons of Cinema 4D as the icon of the plugin, you can browse the
        # builtin icons under:
        #   https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d.bitmaps/RESOURCEIMAGE.html
        bitmap: c4d.bitmaps.BaseBitmap = c4d.bitmaps.InitResourceBitmap(c4d.Tdisplay)
    
        # Register the plugin.
        return c4d.plugins.RegisterCommandPlugin(
            id=DialogManagerCommand.ID_PLUGIN,
            str="Dialog Manager Command",
            info=c4d.PLUGINFLAG_SMALLNODE,
            icon=bitmap,
            help="Opens a dialog with scene element link boxes in it.",
            dat=DialogManagerCommand())
    
    
    # Called by Cinema 4D when this plugin module is loaded.
    if __name__ == '__main__':
        if not RegisterDialogManagerCommand():
            raise RuntimeError(
                f"Failed to register {DialogManagerCommand} plugin.")
    
    posted in Cinema 4D SDK •
    RE: random.seed() vs R21

    Hi,

    I cannot reproduce this neither. The interesting question would be what does print random.seed for you and is this reproduceable on your end?

    My suspicion would be that someone or something treated random.seed() like a property instead of like a function, which then led to - with all the "Python functions are first class objects" thing - random.seed being an integer. Something like this:

    >>> import random
    >>> print(random.seed)
    <bound method Random.seed of <random.Random object at 0x103b62218>>
    >>> random.seed(12345)
    >>> random.seed = 12345 # 'accidently' treating it like a property
    >>> print(random.seed)
    12345
    >>> random.seed(12345)
    Traceback (most recent call last):
      File "<string>", line 1, in <module>
    TypeError: 'int' object is not callable
    

    Cheers
    zipit

    posted in General Talk •
    RE: Best Wishes to the team

    Hi @C4DS and @Motion4D,

    thanks you two, that is very kind of you. A happy new year to you too and everyone in the forum.

    Cheers,
    Ferdinand

    posted in Maxon Announcements •
    RE: Browsing Plugin Paths

    Hi,

    preference data is often even for native c4d features implemented as a PreferenceData plugin. You have to access that plugin then. To get there you can drag and drop description elements into the python command line, delete the __getitem__() part (the stuff in brackets), and get the __repr__ of the object. With that you can figure out the plugin ID of the corresponding BasePlugin and then access your values.

    For your case as a script example:

    import c4d
    from c4d import plugins
    # Welcome to the world of Python
    
    def main():
        # Search for a plugin ID with a known str __repr__ of a BasePlugin. We got from the console:
        # Drag and drop: Plugins[c4d.PREF_PLUGINS_PATHS]
        # >>> Plugins
        #     <c4d.BaseList2D object called 'Plugins/Plugins' with ID 35 at 0x0000028FE8157A50>
        pid, op = None, plugins.GetFirstPlugin()
        while op:
            if 'Plugins/Plugins' in str(op):
                pid = op.GetID()
                break
            op = op.GetNext()
        print "pid:", pid
        
        # Use that ID
        op = plugins.FindPlugin(pid)
        if op:
            print op[c4d.PREF_PLUGINS_PATHS] # we know the enum from the console
    
    # Execute main()
    if __name__=='__main__':
        main()
    

    You can use then that Plugin ID in cpp to do the last part (Use that ID) there.

    Cheers,
    zipit

    posted in Cinema 4D SDK •
    RE: Applying outline selection by script ?

    Hi,

    technically this is possible via c4d.utils.SendModellingCommand(). However, the related specific command ID (c4d.ID_MODELING_OUTLINE_SELECTION_TOOL) is marked as private. So there is no documentation on how to use this properly.

    But it is not to hard to figure out the edges that are border edges. The example below will find all outlines, mimicing c4ds functionality of letting you select a specific loop is just a matter of additional filtering I left out to keep things short.

    import c4d
    # Welcome to the world of Python
    
    
    def set_outline_seleection(op):
        """ Sets the outline edge selection for op.
    
        Args:
            op (c4d.PolygonObject): The object to perform an outline selection on.
        """
        # Return if op is not a polygon object.
        if not isinstance(op, c4d.PolygonObject):
            return
    
        # This is Cinema's version of an edge neighbor data structure
        nbr = c4d.utils.Neighbor()
        nbr.Init(op)
    
        # Get the edge selection for our object.
        selection = op.GetEdgeS()
        selection.DeselectAll()
    
        # Loop over all polygons in our object.
        for pid, cpoly in enumerate(op.GetAllPolygons()):
            # All edge point pair indices of our polygon as a list of tuples with
            # their local edge index.
            edge_point_pairs = [(0, cpoly.a, cpoly.b), (1, cpoly.b, cpoly.c),
                                (2, cpoly.c, cpoly.d), (3, cpoly.d, cpoly.a)]
            # Loop over all edge point pairs in a polygon:
            for eid, a, b in edge_point_pairs:
                # Skip over "empty" triangle edges - c4d presents triangles as four
                # point polygons
                if a == b:
                    continue
                # Test if the current polygon ID is the only ID associated with
                # with the current edge, i. e. if the edge is an outline edge.
                if nbr.GetNeighbor(a, b, pid) == c4d.NOTOK:
                    # Global edge indices in Cinema are indexed as polygon
                    # ID * 4 + edge index in the polygon.
    
                    # Select the edge in our BaseSelect
                    selection.Select(pid * 4 + eid)
        # Update Cinema
        c4d.EventAdd()
    
    
    # Main function
    def main():
        """
        """
        set_outline_seleection(op)
    
    
    # Execute main()
    if __name__ == '__main__':
        main()
    

    Cheers
    zipit

    posted in General Talk •
    About the Upcoming Changes of the Cinema 4D C++ API

    Dear Development Community,

    As we continue to work to provide customers with industry-leading 3D solutions, Maxon will make significant changes to the API of Cinema 4D in its next major release.

    This does not only include the feature additions and minor technical changes you might be accustomed to from past API releases, but also substantial changes to existing parts of the API. These changes are of technical nature and revolve around the const-correctness of the API. Primarily affected are the plugin hooks derived from NodeData and its derived interfaces in the cinema framework.

    :information_source: The Python API is not impacted by these changes as they do not translate into the mutable world of Python.

    All developers who wish to publish their C++ plugins for this future version of Cinema 4D must make significant changes to their C++ code for it to compile. One should expect two to five times the normal workload of updating one's plugins compared to a regular ABI breaking release such as 2023.0.0. We encourage all C++ developers with published plugins who wish to adopt their plugins early to apply for the Maxon Registered Developer (MRD) program, granting early access to future APIs and development pre-documentation. But there will be no information exclusivity associated with the MRD program; non-enrolled developers will receive the same information as enrolled developers once the new API has been released.

    We understand the difficulty in maintaining compatibility as we continue to make ongoing adjustments to the SDK to improve Cinema 4D and wish to assure our customers and development community that these changes are necessary and valuable. Thanks for continuing to enhance the Cinema 4D community.

    Find this announcement on our blog here.

    Happy coding and rendering,
    the Maxon SDK Group

    posted in Maxon Announcements •
    RE: PYTHON - Userdata CTracks and Basecontainer

    Hello,

    here are some answers:

    1. You could use a scripting tag instead of a script to avoid having to deal with the CTRacks at the cost of user interaction.
    2. BaseList2D.GetUserDataContainer() returns a list of DescID and BaseContainer tuples. There is nothing to keyframe there, the BaseContainer only holds the interface settings for that description element. The CTrack and current value of that element are attached to the BaseList2D that does host the user data.
    3. You also do not have to keyframe anything, you can just copy the CTrack and retarget it to the new DescID.

    Here is a script which does what you want. I didn't went overboard with your name matching rules, but the rest should be there.

    """ I broke things into two parts:
     1. get_matches() deals with building a data structure of matching DescID
      elements.
     2. add_ctracks() then does the building of CTracks.
    
     You could probably also streamline some stuff here and there, but I tried
     to be verbose so that things are clear. The script also only deals with
     the currently selected object.
    """
    
    import c4d
    
    
    def get_matches():
        """ Returns a list of tuples of the configuration (source_descid, targets),
        where source_descid is a DescID for which there is a CTrack in op, and
        targets is a list of DescIDs that match source_descid in type and name,
        but there is no CTrack for them in op.
        """
    
        res, user_data_container = [], op.GetUserDataContainer()
    
        """ Step through the user data container of op and find elements (sources)
            for which there is a CTrack in op."""
        for source_descid, source_bc in user_data_container:
            if op.FindCTrack(source_descid) is None:
                continue
            target_descid_list = []
    
            """ Step through the user data container again and find elements
               (targets) for which there is NO CTrack in op and which match the
               current source in type and name."""
            for target_descid, target_bc in user_data_container:
                no_track = op.FindCTrack(target_descid) is None
                if not no_track:
                    continue
                match_name = (source_bc[c4d.DESC_NAME][:-2] ==
                              target_bc[c4d.DESC_NAME][:-2])
                match_type = type(op[source_descid]) == type(op[target_descid])
                is_new = sum(target_descid in data for _, data in res) == 0
    
                if no_track and match_type and match_name and is_new:
                    target_descid_list.append(target_descid)
                res.append((source_descid, target_descid_list))
        return res
    
    
    def add_ctracks(data):
        """ We copy the CTrack for each source DescID the number of target DescID
         which are attached to that source times back into op and set the CTrack 
         DescID each time to the target DescID. 
        """
        for source_did, target_descid_list in data:
            source_ctrack = op.FindCTrack(source_did)
            for target_did in target_descid_list:
                new_ctrack = source_ctrack.GetClone()
                new_ctrack.SetDescriptionID(op, target_did)
                op.InsertTrackSorted(new_ctrack)
    
    
    def main():
        if op is not None:
            data = get_matches()
            add_ctracks(data)
            op.Message(c4d.MSG_UPDATE)
            c4d.EventAdd()
    
    
    # Execute main()
    if __name__ == '__main__':
        main()
    
    
    posted in Cinema 4D SDK •
    RE: Cross platform screencapture code

    Hi,

    I am not familiar with any C++ screen-grab libraries, so I cannot say much.

    • I assume you are aware of the fact that there are multiple C++ implementations on GitHub. While I also would be hesitant to rely on some random github repository, taking a peak into what they are doing on the Mac side of things would not hurt.
    • This probably does not satisfy your criteria of lightweight, but you could use the Python module PIL or its sort of successor pillow. Both are very mature and have a screenshot functionality and are multi-platform. Although the screen-grab stuff does not work on Linux.

    Cheers,
    zipit

    posted in General Talk •
    2024.0.0 SDK Release

    Dear Cinema 4D Community,

    On September the 13th, 2023, Maxon Computer released Cinema 4D 2024.0.0. Alongside this release, a new Cinema 4D SDK and SDK documentation have been released, reflecting the API changes for 2024.0.0. For an overview of the features of Cinema 4D 2024.0, please refer to the fall release announcement. See our 2024.0.0 SDK release posting for details on the API changes.

    Happy rendering and coding,
    the Maxon SDK Team

    posted in Maxon Announcements •
    RE: C4D Crashes on Getting All Specific Type of Objects

    Hi,

    you have to unindent op = op.GetNext() one tab or the while loop condition will always be True unless your whole document consists of spline objects.

    Cheers
    zipit

    posted in Cinema 4D SDK •