Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login
    1. Maxon Developers Forum
    2. ferdinand
    3. Best
    • Profile
    • Following 0
    • Followers 15
    • Topics 53
    • Posts 3,073
    • Best 746
    • Controversial 1
    • Groups 2

    Best posts made by ferdinand

    • How to implement an image control that can receive and generate image data drag events

      Dear community,

      We recently got a non-public support request about handling drag and drop events for a custom image control in a dialog. And while we have shown this, drag event handling, multiple times before, I thought it does not hurt to answer this publicly, as this very case - an image control - is quite common, and it does not hurt having a bit more verbose public example for it.

      edit: I have updated this topic with new code and a new video, to also handle outgoing asset drag events, and talk a bit about the pros and cons of different approaches, and what you could do when not implementing a code example as I did here, and therefore have a bit more leverage room when it comes to complexity.

      In general, doing all this is possible. It is just that not everything is a ready-made solution for you, where you just call a function. At some point you have to "swim" here yourself, as we cannot write your code for you, but I hope the example helps shedding some light on the probably uncessarily complicated drag handling in Cinema 4D.

      Cheers,
      Ferdinand

      Result

      Code

      """Provides an example for implementing a custom control that can generate and receive drag events.
      
      The example implements a dialog which holds multiple "cards" (BitmapCard) that display an image
      and can receive drag events. The user can drag images from the file system into these cards (file
      paths) or drag texture assets from the Asset Browser into these cards. The cards can also generate
      drag events themselves, e.g., when the user drags from a card, it will generate a drag event. 
      Showcased (selectable via the combo box in the menu of the dialog) are three drag types:
      
      1. File paths: Dragging a card will generate an image file path drag event, which for example could 
         be dragged into a file path field of a shader, onto an object in the viewport, or onto other 
         BitmapCard controls. The advantage of this approach is that we do not have to create
         materials or assets, but can just use the file path directly.
      
      2. Materials: Dragging a card will generate an atom drag event, here populated with a Redshift
         material that has the texture of the card as an input. This material can then be dragged onto
         anything that accepts materials, like objects in the viewport. The disadvantage of this
         approach is that we have to create a material, and that we have to exactly define how the 
         material is constructed.
      
      3. Assets: Dragging a card will generate an asset drag event, here populated with a texture asset
         that has the texture of the card as an input. This asset can then be dragged onto anything that
         texture accepts assets, such as an object in the viewport. The disadvantage of this
         approach is that we have to create an asset.
      
         - The example also showcases a variation oof this approach, where we exploit a bit how the
           texture asset drag handling works, to avoid having to create an asset.
      
      
      Overview:
      
      - BitmapCard: A user area control that can receive drag events and generate drag events itself. Here
                    you will find almost all the relevant code for drag and drop handling
      - BitmapStackDialog: A dialog that holds multiple BitmapCard controls and allows the user to select
                      the drag type via a combo box. This dialog is the main entry point of the example
                      but does not contain much relevant code itself.
      """
      __author__ = "Ferdinand Hoppe"
      __copyright__ = "Copyright 2025 Maxon Computer GmbH"
      
      import os
      import re
      
      import c4d
      import maxon
      import mxutils
      
      # The file types that are supported to be dragged into a BitmapCard control.
      DRAG_IN_FILE_TYPES: list[str] = [".png", ".jpg", ".jpeg", ".tif"]
      
      # A regex to check of a string (which is meant to be a path/url) already as a scheme (like file:///
      # or http:///). The protocol we are mostly checking for is "asset:///", but we keep this regex generic
      # to allow for other schemes as well.
      RE_URL_HAS_SCHEME = re.compile("^[a-zA-Z][a-zA-Z\d+\-.]*:\/\/\/")
      
      # A non-public core message which need when we want to properly generate asset drag events.
      COREMSG_SETCOMMANDEDITMODE: int = 300001000
      
      
      class BitmapCard(c4d.gui.GeUserArea):
          """Implements a control that can receive bitmap path drag events and generates drag events itself.
          """
      
          def __init__(self, host: "BitmapStackDialog") -> None:
              """Constructs a new BitmapCard object.
              """
              # The host dialog that holds this card. This is used to access the drag type selected in the
              # combo box of the dialog.
              self._host: BitmapStackDialog = host
      
              # The image file path and the bitmap that is displayed in this card.
              self._path: str | None = ""
              self._bitmap: c4d.bitmaps.BaseBitmap | None = None
      
          def GetMinSize(self) -> tuple[int, int]:
              """Called by Cinema 4d to evaluate the minimum size of the user area.
              """
              return 250, 100
      
          def DrawMsg(self, x1, y1, x2, y2, msg_ref):
              """Called by Cinema 4D to let the user area draw itself.
              """
              self.OffScreenOn()
              self.SetClippingRegion(x1, y1, x2, y2)
      
              # Draw the background and then the bitmap if it exists. In the real world, we would have to
              # either adapt #GetMinSize to the size of the bitmap, or draw the bitmap in a way that it
              # fits into the user area, e.g., by scaling it down. Here we just draw it at a fixed size.
              self.DrawSetPen(c4d.gui.GetGuiWorldColor(c4d.COLOR_BGGADGET))
              self.DrawRectangle(x1, y1, x2, y2)
              if self._bitmap:
                  w, h = self._bitmap.GetSize()
                  self.DrawBitmap(self._bitmap, 5, 5, 240, 90, 0,
                                  0, w, h, c4d.BMP_NORMALSCALED)
      
          def Message(self, msg: c4d.BaseContainer, result: c4d.BaseContainer) -> int:
              """Called by Cinema 4D to handle messages sent to the user area.
      
              Here we implement receiving drag events when the user drags something onto this user area.
              """
              # A drag event is coming in.
              if msg.GetId() == c4d.BFM_DRAGRECEIVE:
                  # Get out when the drag event has been discarded.
                  if msg.GetInt32(c4d.BFM_DRAG_LOST) or msg.GetInt32(c4d.BFM_DRAG_ESC):
                      return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
      
                  # Get out when this is not a drag type we support (we just support images). Note that we
                  # cannot make up drag types ourselves, so when we want to send/receive complex data, we
                  # must use the DRAGTYPE_ATOMARRAY type and pack our data into a BaseContainer attached
                  # to the nodes we drag/send.
                  data: dict = self.GetDragObject(msg)
                  dragType: int = data.get("type", 0)
                  if dragType not in [c4d.DRAGTYPE_FILENAME_IMAGE, maxon.DRAGTYPE_ASSET]:
                      return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
      
                  # Here we could optionally check the drag event hitting some target area.
                  # yPos: float = self.GetDragPosition(msg).get('y', 0.0)
                  # if not 0 < yPos < 50 or not self.CheckDropArea(msg, True, True):
                  #     return self.SetDragDestination(c4d.MOUSE_FORBIDDEN)
      
                  # After this point, we are dealing with a valid drag event.
      
                  # The drag is still going on, we just set the mouse cursor to indicate that we are
                  # ready to receive the drag event.
                  if msg.GetInt32(c4d.BFM_DRAG_FINISHED) == 0:
                      return self.SetDragDestination(c4d.MOUSE_MOVE)
                  # The drag event is finished, we can now process the data.
                  else:
                      # Unpack a file being dragged directly.
                      path: str | None = None
                      if dragType == c4d.DRAGTYPE_FILENAME_IMAGE:
                          path = data.get("object", None)
                      # Unpack an asset being dragged.
                      elif dragType == maxon.DRAGTYPE_ASSET:
                          array: maxon.DragAndDropDataAssetArray = data.get(
                              "object", None)
                          descriptions: tuple[tuple[maxon.AssetDescription, maxon.Url, maxon.String]] = (
                              array.GetAssetDescriptions())
                          if not descriptions:
                              # Invalid asset but we are not in an error state.
                              return True
      
                          # Check that we are dealing with an image asset.
                          asset: maxon.AssetDescription = descriptions[0][0]
                          metadata: maxon.BaseContainer = asset.GetMetaData()
                          subType: maxon.Id = metadata.Get(
                              maxon.ASSETMETADATA.SubType, maxon.Id())
                          if (subType != maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
                              # Invalid asset but we are not in an error state.
                              return True
      
                          path = str(maxon.AssetInterface.GetAssetUrl(asset, True))
      
                      if not isinstance(path, str):
                          return False  # Critical failure.
      
                      if (dragType == c4d.DRAGTYPE_FILENAME_IMAGE and
                          (not os.path.exists(path) or
                           os.path.splitext(path)[1].lower() not in DRAG_IN_FILE_TYPES)):
                          # Invalid file type but we are not in an error state.
                          return True
      
                      self._path = path
                      self._bitmap = c4d.bitmaps.BaseBitmap()
                      if self._bitmap.InitWith(path)[0] != c4d.IMAGERESULT_OK:
                          return False  # Critical failure.
      
                      self.Redraw()  # Redraw the user area to show the new bitmap.
      
                      return True
      
              # Process other messages, doing this is very important, as we otherwise break the
              # message chain.
              return c4d.gui.GeUserArea.Message(self, msg, result)
      
          def InputEvent(self, msg: c4d.BaseContainer) -> bool:
              """Called by Cinema 4D when the user area receives input events.
      
              Here we implement creating drag events when the user drags from this user area. The type of
              drag event which is initiated is determined by the drag type selected in the combo box
              of the dialog.
              """
              # When this is not a left mouse button event on this user area, we just get out without
              # consuming the event (by returning False).
              if (msg.GetInt32(c4d.BFM_INPUT_DEVICE) != c4d.BFM_INPUT_MOUSE or
                      msg.GetInt32(c4d.BFM_INPUT_CHANNEL) != c4d.BFM_INPUT_MOUSELEFT):
                  return False
      
              # Get the type of drag event that should be generated, and handle it.
              dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
              return self.HandleDragEvent(msg, dragType)
          
          # --- Custom drag handling methods -------------------------------------------------------------
      
          # I have split up thing into three methods for readability, this all could also be done directly
          # in the InputEvent method.
      
          def HandleDragEvent(self, event: c4d.BaseContainer, dragType: int) -> bool:
              """Handles starting a drag event by generating the drag data and sending it to the system.
      
              This is called when the user starts dragging from this user area.
              """
              # This requires us to modify the document, so we must be on the main thread (technically
              # not true when the type is DRAGTYPE_FILENAME_IMAGE, but that would be for you to optimize).
              if not c4d.threading.GeIsMainThread():
                  raise False
      
              # Generate our drag data, either a file path, a material, or an asset.
              doc: c4d.documents.BaseDocument = c4d.documents.GetActiveDocument()
              data: object = self.GenerateDragData(doc, dragType)
      
              # Now we set off the drag event. When we are dragging assets, we have to sandwich the event
              # in these core message calls, as otherwise the palette edit mode toggling will not work
              # correctly.
              if dragType == maxon.DRAGTYPE_ASSET:
                  bc: c4d.BaseContainer = c4d.BaseContainer(
                      COREMSG_SETCOMMANDEDITMODE)
                  bc.SetInt32(1, True)
                  c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0)
      
              # When #HandleMouseDrag returns #False, this means that the user has cancelled the drag
              # event, one case could be that the user actually did not intend to drag, but just
              # clicked on the user area.
              #
              # In this case we remove the drag data we generated, as it is not needed anymore. Note that
              # this will NOT catch the case that the drag event is cancelled by the recipient, e.g., the
              # user drags a material onto something that does not accept materials.
              if not self.HandleMouseDrag(event, dragType, data, 0):
                  self.RemoveDragData(doc, data)
                  return True
      
              # Other half of the sandwich, we toggle the palette edit mode back to normal.
              if dragType == maxon.DRAGTYPE_ASSET:
                  bc: c4d.BaseContainer = c4d.BaseContainer(
                      COREMSG_SETCOMMANDEDITMODE)
                  bc.SetInt32(1, False)
                  c4d.SendCoreMessage(c4d.COREMSG_CINEMA, bc, 0)
      
              return True
      
          def GenerateDragData(self, doc: c4d.documents.BaseDocument, dragType
                               ) -> str | c4d.BaseMaterial | maxon.DragAndDropDataAssetArray:
              """Generates the drag data for the given drag type.
      
              Each tile just encapsulates a file path, but we realize dragging them as file paths,
              materials, or assets. So, when the type is material or asset, the drag data must be
              generated from the file path. Which is why we have this method here.
              """
              if not c4d.threading.GeIsMainThread():
                  raise RuntimeError(
                      "GenerateDragData must be called from the main thread.")
      
              # The user has selected "File" as the drag type, we just return the file path.
              if dragType == c4d.DRAGTYPE_FILENAME_IMAGE:
                  return self._path
              # The user has selected "Material" as the drag type, we create a Redshift material with the
              # texture as input and return that material.
              elif dragType == c4d.DRAGTYPE_ATOMARRAY:
                  material: c4d.BaseMaterial = mxutils.CheckType(
                      c4d.BaseMaterial(c4d.Mmaterial))
      
                  # Create a simple graph using that texture, and insert the material into the
                  # active document.
                  maxon.GraphDescription.ApplyDescription(
                      maxon.GraphDescription.GetGraph(
                          material, maxon.NodeSpaceIdentifiers.RedshiftMaterial),
                      {
                          "$type": "Output",
                          "Surface": {
                              "$type": "Standard Material",
                              "Base/Color": {
                                  "$type": "Texture",
                                  # Set the texture. When our source is a file we will just have a plain
                                  # path, e.g. "C:/path/to/file.png" and we have to prefix with a scheme
                                  # (file) to make it a valid URL. When we are dealing with an asset, the
                                  # texture path will already be a valid URL in the "asset:///" scheme.
                                  "Image/Filename/Path": (maxon.Url(f"{self._path}")
                                                          if RE_URL_HAS_SCHEME.match(self._path) else
                                                          maxon.Url(f"file:///{self._path}"))
                              }
                          }
                      })
                  doc.InsertMaterial(material)
                  return [material]
              # The user has selected "Asset" as the drag type, we create a texture asset and return that.
              elif dragType == maxon.DRAGTYPE_ASSET:
                  # So we have to cases here, either that we want truly want to generate an asset drag
                  # event or that we just want to piggy-back onto the texture asset drag and drop
                  # mechanism of Cinema 4D, which is what we do here.
      
                  # Code for truly generating an asset drag event, where we would store the asset in the
                  # scene repository.
                  generateAssets: bool = False
                  if generateAssets:
                      # Get the scene repository and create an asset storage structure for the asset.
                      repo: maxon.AssetRepository = doc.GetSceneRepository(True)
                      store: maxon.StoreAssetStruct = maxon.StoreAssetStruct(
                          maxon.Id(), repo, repo)
      
                      # Now save the texture asset to the repository.
                      url: maxon.Url = maxon.Url(self._path)
                      name: str = url.GetName()
                      asset, _ = maxon.AssetCreationInterface.SaveTextureAsset(
                          url, name, store, (), True)
      
                      # Create a drag array for that asset. It is important that we use the URL of the
                      # asset (maxon.AssetInterface.GetAssetUrl) and not #url or asset.GetUrl(), as both
                      # are physical file paths, and will then cause the drag and drop handling to use
                      # these physical files, including the popup asking for wether the file should be
                      # copied. We will exploit exactly this behavior in the second case.
                      dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray()
                      dragArray.SetLookupRepository(repo)
                      dragArray.SetAssetDescriptions((
                          (asset, maxon.AssetInterface.GetAssetUrl(asset, True), maxon.String(name)),
                      ))
      
                      return dragArray
                  # With this case we can piggy-back onto the existing drag and drop texture asset
                  # handling of Cinema 4D, without actually having to create an asset. THIS IS A
                  # WORKAROUND, we could decide at any moment to change how the drag and drop
                  # handling of assets works, possibly rendering this approach obsolete.
                  else:
                      # The slight disadvantage of this approach (besides it being a hack and us possibly
                      # removing it at some point) is that we have to pay the download cost of
                      # #self._host._dummyAssetId once. I.e., the user has to download that texture once
                      # either by using it in the Asset Browser or by running this code. Once the asset has
                      # been cached, this will not happen again.
                      #
                      # But since we search below in a 'whoever comes first' manner, what is the first
                      # texture asset could change, and this could then be an asset which has not been
                      # downloaded yet. So, in a more robust world we would pick a fixed asset below. But
                      # this comes then with the burden of maintenance, as we could remove one of the
                      # builtin texture assets at any time. So, the super advanced solution would be to
                      # ship the plugin with its own asset database, and mount and use that. Here we could
                      # use a hard-coded asset ID, and would have to have never pay download costs, as
                      # the repository would be local.
      
                      # Find the asset by its Id, which we have stored in the dialog.
                      repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository()
                      dummy: maxon.AssetDescription = repo.FindLatestAsset(
                          maxon.AssetTypes.File().GetId(), self._host._dummyAssetId, maxon.Id(),
                          maxon.ASSET_FIND_MODE.LATEST)
      
                      # Setup the drag array with the asset. Note that #asset must be a valid texture
                      # asset, so just passing maxon.AssetDescription() will not work. But the backend
                      # for drag and drop handling will actually ignore the asset except for type checking
                      # it, and use the URL we provided instead.
                      dragArray: maxon.DragAndDropDataAssetArray = maxon.DragAndDropDataAssetArray()
                      dragArray.SetLookupRepository(repo)
                      dragArray.SetAssetDescriptions((
                          (dummy, maxon.Url(self._path), maxon.String(os.path.basename(self._path))),
                      ))
                  return dragArray
              else:
                  raise ValueError(f"Unsupported drag type: {dragType}")
      
          def RemoveDragData(self, doc: c4d.documents.BaseDocument,
                             data: str | list[c4d.BaseMaterial] | maxon.DragAndDropDataAssetArray) -> bool:
              """Removes generated content when the user cancels a drag event.
      
              Sometimes the user starts a drag event, but then cancels it, e.g., by pressing the
              escape key. In this case, we have to remove the generated content, e.g., the material or
              asset that we created for the drag event.
              """
              if not c4d.threading.GeIsMainThread():
                  return False
      
              if isinstance(data, list):
                  # Remove the dragged materials from the document. Since we are always generating a new
                  # material, we can just remove them. That is in general probably not the best design,
                  # and a real world application should avoid duplicating materials.
                  for item in data:
                      if isinstance(item, c4d.BaseList2D):
                          item.Remove()
              elif isinstance(data, str):
                  # Here we could technically remove a file.
                  pass
              elif isinstance(data, maxon.DragAndDropDataAssetArray):
                  # We could remove the asset, but other than for the material, there is no guarantee
                  # that we are the only user of it, as the Asset API has a duplicate preventing
                  # mechanism. So, the #SaveTextureAsset call above could have returned an already
                  # existing asset. Since we could operate with a document bound repository, we could
                  # search the whole document for asset references and only when we find none, remove
                  # the asset.
                  #
                  # We use here the little hack that we actually do not create an asset, to just to piggy-
                  # back onto the material drag and drop mechanism of assets, so we can just ignore
                  # this case.
                  pass
      
              return True
      
      
      class BitmapStackDialog(c4d.gui.GeDialog):
          """Implements a a simple dialog that stacks multiple BitmapCard controls.
          """
          ID_DRAG_TYPE: int = 1002
      
          def __init__(self):
              """Constructs a new ExampleDialog object.
              """
              # The user area controls that will generate and receive drag events.
              self._cards: list[BitmapCard] = [
                  BitmapCard(host=self) for _ in range(4)]
      
              # The id for the dummy asset that used to generate 'fake' texture asset drag events. We
              # search for it here once, so that we do not have to do it in the __init__ of each
              # BitmapCard, or even worse, in the HandleDragEvent of each BitmapCard.
      
              # We could technically also store here the AssetDescription instead of the Id, but that
              # reference can go stale, so we just store the Id and then grab with it the asset when we
              # need it. Which is much faster than searching asset broadly as we do here. In the end, we
              # could probably also do all this in the drag handling of the BitmapCard, but a little bit
              # of optimization does not hurt.
              self._dummyAssetId: maxon.Id | None = None
              if not maxon.AssetDataBasesInterface.WaitForDatabaseLoading():
                  raise RuntimeError("Could not initialize the asset databases.")
      
              def findTexture(asset: maxon.AssetDescription) -> bool:
                  """Finds the first texture asset in the user preferences repository.
                  """
                  # When this is not a texture asset, we just continue searching.
                  meta: maxon.AssetMetaData = asset.GetMetaData()
                  if (meta.Get(maxon.ASSETMETADATA.SubType, maxon.Id()) !=
                          maxon.ASSETMETADATA.SubType_ENUM_MediaImage):
                      return True
      
                  # When it is a texture asset, we store it as the dummy asset and stop searching.
                  self._dummyAssetId = asset.GetId()
                  return False
      
              # Search for our dummy asset in the user preferences repository.
              repo: maxon.AssetRepositoryInterface = maxon.AssetInterface.GetUserPrefsRepository()
              repo.FindAssets(maxon.AssetTypes.File().GetId(), maxon.Id(), maxon.Id(),
                              maxon.ASSET_FIND_MODE.LATEST, findTexture)
      
          def CreateLayout(self):
              """Called by Cinema 4D to populate the dialog with controls.
              """
              self.SetTitle("Drag and Drop Example")
              self.GroupSpace(5, 5)
              self.GroupBorderSpace(5, 5, 5, 5)
      
              # Build the combo box in the menu bar of the dialog.
              self.GroupBeginInMenuLine()
              self.GroupBegin(1000, c4d.BFH_LEFT | c4d.BFV_TOP, cols=2)
              self.GroupBorderSpace(5, 5, 5, 5)
              self.GroupSpace(5, 5)
              self.AddStaticText(1001, c4d.BFH_LEFT |
                                 c4d.BFV_CENTER, name="Drag Events as:")
              self.AddComboBox(1002, c4d.BFH_RIGHT | c4d.BFV_TOP)
              self.GroupEnd()
              self.GroupEnd()
      
              # Add the BitmapCard controls to the dialog.
              for i, card in enumerate(self._cards):
                  self.AddUserArea(2000 + i, c4d.BFH_LEFT | c4d.BFV_TOP)
                  self.AttachUserArea(card, 2000 + i)
      
              # Add items to the combo box to select the drag type.
              self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE, "Files")
              self.AddChild(self.ID_DRAG_TYPE, c4d.DRAGTYPE_ATOMARRAY, "Materials")
              self.AddChild(self.ID_DRAG_TYPE, maxon.DRAGTYPE_ASSET, "Assets")
              self.SetInt32(self.ID_DRAG_TYPE, c4d.DRAGTYPE_FILENAME_IMAGE)
      
              return True
      
      
      # Define a global variable of the dialog to keep it alive in a Script Manager script. ASYNC dialogs
      # should not be opened in production code from a Script Manager script, as this results in a
      # dangling dialog. Implement a command plugin when you need async dialogs in production.
      dlg: BitmapStackDialog = BitmapStackDialog()
      if __name__ == "__main__":
          dlg.Open(dlgtype=c4d.DLG_TYPE_ASYNC, defaultw=150, defaulth=250)
      
      
      posted in Cinema 4D SDK 2025 python
      ferdinandF
      ferdinand
    • 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 News & Information
      ferdinandF
      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
      ferdinandF
      ferdinand
    • Projecting Points from Object/World Space into Texture Space

      Dear Community,

      this question reached us via email-support in the context of C++, but I thought the answer might be interesting for other users too.

      The underlying question in this case was how to project points from object or world space into the texture space of an object with UV data. I am showing here deliberately an approach that can be followed both in C++ and Python, so that all users can benefit from this. In C++ one has also the option of using VolumeData and its methods VolumeData::GetUvw or VolumeData::ProjectPoint but must then either implement a volume shader (as otherwise the volume data attached to the ChannelData passed to ShaderData::Output will be nullptr), or use VolumeData:: AttachVolumeDataFake to access ::ProjectPoint. There is however no inherent necessity to take this shader bound route as shown by the example.

      Cheers,
      Ferdinand

      Result

      The script has created a texture with red pixels for the intersection points of the rays cast from each vertex of the spline towards the origin of the polygon object. The script also created the null object rays to visualize the rays which have been cast.
      820ac56e-be8c-4e02-adde-62301f1dfd79-image.png

      raycast_texture.c4d : The scene file.

      Code

      ⚠ You must save the script to disk before running it, as the script infers from the script location the place to save the generated texture to.

      """Demonstrates how to project points from world or object space to UV space.
      
      This script assumes that the user has selected a polygon object and a spline object in the order
      mentioned. The script projects the points of the spline object onto the polygon object and creates
      a texture from the UV coordinates of the projected points. The texture is then applied to the
      polygon object.
      
      The script uses the `GeRayCollider` class to find the intersection of rays cast from the points of
      the spline object to the polygon object. The UV coordinates of the intersection points are then
      calculated using the `HairLibrary` class. In the C++ API, one should use maxon::
      GeometryUtilsInterface::CalculatePolygonPointST() instead.
      
      Finally, using GeRayCollider is only an example for projecting points onto the mesh. In practice,
      any other method can be used as long as it provides points that lie in the plane(s) of a polygon.
      
      The meat of the example is in the `main()` function. The other functions are just fluff.
      """
      
      import os
      import c4d
      import mxutils
      import uuid
      
      from mxutils import CheckType
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def CreateTexture(points: list[c4d.Vector], path: str, resolution: int = 1000) -> None:
          """Creates a texture from the given `points` and saves it to the given `path`.
      
          Parameters:
              path (str): The path to save the texture to.
              points (list[c4d.Vector]): The points to create the texture from.
          """
          # Check the input values for validity.
          if os.path.exists(path):
              raise FileExistsError(f"File already exists at path: {path}")
          if not path.endswith(".png"):
              raise ValueError("The path must end with '.png'.")
      
          # Create a drawing canvas to draw the points on.
          canvas: c4d.bitmaps.GeClipMap = CheckType(c4d.bitmaps.GeClipMap())
          if not canvas.Init(resolution, resolution, 24):
              raise MemoryError("Failed to initialize GeClipMap.")
      
          # Fill the canvas with white.
          canvas.BeginDraw()
          canvas.SetColor(255, 255, 255)
          canvas.FillRect(0, 0, resolution, resolution)
      
          # Draw the points on the canvas.
          canvas.SetColor(255, 0, 0)
          for p in points:
              x: int = int(p.x * resolution)
              y: int = int(p.y * resolution)
              x0: int = max(0, x - 1)
              y0: int = max(0, y - 1)
              x1: int = min(resolution, x + 1)
              y1: int = min(resolution, y + 1)
              canvas.FillRect(x0, y0, x1, y1)
      
          canvas.EndDraw()
      
          # Save the canvas to the given path.
          bitmap: c4d.bitmaps.BaseBitmap = CheckType(canvas.GetBitmap())
          bitmap.Save(path, c4d.FILTER_PNG)
      
          c4d.bitmaps.ShowBitmap(bitmap)
      
      def ApplyTexture(obj: c4d.BaseObject, path: str) -> None:
          """Applies the texture at the given `path` to the given `obj`.
          """
          CheckType(obj, c4d.BaseObject)
      
          # Check the input values for validity.
          if not os.path.exists(path):
              raise FileNotFoundError(f"File does not exist at path: {path}")
      
          # Create a material and apply the texture to it.
          material: c4d.BaseMaterial = CheckType(c4d.BaseMaterial(c4d.Mmaterial), c4d.BaseMaterial)
          obj.GetDocument().InsertMaterial(material)
      
          shader: c4d.BaseShader = CheckType(c4d.BaseShader(c4d.Xbitmap), c4d.BaseShader)
          shader[c4d.BITMAPSHADER_FILENAME] = path
          material.InsertShader(shader)
          material[c4d.MATERIAL_COLOR_SHADER] = shader
          material[c4d.MATERIAL_PREVIEWSIZE] = c4d.MATERIAL_PREVIEWSIZE_1024
      
          # Apply the material to the object.
          tag: c4d.TextureTag = CheckType(obj.MakeTag(c4d.Ttexture))
          tag[c4d.TEXTURETAG_PROJECTION] = c4d.TEXTURETAG_PROJECTION_UVW
          tag[c4d.TEXTURETAG_MATERIAL] = material
      
      def CreateDebugRays(spline: c4d.SplineObject, p: c4d.Vector) -> None:
          """Adds spline objects to the document to visualize the rays from the given `p` to the points of
          the given `spline`.
          """
          doc: c4d.documents.BaseDocument = CheckType(spline.GetDocument(), c4d.documents.BaseDocument)
          rays: c4d.BaseObject = c4d.BaseObject(c4d.Onull)
          rays.SetName("Rays")
          doc.InsertObject(rays)
      
          for q in spline.GetAllPoints():
              ray: c4d.SplineObject = c4d.SplineObject(2, c4d.SPLINETYPE_LINEAR)
              ray.SetPoint(0, p)
              ray.SetPoint(1, q * spline.GetMg())
              ray.Message(c4d.MSG_UPDATE)
              ray.InsertUnder(rays)
      
      def main() -> None:
          """Carries out the main logic of the script.
          """
          # Check the object selection for being meaningful input.
          selected: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER)
          if (len(selected) != 2 or not selected[0].CheckType(c4d.Opolygon) or
              not selected[1].CheckType(c4d.Ospline)):
              raise ValueError("Please select a polygon object and a spline object.")
      
          polygonObject, splineObject = selected
      
          # Get the uvw tag, the points, and the polygons of the polygon object.
          uvwTag: c4d.UvwTag = mxutils.CheckType(polygonObject.GetTag(c4d.Tuvw))
          points: list[c4d.Vector] = [polygonObject.GetMg() * p for p in polygonObject.GetAllPoints()]
          polys: list[c4d.CPolygon] = polygonObject.GetAllPolygons()
      
          # We are casting here in a dumb manner towards the center of the polygon object. In practice,
          # one should cast rays towards the plane of the polygon object. Or even better, use another
          # method to project the points onto the polygon object, as GeRayCollider is not the most 
          # efficient thing in the world.
          rayTarget: c4d.Vector = polygonObject.GetMg().off
          CreateDebugRays(splineObject, rayTarget)
      
          # Initialize the GeRayCollider to find the intersection of rays cast from the points of the
          # spline object to the polygon object.
          collider: c4d.utils.GeRayCollider = c4d.utils.GeRayCollider()
          if not collider.Init(polygonObject):
              raise MemoryError("Failed to initialize GeRayCollider.")
      
      
          # Init our output list and iterate over the points of the spline object.
          uvPoints: list[c4d.Vector] = []
          for p in splineObject.GetAllPoints():
      
              # Transform the point from object to world space (q) and then to the polygon object's space
              # (ro). Our ray direction always points towards the center of the polygon object.
              q: c4d.Vector = splineObject.GetMg() * p
              ro: c4d.Vector = ~polygonObject.GetMg() * q
              rd: c4d.Vector = rayTarget - ro
      
              # Cast the ray and check if it intersects with the polygon object.
              if not collider.Intersect(ro, rd, 1E6) or collider.GetIntersectionCount() < 1:
                  continue
              
              # Get the hit position and the polygon ID of the intersection.
              hit: dict = collider.GetNearestIntersection()
              pos: c4d.Vector = mxutils.CheckType(hit.get("hitpos", None), c4d.Vector)
              pid: int = mxutils.CheckType(hit.get("face_id", None), int)
      
              # One mistake would be now to use the barycentric coordinates that are in the intersection
              # data, as Cinema uses an optimized algorithm to interpolate in a quad and not the standard
              # cartesian-barycentric conversion. In Python these polygon weights are only exposed in a 
              # bit weird place, the hair library. In C++ these barycentric coordinates make sense because
              # there exist methods to convert them to weights. In Python the barycentric coordinates are
              # pretty much useless as we do not have such a conversion function here.
      
              # Compute the weights s, t for the intersection point in the polygon.
              s, t = c4d.modules.hair.HairLibrary().GetPolyPointST(
                  pos, points[polys[pid].a], points[polys[pid].b],
                       points[polys[pid].c], points[polys[pid].d], True)
      
              # Get the uv polygon and bilinearly interpolate the coordinates using the weights. It would
              # be better to use the more low-level variable tag data access functions in VariableTag 
              # than UvwTag.GetSlow() in a real-world scenario.
              uvw: list[c4d.Vector] = list(uvwTag.GetSlow(pid).values())
              t0: c4d.Vector = c4d.utils.MixVec(uvw[0], uvw[1], s)
              t1: c4d.Vector = c4d.utils.MixVec(uvw[3], uvw[2], s)
              uv: c4d.Vector = c4d.utils.MixVec(t0, t1, t)
      
              # Append the UV coordinates to the output list.
              uvPoints.append(uv)
      
          # Write the UV coordinates to a texture and apply it to the polygon object.
          path: str = os.path.join(os.path.dirname(__file__), f"image-{uuid.uuid4()}.png")
          CreateTexture(uvPoints, path, resolution=1024)
          ApplyTexture(polygonObject, path)
      
          c4d.EventAdd()
      
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK 2024 python
      ferdinandF
      ferdinand
    • 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
      ferdinandF
      ferdinand
    • Forum and Documentation Maintenance on the 18th and 22nd

      Dear community,

      We will have to touch multiple parts of developers.maxon.net on the 18.01.2024 and 19.01.2024 22.01.2024. This will result in outages of our documentation and the forum these days. I will try to keep the outage times to a minimum and it will certainly not span the whole two days. But especially one task I will do on Friday might take hours to complete and I can only do that on a forum which is in maintenance mode.

      Please make sure to download a recent offline documentation in case you plan to do extended development work the next two days. As a result, forum support might also be delayed on these days.

      Cheers,
      Ferdinand

      posted in News & Information forum news
      ferdinandF
      ferdinand
    • 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
      ferdinandF
      ferdinand
    • 2025.0.0 SDK Release

      Dear development community,

      On September the 10th, 2024, Maxon Computer released Cinema 4D 2025.0.0. For an overview of the new features of Cinema 4D 2025.0, please refer to the release announcement. Alongside this release, a new Cinema 4D SDK and SDK documentation have been released, reflecting the API changes for 2025.0.0. The major changes are:

      C++ API

      • What was formerly has been know as the Classic API has been deprecated in favour of the Cinema API. Alongside this a new cinema namespace has been introduced which contains all the entities which were formerly in the anonymous global namespace known as the Classic API. Plugin authors must adopt their code to this new API, although the changes are not nearly as extensive as for 2024. See the 2025 migration guide for details. Code examples and documentation have been updated to now refer to a Cinema API.
      • 2025 uses OCIO as the default color management mode, brings an improved color picker, and made general improvements to the consistency of the OCIO implementation. This had some effects on the underlying OCIO API which are reflected in two new code examples in the OCIO Manual and a new plugin in the SDK.

      Python API

      • Python also received the update from Classic to Cinema API. But here the change was more of a cosmetic nature confined to the documentation. The c4d package remains the home for all formerly Classic and now Cinema API entities.
      • The mxutils package received updates around standardized scene traversal, random number generation, and more.
      • Graph descriptions now support variadic ports of arbitrary complexity and explicit port references.

      Head to our download section for the newest SDK downloads, or the C++ and Python API change notes for an in detail overview of the changes.

      ⚠ We discovered late in the cycle bugs in the Asset API code examples and OCIO code in the Python SDK. Which is why the publication of the Python SDK and GitHub code examples has been postponed until these bugs are fixed. They should be ready latest by Friday the 13th of September. But the Python online documentation is accessible and error free (to our knowledge).

      ⚠ We had to make some last minute changes to the C++ SDK regarding OCIO code examples. Only the extended C++ SDK contains these changes. The application provided sdk.zip will catch up with the next release of Cinema 4D.

      Happy rendering and coding,
      the Maxon SDK Team

      ℹ Cloudflare unfortunately still does interfere with our server cache. And you might have to refresh your cache manually.

      When you are not automatically redirected to the new versions, and also do not see 2024.5 in the version selector, please press CTRL + F5 or press CTRL and click on the reload icon of your browser anywhere on developers.maxon.net/docs/ to refresh your cache. You only have to do this once and it will apply to all documentations at once. Otherwise your cache will automatically update latest by 19/07/2024 00:00.

      posted in News & Information cinema 4d news c++ python sdk
      ferdinandF
      ferdinand
    • 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
      ferdinandF
      ferdinand
    • 2025.3.0 SDK Release

      Dear development community,

      On June the 18th, 2025, Maxon Computer released Cinema 4D 2025.3.0. For an overview of the new features of Cinema 4D 2025.3.0, please refer to the 2025.3.0 release notes. Alongside this release, a new Cinema 4D SDK and SDK documentation have been released, reflecting the API changes for 2025.3.0. The major changes are:

      C++ API

      • After the introduction of the CMake build system generator in Cinema 4D 2025.2.0, the C++ SDK now only supports CMake as its build system generator. The Legacy Build System is no longer supported. When you have not yet switched to CMake, please refer to our Build Systems manual for more information on how to switch to CMake.
      • Updated the required Windows SDK version to Windows 10 SDK 10.0.20348.0. You might have to update Visual Studio and install the SDK via the Visual Studio installer app.

      Python API

      ⚠ In 2025.3.0, there is a critical issue with c4dpy that will cause it to halt indefinitely when run for the first time. See the c4dpy manual for an explanation and workaround. This issue will be fixed with a hotfix for 2025.3.0.

      • Added features to c4d.documents.BatchRender class, to have more control over render settings, cameras, and takes for a given job.
      • Added a suite of new code examples around the subject of dialogs, including both simple beginner examples, as well as more complex examples covering subjects such as dynamic GUIs, value and layout persistence, and using resources to define dialogs and string translations. The new examples all begin with the prefix py-cmd_gui_, see our plugin examples overview for details.

      Head to our download section to grab the newest SDK downloads, or read the C++ or Python API change notes for an in detail overview of the changes.

      Happy rendering and coding,
      the Maxon SDK Team

      ℹ Cloudflare unfortunately still does interfere with our server cache. You might have to refresh your cache manually to see new data when you read this posting within 24 hours of its release.

      posted in News & Information news cinema 4d c++ python sdk information
      ferdinandF
      ferdinand
    • 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
      ferdinandF
      ferdinand
    • 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
      ferdinandF
      ferdinand
    • RE: Getting poly normal and CreatePhongNormals()

      Hi,

      a bilinear interpolation is quite straight forward. If you have the quadrilateral Q with the the points,

      c---d
      |   |
      a---b
      

      then the bilinear interpolation is just,

      ab = lerp(a, b, t0)
      cd = lerp(c, d, t0)
      res = lerp(ab, cd, t1)
      

      where t0, t1 are the interpolation offset(s), i.e. the texture coordinates in your case (the ordering/orientation of the quad is obviously not set in stone). I am not quite sure what you do when rendering normals, but when you render a color gradient, in a value noise for example, you actually want to avoid linear interpolation, because it will give you these ugly star-patterns. So you might need something like a bi-quadratic, bi-cubic or bi-cosine interpolation, i.e. pre-interpolate your interpolation offsets.

      If I am not overlooking something, this should also work for triangles when you treat them as quasi-quadrilaterals like Cinema does in its polygon type.

      Cheers,
      zipit

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • 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
      ferdinandF
      ferdinand
    • 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
      ferdinandF
      ferdinand
    • 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 python r25
      ferdinandF
      ferdinand
    • 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/py/2023_2/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
      ferdinandF
      ferdinand
    • RE: Building menus with C4DPL_BUILDMENU in S26+

      Dear Community,

      this is our answer 🙂

      1. Why is c4d.C4DPL_BUILDMENU fired 4 times, instead of once?

      In general, we do not make any promises regarding messages being emitted only once or things not being None \ nullptr. When there are any guarantees, the message or function description will explicitly say so. The recent change in behavior is caused by the menu being dynamically rebuilt by Cinema 4D.

      1. If we react to the first pluginMessage only (→ same time in startup sequence), why is R26 behaving differently?

      I am not quite sure how you mean that, what do you mean by first? Are you using a module attribute like, for example, didBuiltMenu? Your code did not show this. You could use a module attribute, and this would mostly work, as *.pyp plugin modules are persistent, but reloading the Python plugins will throw a wrench into things. I personally would simply check for the existence of a menu item by title.

      1. Is there something like executedDelayed(), so that we can execute Python code at the very end of the startup sequence, after the UI initialization has been finished?

      There are multiple phases in the lifecycle of a Cinema 4D instance, they are all documented under PluginMessage. When you want to update the menu outside of C4DPL_BUILDMENU, you must call c4d.gui.UpdateMenus() after the changes.

      But this all seems a bit too complicated for the task IMHO. Simply search for the menu entry you want to add, e. g., My Menu, and stop operations when it already does exist. You could also make this more complicated and index based, so that you can distinguish two menus called My Menu. You could also flush and update existing menus, in the end, menus are just an instance of c4d.BaseContainer. You can more or less do what you want.

      Find a simple example for your problem below.

      Cheers,
      Ferdinand

      Result:
      Screenshot 2022-11-23 at 14.55.19.png

      The code (must be saved as a pyp file):

      """Demonstrates adding and searching menu entries.
      """
      
      import c4d
      import typing
      
      # A type alias for a menu data type used by the example, it is just easier to define a menu as
      # JSON/a dict, than having to write lengthy code.
      MenuData: typing.Type = dict[str, typing.Union[int, 'MenuData']]
      
      # Define the menu which should be inserted. The data does not carry a title for the root of the
      # menu, it will be defined by the #UpdateMenu call. All dictionaries are expanded (recursively) 
      # into sub-menus and all integer values will become commands. The keys for commands do not matter
      # in the sense that Cinema 4D will determine the label of a menu entry, but they are of course 
      # required for the dictionary. For older version of Cinema 4D, you will have to use #OrderedDict,
      # as ordered data for #dict is a more recent feature of Python (3.7 if I remember correctly).
      MENU_DATA: MenuData = {
          "Objects": {
              "0": c4d.Ocube,
              "1": c4d.Osphere,
              "Splines": {
                  "0": c4d.Osplinecircle,
                  "1": c4d.Osplinerectangle
              }
          },
          "0": 13957, # Clear console command
          # "1": 12345678 # Your plugin command
      }
      
      def UpdateMenu(root: c4d.BaseContainer, title: str, data: MenuData, forceUpdate: bool = False) -> bool:
          """Adds #data to #root under a menu entry called #title when there is not yet a menu entry 
          called #title.
      
          When #forceUpdate is true, the menu of Cinema 4D will be forced to update to the new state 
          outside of C4DPL_BUILDMENU.
          """
          def doesContain(root: c4d.BaseContainer, title: str) -> bool:
              """Tests for the existence of direct sub containers with #title in #root.
      
              One could also use #c4d.gui.SearchMenuResource, this serves more as a starting point if one
              wants to customize this (search for a specific entry among multiple with the same title,
              flushing an entry, etc.).
              """
              # BaseContainer can be iterated like dict.items(), i.e., it yields keys and values.
              for _, value in root:
                  if not isinstance(value, c4d.BaseContainer):
                      continue
                  elif value.GetString(c4d.MENURESOURCE_SUBTITLE) == title:
                      return True
              
              return False
          
          def insert(root: c4d.BaseContainer, title: str, data: MenuData) -> c4d.BaseContainer:
              """Inserts #data recursively under #root under the entry #title.
              """
              # Create a new container and set its title.
              subMenu: c4d.BaseContainer = c4d.BaseContainer()
              subMenu.InsData(c4d.MENURESOURCE_SUBTITLE, title)
      
              # Iterate over the values in data, insert commands, and recurse for dictionaries.
              for key, value in data.items():
                  if isinstance(value, dict):
                      subMenu = insert(subMenu, key, value)
                  elif isinstance(value, int):
                      subMenu.InsData(c4d.MENURESOURCE_COMMAND, f"PLUGIN_CMD_{value}")
              
              root.InsData(c4d.MENURESOURCE_SUBMENU, subMenu)
              return root
      
          # #title is already contained in root, we get out. You could also fashion the function so
          # that is clears out an existing entry instead of just reporting its existence, but I did not
          # do that here.
          if doesContain(root, title):
              return False
      
          # Update #root and force a menu update when so indicated by the user.
          insert(root, title, data)
          if forceUpdate and c4d.threading.GeIsMainThreadAndNoDrawThread():
              c4d.gui.UpdateMenus()
      
          return True
      
      
      def PluginMessage(mid: int, data: typing.Any) -> bool:
          """Updates the menu with some menu data only once when C4DPL_BUILDMENU is emitted.
          """
          if mid == c4d.C4DPL_BUILDMENU:
              # Get the whole menu of Cinema 4D an insert #MENU_DATA under a new entry "MyMenu"
              menu: c4d.BaseContainer = c4d.gui.GetMenuResource("M_EDITOR")
              UpdateMenu(root=menu, title="MyMenu", data=MENU_DATA)
      
      def SomeFunction():
          """Can be called at any point to update the menu, as long as the call comes from the main
          thread (and is not a drawing thread).
          """
          # Get the whole menu of Cinema 4D an insert #MENU_DATA under a new entry "MyMenu"
          menu: c4d.BaseContainer = c4d.gui.GetMenuResource("M_EDITOR")
          UpdateMenu(root=menu, title="MyMenu", data=MENU_DATA, forceUpdate=True)
      
      if __name__ == "__main__":
          pass
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How to get the bounding box for the whole scene

      Hello @sawyerwolf19,

      Thank you for reaching out to us and please excuse the slight delay.

      Cinema 4D offers bounding box computation via BaseObject.GetRad() and BaseObject.GetMp(). GetMp returns here the offset of the midpoint of the bounding from the origin of the object (usually the null vector for primitive generators) and GetRad the distance from the midpoint to the boundary of the bounding box on each principal axis.A BaseObject will always express its bounding box in its local object space. And a BaseObject will also not include its descendants in its bounding box computation. A null object will for example always return (0, 0, 0) for its radius and midpoint.

      So, one must carry out such computations oneself, but iterating over points is not necessary, one can reuse the mentioned BaseObject methods. Your question was slightly ambiguous, as you did not express in which space you want the cumulative bounding boxes to be expressed: in the product of the spaces of all contained objects or in world space.

      I have provided a solution for world space below, as I assumed this to be more likely to be what you want.

      Cheers,
      Ferdinand

      Result:
      bounding_box.gif

      Code

      """Demonstrates building bounding boxes in world space.
      
      Must be run as a Script Manger script. Will build a bounding box for all objects in a scene and
      one for the current object selection.
      """
      
      import c4d
      import typing
      import sys
      
      op: typing.Optional[c4d.BaseObject]  # The active object, can be `None`.
      doc: c4d.documents.BaseDocument  # The active document.
      
      class BoundingBox:
          """Represents a bounding box in the world space of a document.
      
          Other than for BaseObject.GetRad(), this bounding box will not be aligned with the local 
          transform of an object. Instead, the orientation of BoundingBox will always be equal to the
          identity matrix, i.e., world space.
          """
          def __init__(self) -> None:
              """Initializes the bounding box.
              """
              self.min: c4d.Vector = c4d.Vector(sys.float_info.max)
              self.max: c4d.Vector = c4d.Vector(-sys.float_info.max)
      
          def __repr__(self) -> str:
              """Returns a string representation of the bounding box.
              """
              m, r = self.GetMidpoint(), self.GetRadius()
              m, r = ((round(m.x, 3), round(m.y, 3), round(m.z, 3)), 
                      (round(r.x, 3), round(r.y, 3), round(r.z, 3)))
              return (f"{self.__class__.__name__} with the midpoint {m} and radius {r}.")
      
          def AddPoint(self, p: c4d.Vector) -> None:
              """Adds the point #p to the bounding box.
      
              #p must be in global space and the bounding box will be updated so that it includes #p.
              """
              if p.x < self.min.x:
                  self.min.x = p.x
              if p.y < self.min.y:
                  self.min.y = p.y
              if p.z < self.min.z:
                  self.min.z = p.z
              if p.x > self.max.x:
                  self.max.x = p.x
              if p.y > self.max.y:
                  self.max.y = p.y
              if p.z > self.max.z:
                  self.max.z = p.z
      
          def AddObject(self, node: c4d.BaseObject) -> None:
              """Adds the object #node and all its descendants to the bounding box.
      
              The bounding box will be updated so that it encompasses #node and its descendants.
              """
              transform: c4d.Matrix = node.GetMg()
              midpoint: c4d.Vector = node.GetMp()
              radius: c4d.Vector = node.GetRad()
              self.AddPoint(transform * (midpoint + c4d.Vector(+radius.x, +radius.y, +radius.z)))
              self.AddPoint(transform * (midpoint + c4d.Vector(-radius.x, +radius.y, +radius.z)))
              self.AddPoint(transform * (midpoint + c4d.Vector(+radius.x, -radius.y, +radius.z)))
              self.AddPoint(transform * (midpoint + c4d.Vector(+radius.x, +radius.y, -radius.z)))
              self.AddPoint(transform * (midpoint + c4d.Vector(-radius.x, -radius.y, +radius.z)))
              self.AddPoint(transform * (midpoint + c4d.Vector(-radius.x, +radius.y, -radius.z)))
              self.AddPoint(transform * (midpoint + c4d.Vector(+radius.x, -radius.y, -radius.z)))
              self.AddPoint(transform * (midpoint + c4d.Vector(-radius.x, -radius.y, -radius.z)))
      
              for child in node.GetChildren():
                  box = BoundingBox()
                  box.AddObject(child)
                  childMidpoint: c4d.Vector  = box.GetMidpoint()
                  childRadius: c4d.Vector  = box.GetRadius()
                  self.AddPoint(childMidpoint + c4d.Vector(+childRadius.x, +childRadius.y, +childRadius.z))
                  self.AddPoint(childMidpoint + c4d.Vector(-childRadius.x, +childRadius.y, +childRadius.z))
                  self.AddPoint(childMidpoint + c4d.Vector(+childRadius.x, -childRadius.y, +childRadius.z))
                  self.AddPoint(childMidpoint + c4d.Vector(+childRadius.x, +childRadius.y, -childRadius.z))
                  self.AddPoint(childMidpoint + c4d.Vector(-childRadius.x, -childRadius.y, +childRadius.z))
                  self.AddPoint(childMidpoint + c4d.Vector(-childRadius.x, +childRadius.y, -childRadius.z))
                  self.AddPoint(childMidpoint + c4d.Vector(+childRadius.x, -childRadius.y, -childRadius.z))
                  self.AddPoint(childMidpoint + c4d.Vector(-childRadius.x, -childRadius.y, -childRadius.z))
      
          def GetMidpoint(self) -> c4d.Vector:
              """Returns the midpoint of the bounding box in global space.
              """
              return (self.min + self.max) * 0.5  
      
          def GetRadius(self) -> c4d.Vector:
              """Returns the distance from the midpoint of the bounding box to its boundary on each axis.
              """
              return self.max - self.GetMidpoint()
      
          def GetCubeObject(self, name: typing.Optional[str] = None) -> c4d.BaseObject:
              """Returns a cube generator object representation of this bounding box.
              """
              cube: c4d.BaseObject = c4d.BaseObject(c4d.Ocube)
              if not cube:
                  raise MemoryError(f"{cube = }")
      
              tag: c4d.BaseTag = cube.MakeTag(c4d.Tdisplay)
              if not tag:
                  raise MemoryError(f"{tag = }")
      
              cube.SetMg(c4d.Matrix(off=self.GetMidpoint()))
              cube[c4d.PRIM_CUBE_LEN] = self.GetRadius() * 2
              tag[c4d.DISPLAYTAG_AFFECT_DISPLAYMODE] = True
              tag[c4d.DISPLAYTAG_SDISPLAYMODE] = c4d.DISPLAYTAG_SDISPLAY_NOSHADING
              if isinstance(name, str):
                  cube.SetName(name)
      
              return cube
      
      
      def main() -> None:
          """Runs the example.
          """
          # Build the bounding box for the whole document by adding each top level object to the box.
          obj: c4d.BaseObject = doc.GetFirstObject()
          if obj is None:
              return
      
          box: BoundingBox = BoundingBox()
          while obj:
              box.AddObject(obj)
              obj = obj.GetNext()
      
          doc.InsertObject(box.GetCubeObject("document_box"))
          print(f"document: {box}")
      
          # Build a bounding box just for the active selection.
          selection: list[c4d.BaseObject] = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE)
          if len(selection) < 1:
              c4d.EventAdd()
              return
      
          box: BoundingBox = BoundingBox()
          for obj in selection:
              box.AddObject(obj)
      
          doc.InsertObject(box.GetCubeObject("selection_box"))
          print(f"selection: {box}")
          c4d.EventAdd()
      
      if __name__ == "__main__":
          main()
      

      edit: fixed upper boundary bug

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • How to Add Enum Values to a Node Attribute?

      Dear Community,

      This question reached us via mail and since answering it does not require confidential information and since I thought others find this interesting too, I am sharing my answer here. The question was How to add enum values dynamically to a node attribute that uses an Enum GUI?".

      Find my answer below.

      Cheers,
      Ferdinand

      Result:
      enum_values.gif

      Code:

      #include "c4d_basematerial.h"
      #include "maxon/datadescription_nodes.h"
      #include "maxon/graph.h"
      #include "maxon/graph_helper.h"
      #include "maxon/nodesgraph.h"
      #include "maxon/nodesystem.h"
      
      namespace maxon 
      {
        /// @brief Demonstrates how to modify the enum values of an attribute.
        /// @details One place where one could do this is inside the #::InstantiateImpl method of a 
        /// #NodeTemplateInterface one is implementing so that every new node is being populated with
        /// enum values of our liking. But as demonstrated by the example below, nothing prevents us
        /// from doing the same thing at runtime on a graph.
        Result<void> AddEnumValues(BaseDocument* doc)
        {
          iferr_scope;
          
          // Get the active material's node graph and start a transaction.
          NodeMaterial* const material = static_cast<NodeMaterial*>(doc->GetActiveMaterial());
          CheckArgument(material);
          nodes::NodesGraphModelRef graph = material->GetGraph(GetActiveNodeSpaceId()) iferr_return;
      
          GraphTransaction transaction = graph.BeginTransaction() iferr_return;
          {
            // Get the mutable root for #graph. This way is not only shorter than first getting the
            // mutable node system for #graph and then its mutable root, but also the only way that
            // actually works here. We can do this because starting a transaction on a graph model also
            // implies modifying the node system. So, we do not have to call NodeSystem::BeginModification
            // in this case.
            nodes::MutableNode root = nodes::ToMutableNode(graph.GetRoot()) iferr_return;
      
            // Iterate over all children of the root to get hold of nodes which have our port.
            for (auto node : root.GetChildren())
            {
              // Attempt to get hold of our enum port.
              nodes::MutablePort enumPort = node.GetInputs().FindPort(
                Id("in@eSp1K8T8GNcuPwKSds8Lvs")) iferr_return;
      
              // We could also add a non-existing port here with MutableNode::AddPort
              if (!enumPort || !enumPort.IsValid())
                continue;
      
              // NodeTemplateInterface::InstantiateImpl code would start here.
      
              // Set the data type and label of the port, doing this is obviously optional.
              enumPort.SetType<String>() iferr_return;
              enumPort.SetValue(NODE::BASE::NAME, "My Enumeration"_s) iferr_return;
      
              // Build the enum data and write it into the port.
              BaseArray<Tuple<Id, Data>> entries;
              for (const Int32 i : {1, 2, 3, 4, 5})
              {
                const String data = FormatString("Item @", i);
                const Id id = Id::Create(label) iferr_return;
                entries.Append(Tuple<Id, Data>(id, data)) iferr_return;
              }
      
              DataDictionary enumPortData;
              enumPortData.Set(DESCRIPTION::DATA::BASE::ENUM, entries) iferr_return;
              enumPort.SetValue(nodes::PortDescriptionData, std::move(enumPortData)) iferr_return;
      
              // And set the default value of the port.
              enumPort.SetDefaultValue("Item 1"_s) iferr_return;
            }
          // Commit the transaction and with it the node system modification.
          } transaction.Commit() iferr_return;
      
          return OK;
        }
      }
      
      posted in Cinema 4D SDK c++ 2023
      ferdinandF
      ferdinand