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. Posts
    • Profile
    • Following 0
    • Followers 16
    • Topics 52
    • Posts 3,027
    • Best 739
    • Controversial 1
    • Groups 2

    Posts made by ferdinand

    • RE: Getting an effective value of an enum from a GraphNode

      Hey @Dunhou ,

      That is a good question, although slightly off-topic. What I used there is called trailing return type. It was introduced in C++11, specifically in the context of lambda expressions. The syntax allows you to specify the return type after the function parameters, which can be useful in certain situations, especially when dealing with complex types or when the return type depends on template parameters. It is functionally identical to the leading style. Python is deeply related to C and C++, and many of its conventions, concepts, and features are rooted in C/C++ programming practices.

      I just went here instinctively for this notation, as it is somewhat common to use it when you talk about a function in terms of its signature, rather than its concrete implementation. It is not a requirement, and you can write the function both ways, we in fact - except for lambda expressions - do not use this notation in our codebase.

      
      // This is the same as
      Sum1(const int a, const int b) -> int  { return a + b; }
      
      // as this (except for the name so that they can coexist).
      int Sum2(const int a, const int b) { return a + b; }
      
      // But we need the trailing notation for lambdas, of which modern Maxon API Cinema 4D makes heavy use.
      void Foo() {
          auto add = [](const int a, const int b) -> int { return a + b; };
          cout << add(1, 2) << std::endl;
      }
      

      So, to recap, we recommend to use Result<T> as the return type for everything that comes into contact with Maxon API error handling, so that you can propagate errors. We do not make any strong recommendations regarding return type conventions. We slightly recommend the traditional leading return type notation, but it is not a requirement.

      Cheers
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Getting an effective value of an enum from a GraphNode

      Hey @ECHekman,

      The value of a port should be accessed with GraphNodeFunctions.GetPortValue or GraphNodeFunctions::GetEffectivePortValue. Usually, the former is fine, and only for some specialty cases where the actual port data does not reflect what the user sees, and you want to access the user facing value, you have to use the latter.

      What you are doing there, is the old access via GetValue which is primarily meant for attribute access these days. But it should still work. As always for non-executable code examples, it is hard to judge what is going wrong there for you. But note that checks like these are not doing what you probably think they are doing:

      ifnoerr(auto & typeNode = bumpNode.GetInputs().FindChild(maxon::Id("...")))
      {
        ...
      }
      

      I personally would use auto only sparingly and especially with the added line-break and the unfortunate placement of the reference operator had to look twice what you are doing here. But that is just personal taste, even if we wrote it like this in a more Maxonic way, it would not do what you probably think it does.

      void SomeFunction()
      {
        iferr_scope_handler
        {
          // DiagnosticOutput("@ failed with error: @", MAXON_FUNCTIONNAME, err);
          return;
        };
      
        const maxon::GraphNode& someInput = bumpNode.GetInputs().FindChild(maxon::Id("...")) iferr_return;
        if (!someInput)
          return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Input node not found"_s);
      }
      

      The return value of FindChild will always be a maxon::GraphNode, unless an internal error occurs. A non-valid node path will not raise an error but return an empty and with that invalid node.

      if (someInput.IsValid())
      {
        // Do something with someInput
      }
      else
      {
        // Handle the case where the input node is not found
      }
      

      Cheers,
      Ferdinand

      edit: It is good to see that you are moving towards the Maxon API. But when I see things like this:

      maxon::GraphNode bumpNode = getConnectedNode(bumpRes, maxon::NODE_KIND::NODE);
      

      I am not so sure you are on the right track. When you write functions within the Maxon API, you should use our error handling. When you terminate the error handling within your functions, you will probably not have a good time. I.e., the signature should be this

      getConnectedNode(const GraphNode& input, const NODE_KIND kind) -> Result<GraphNode>
      

      and not -> GraphNode& or -> GraphNode. Note that there is also GraphModelHelperInterface which likely already implements what you are implementing there. See the GraphModelInterface Manual for an overview.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How do I create a Plugin Identifier?

      I see your edited/deleted posting, did you find the mail? When push comes to shove, I can also just give you a bunch of plugin IDs.

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How do I create a Plugin Identifier?

      That is the mail I used, given that you had problems before, I would say resending it will not make any difference. I also just created a new account myself this morning, and it worked fine. This is probably some mail spam filter or general firewall issue on your side.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How do I create a Plugin Identifier?

      Hey @shir,

      the site is being actively used, and we also had a couple of registrations last month. So, it is probably you who is doing something wrong, most likely some kind of firewall. I have sent you a manual invite to the e-mail handle your developers.maxon.net account is bound to and also tested that you can still create a new account (you can).

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How to obtain the object deformed by the deformer

      Hey @chuanzhen,

      limiting the deformable device to only act on the parent object is a limited but more efficient method

      The only way you could do that is by checking each deformed object op being passed into ModifyObjects, to be the parent of the also passed in gen (i.e., the deformer also simply accessible via NodeData.Get). Only for an op which is the parent of gen would you then carry out the modification. But there are no flags with which you can limit what Cinema 4D considers a valid deform target and what not (at least I am not aware of any).

      I had a look at our code base, and what I wrote about implementing the deformer logic yourself is not true (I removed the section). If I would have to restate it now, I would say a deformer can act upon anything that is either its direct parent or one of its siblings or one of their descendants, e.g., the tree below, where D means a deformer, T is a deformable and F is non-deformable. This tree is for a deformer D in Unlimited mode.

      F
      └─ T
          ├── T
          ├── D
          |   └── T
          ├── T
          |   └── T
          |       └── T
          └── T
      

      But I might have missed details, as the deform cache handling is not just a function you read in our code base and instead a bit scattered. So, reimplementing this is probably not such a good idea, as you would also have to take deform modes into account and things can then get complicated.

      In general, I would circle back to the idea that what you are trying to do, get the deformed objects in NodeData.Message, hints at the fact that you might be approaching something from the wrong angle (because it is a very unusual thing to do). Maybe you can explain what you are trying to achieve on a higher level, so that we can see if there might be a better solution.

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How to obtain the object deformed by the deformer

      Hey @chuanzhen,

      Thank you for reaching out to us. Please excuse the delay. What you are asking for is effectively a dependency graph; which Cinema 4D simply does not offer. While there are methods such as BaseList2D.GetMain() or BaseObject.GetCacheParent with which you can traverse the scene graph, there are no methods which would tell you which objects are influenced by a deformer.

      Possible Solutions

      That you are trying to do this, access the deformed objects irregularly, hints at the fact that you either are doing something in a different manner than it should be done or that you are trying to do something that is not intended. With that being said:

      Cache the Dependencies

      When you implement a deformer, you get passed in the to be modified objects in ObjectData.ModifyObject one by one as op. You could 'simply' cache that data. The caveat is here that these object references can become stale, i.e., not alive in Cinema 4D terms. What you would have to do, is cache the MAXON_CREATOR_ID or since 2025 simply the hash(node) and then later grab the matching objects over the ID/hash.

      Implement the Deformer Logic

      Not a god idea

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Joining Polygon Objects (MCOMMAND_JOIN)

      Hello @Kantronin,

      Thank you for reaching out to us. Your script does not work because it does not follow the conditions of MCOMMAND_JOIN.

      Joins the objects that are parented to the passed null object. Passing multiple objects into SMC will not join them.

      edit: Moved this into the correct forum and added a small example.

      Cheers,
      Ferdinand

      Here is how I would write that in modern Cinema 4D. I have seen that your screenshot is from an older version such as S26, or even older, but I cannot write code examples for such old versions. There are multiple things that wont work in such old version such as type hinting and the mxutils lib, but you will be able to copy the general approach - move everything under a null while preserving the transforms.

      Code
      """Demonstrates how to join all objects in a document.
      """
      
      import c4d
      import mxutils
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def main() -> None:
          """Called by Cinema 4D when the script is being executed.
          """
          # There is no super good way to do this in the manner you implied, joining everything in a
          # document. I clone here the whole document, which is not the cheapest operation (but also
          # not as expensive as it may sound like). When we do not have to join everything, it is better
          # just to clone the things you want to join. En even better way could be to not clone anything
          # and instead use undos to revert the transform and hierarchy changes we have to make.
          temp: c4d.documents.BaseDocument = doc.GetClone(c4d.COPYFLAGS_NONE)
          firstObject: c4d.BaseObject | None = temp.GetFirstObject()
          if firstObject is None:
              c4d.gui.MessageDialog("No object selected.")
              return
          
          # Get all objects in the document, and move them under a null object. We must record and
          # restore their global matrices, when we deconstruct the scene and move it all under one null
          # object.
          allObjects: list[c4d.BaseObject] = list(mxutils.IterateTree(firstObject, True))
          null: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Onull))
          for obj in allObjects:
              mg: c4d.Matrix = obj.GetMg() # save the transform of the object
              obj.Remove() # Technically not necessary in the Python API, as it will do it for you when
                           # you call things like InsertUnderLast. But C++ will not do that and a node
                           # cannot be inserted more than once in a document, otherwise the fireworks
                           # will start.
              obj.InsertUnderLast(null)
              obj.SetMg(mg) # restore the transform of the object under the null
      
          # Insert the null object into the temporary document and then join all objects under it.
          temp.InsertObject(null)
          result: list[c4d.BaseObject] = c4d.utils.SendModelingCommand(
              command=c4d.MCOMMAND_JOIN,
              list=[null],
              doc=temp,
              mode=c4d.MODELINGCOMMANDMODE_ALL,
          )
          if not result:
              c4d.gui.MessageDialog("Failed to join objects.")
              return
          
          # Now insert the joined object into the original document.
          joinedObject: c4d.BaseObject = result[0]
          joinedObject.SetName("Joined Object")
          joinedObject.Remove()
          doc.InsertObject(joinedObject)
      
          c4d.EventAdd()
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: I want to extract the polygon index of the inverted normal

      Hey,

      It is not just Cinema 4D which stores things like that, it is baked into math itself. At the core this is about the winding order as explained above. The cross product u * v will point into the opposite direction of v * u. When you have two edges e and f in a polygon, it matters in which order they appear as it will determine normal of the polygon and how the 'rendering math' can interact with it.

      You can for example find the same principle documented in the Open GL or Unity docs.

      It is totally possible to realize a tool that aligns polygons from some user input. E.g., you could use c4d.utils.ViewportSelect to let a user select a polygon, and then using raycasting to figure out if that polygon is front or back facing for the current camera and with that then figure out if that polygon is correctly aligned or not, and then start to adjust all other polygons according to that polygon (because with raycasting you can do that, check GeRayCollider.GetIntersection). But it requires the user choice of that "that is the camera position and polygon that counts".

      But what is impossible, is to determine how polygons should face for an object with no further inputs. You do not need a Moebius strip as an example for that; just take a simple plane with two polygons, facing in different directions. It is impossible to solve the aligmment of that objects polygons. One polygon could be misaligned (we cannot figure out which), two could be misaligned, or both could be correct. But we cannot find out without any user input. For closed meshes with some quickly-not-so-trivial-anymore math, we can figure out what likely should be the outside of something and what not. But even there one could then run into the case, that someone intentionally created a sphere with inverted normals to do some rendering tricks.

      Cheers,
      Ferdinand

      posted in General Talk
      ferdinandF
      ferdinand
    • RE: How to create a Track on a Vector Userdata?

      Hey @pyr,

      Thank you for reaching out to us. Do you refer with 'userdata' to your JSON input or do you mean 'a vector parameter in the user data of an object.'? Your code generally looks fine, as you took the main hurdle of understanding that there is no such thing as an animated vector; for animations, all data is decomposed into float/int/bool or custom tracks.

      When you just want to address a vector inside the user data, the answer is simply that you have to add that desc level for the user data, forming an ID such as (user data container, user data element, ...). Because the user data is itself a just a parameter of type DTYPE_SUBCONTAINER.

      Cheers,
      Ferdinand

      """Showcases how to use a three level DescID to address the x component of a vector in the user 
      data of a scene element.
      """
      
      import c4d
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def main() -> None:
          """Called by Cinema 4D when the script is being executed.
          """
          if not op:
              c4d.gui.MessageDialog("No object selected.")
              return
          
          uid: int = 1 # The ID of the thing in the user data we want to access.
          did: c4d.DescID = c4d.DescID(
              c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, 0), # 1st level, the user data container.
              c4d.DescLevel(uid, c4d.DTYPE_VECTOR, 0), # 2nd level, the user data ID, here a vector.
              c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)) # 3rd level, the x component of the vector.
          
          if op.FindCTrack(did):
              return
          
          track: c4d.CTrack = c4d.CTrack(op, did)
          op.InsertTrackSorted(track)
          c4d.EventAdd()
          print(f"Added track for {did} to {op.GetName()}.")
      
      
      if __name__ == '__main__':
          main()
      
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Userarea keyboard focus issues in 2025.3

      Hey @ECHekman,

      you can find our general answer here.

      This is unfortunately an unintended regression caused by an intentional bugfix, not taking API stability into account. The flag USERAREAFLAGS::HANDLEFOCUS is meant to signal if a user area is focusable or not. But before user areas were always focusable, as this flag was erroneously being ignored.

      We then 'fixed' this, causing the chain of events. The in detail logic is then that some types of events are not possible for non-focusable gadgets, as for example keyboard events. I will touch up the docs a bit in that area, as things are indeed a bit thin there. But the doc changes will probably not make it into the next release, given how close it is, unless I patch them in there manually.

      So, long story short, compiling with USERAREAFLAGS::HANDLEFOCUS against the 2025.0.0 SDK will result in a binary that will be compatible with all versions of 2025, you did the right thing.

      Cheers,
      Ferdinand

      posted in Bugs
      ferdinandF
      ferdinand
    • Service Announcement: Regression in GeUserArea Handling in 2025.3.X Releases

      Dear developers,

      In Cinema 4D 2025.3.X we unfortunately discovered a regression that may require actions from plugin vendors to keep your plugins functional for 2025.3.X releases. The regression impacts both the Cinema 4D Python and C++ API.

      2025.3.0 changed the focusing behavior of GeUserArea, possibly altering the runtime behavior of plugins. This direct change was not intended and is a regression in the Cinema 4D API, which we will fix with the next minor release of Cinema 4D. We apologize for the inconvenience this may cause and want to clarify the situation.

      Regression

      Attaching a user area with default flags such as shown below,

      def CreateLayout(self):
          self.SetTitle("My Dialog")
          self.AddUserArea(self.ID_USER_AREA, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 0, 0)
          self.AttachUserArea(self._ua, self.ID_USER_AREA)
          return True
      
      Bool MyDialog::CreateLayout() override
      {
          SetTitle("My Dialog");
          AddUserArea(ID_USER_AREA, BFH_SCALEFIT | BFV_SCALEFIT, 0, 0);
          AttachUserArea(_ua, ID_USER_AREA);
          return true;
      }
      

      will not handle the focus state anymore, which in turn will then cause some events such as keyboard events not to be issued anymore to the user area.

      The change itself was intentional but should not have been issued in this form. It was intended as a bugfix, because it was unintentional that all user areas are always focusable, no matter if HANDLEFOCUS was passed or not. To revert to the pre 2025.3.X behavior, one will have now to use the HANDLEFOCUS flag.

      Action from Maxon

      We will fix the issue by reverting the change with the upcoming minor release of Cinema 4D. A hotfix does not make sense in this case, as it would arrive at the same time as the minor release. With Cinema 4D 2026.0.0, we will then apply the change again, as the focus behavior of the user area is a bug which must be fixed.

      This back and forth might seem confusing, but we are committed to keeping our API stable and predictable, which is why we will fix the issue in the next minor release and then apply the change again with Cinema 4D 2026.0.0.

      Action for 3rd Parties

      As a plugin vendor using user areas which are subject to the issue, by for example listening for keyboard inputs, you have two options:

      • Keep using the binaries or Python scripts you already issued to customers and inform them that your plugin will malfunction in the context of Cinema 4D 2025.3.0 and 2025.3.1 due to a bug in the Cinema 4D API, recommending customers to fall back to 2025.2.1 in the context of your plugin. In the upcoming minor release of Cinema 4D 2025 your plugin will then work again as before.
      • Apply the fix shown below, which will then make your plugin work with all versions of Cinema 4D 2025.X.X.
      def CreateLayout(self):
          self.SetTitle("My Dialog")
          self.AddUserArea(self.ID_USER_AREA, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 0, 0)
          
          # We can always blindly set the flag, because in prior releases, even when the flag was not 
          # set explicitly, it acted as if it was set. This will act the same across all versions of 2025.
          self.AttachUserArea(self._ua, self.ID_USER_AREA, myOtherFlags | c4d.USERAREAFLAGS_HANDLEFOCUS)
      
          return True
      
      Bool MyDialog::CreateLayout()
      {
          SetTitle("My Dialog");
          AddUserArea(ID_USER_AREA, BFH_SCALEFIT | BFV_SCALEFIT, 0, 0);
          // Same here, you can compile this code for the 2025.0.0 SDK, and it will work everywhere.
          AttachUserArea(_ua, ID_USER_AREA, myOtherFlags | USERAREAFLAGS::HANDLEFOCUS);
      
          return true;
      }
      

      We have not yet fully decided how we will handle the change in Cinema 4D 2026.0.0, we might make HANDLEFOCUS a default flag for user areas, and with that then have the same surface level behavior as in 2025.2.1 (but you could then still disable an UA being focusable it if you wanted to).

      Thank you for your understanding,
      The Maxon SDK Team

      posted in News & Information cinema 4d c++ python sdk information
      ferdinandF
      ferdinand
    • RE: Userarea keyboard focus issues in 2025.3

      Yeah, no worries, only what matters is the form, what really helps us is the repro steps and code example thing. But users cannot post directly into the Bugs forum, only we can move things there. We do this so that this forum remains a clean list of actual issues and is not polluted by topics which turned out not to be bugs. So, you did nothing wrong in that regard.

      We are currently cooking on a solution, I will probably communicate the outcome tomorrow.

      Cheers,
      Ferdinand

      posted in Bugs
      ferdinandF
      ferdinand
    • RE: Userarea keyboard focus issues in 2025.3

      Hey @ECHekman,

      Thank you for reaching out to us and reporting the issue. Please follow our support procedures for reporting bugs and crashes in the future. But I can confirm that there happened a regression. That is all I can say for now, as I haven't investigated deeper yet (for example, if using GeUserArea::Message helps).

      I moved this into bugs, we are investigating.

      Cheers,
      Ferdinand


      ITEM#600193 GeUserArea is drawn delayed and does not respond to all input events anymore

      GeUserArea is drawn delayed and does not forward all input events to GeUserArea::InputEvent aynmore.

      OK: 2025.2.1
      NOK: 2025.3.1

      Reproduction:

      1. Run the attached code
      2. Click the red square
      3. Type on your keyboard with the dialog focused

      Result:

      1. The user area is drawn with a delay
      2. Keyboard inputs are not handled anymore.
      3. Adding the UA attachment flag HANDLEFOCUS will fix the non-forwarded keyboard input events but not the draw lag.

      Code

      import c4d
      
      class MyUserArea(c4d.gui.GeUserArea):
      
          def DrawMsg(self, x1: int, y1: int, x2: int, y2: int, msg: c4d.BaseContainer) -> bool:
              self.DrawSetPen(c4d.Vector(1, 0, 0))
              self.DrawRectangle(x1, y1, x2, y2) 
          
          def GetMinSize(self):
              return (250, 250)
          
          def InputEvent(self, msg: c4d.BaseContainer) -> bool:
              print(f"{msg.GetId() = }")
              return True
      
      class MyDialog(c4d.gui.GeDialog):
          ID_USER_AREA = 1000
      
          def __init__(self):
              self._ua: MyUserArea = MyUserArea()
      
          def CreateLayout(self):
              self.SetTitle("Blah")
              self.AddUserArea(self.ID_USER_AREA, c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 0, 0)
              self.AttachUserArea(self._ua, self.ID_USER_AREA) # c4d.USERAREAFLAGS_HANDLEFOCUS
              return True
          
      if __name__ == '__main__':
          dlg = MyDialog()
          dlg.Open(c4d.DLG_TYPE_ASYNC, 0, -1, -1, 400, 400)
      
      posted in Bugs
      ferdinandF
      ferdinand
    • RE: CAMERA_ZOOM and Pparallel

      Hey @WickedP,

      It did not come across as abrupt or rude, but it was clear that you were a bit frustrated, and I was just trying to make clear that they had to choose some magic number, as that is the nature of rendering an orthographic projection. As to why we chose this number, I have no idea, as this happened 20 years or an even longer time back, long before I was at Maxon. It could either be because programmers simply love powers of two, or something more concrete such as that a power of two leads to less floating precision losses when multiplying other numbers with them, or simply that they did chose 1024x576 as the norm render size then.

      And you can set the camera width and height, but you just do this in the render settings with the render resolution, which was pretty standard for 3D software I would say. The new Redshift camera then uses a more complex model with an actual sensor and fitting the sensor to the render resolution (which I ignored here since you showed us using a Standard camera).

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: 2025.3.0 SDK Release

      Hey @ECHekman, this is absolutely not the right place to report issues. Please create a new thread.

      posted in News & Information
      ferdinandF
      ferdinand
    • 2025.3.1 SDK Release

      Dear development community,

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

      C++ API

      • None

      Python API

      • Fixed a critical issue with c4dpy that would cause it to halt indefinitely when run for the first time.

      Head to our download section to grab the newest SDK downloads, or the read the 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 cinema 4d news c++ python sdk information
      ferdinandF
      ferdinand
    • RE: CAMERA_ZOOM and Pparallel

      Hey @WickedP,

      Thank you for reaching out to us. I sense there is some frustration, but I am not sure how you would expect this to work to be more intuitive. Cinema 4D has to norm its orthographic camera somehow, since an orthographic projection preserves length.

      You could just use one of the built-in framing commands or API functions, but here is how to do it manually. Cinema 4D normalizes its orthographic projection to 1024 units on the larger camera axis. I.e., for a standard 16:9 resolution such as 1080p, this means a plane with the dimensions 1024x576 will fill exactly the viewport. The rest is then just some math, see my example below.

      Cheers,
      Ferdinand

      Result

      ead1ec9c-3ed8-44fd-8c5b-e11d65fd8758-image.png

      Code

      """Demonstrates how to fit an object into the view frustum of an orthographic camera.
      
      Will create a plane of random size, a random render resolution, an orthographic camera, and then fit
      the camera to the plane so that it fits in the viewport. You can run this script rapidly multiple 
      times to see it fit different planes into different aspect ratios of a viewport.
      
      WARNING: 
          
          This script will empty the whole document each time it is run (i.e., it will delete all objects, 
          materials, lights, etc.), so make sure to save your work before running it.
      """
      
      import c4d
      import mxutils
      
      doc: c4d.documents.BaseDocument  # The currently active document.
      op: c4d.BaseObject | None  # The primary selected object in `doc`. Can be `None`.
      
      def main() -> None:
          """Called by Cinema 4D when the script is being executed.
          """
          # Empty the whole document.
          doc.Flush()
      
          # Set the width, height and aspect ratio of the current render settings.
          rData: c4d.documents.RenderData = doc.GetActiveRenderData()
          renderSize: c4d.Vector = mxutils.g_random.RandomVector(
              lower=c4d.Vector(64), upper=c4d.Vector(2048))
          rData[c4d.RDATA_XRES] = int(renderSize.x)
          rData[c4d.RDATA_YRES] = int(renderSize.y)
          rData[c4d.RDATA_FILMASPECT] = renderSize.x / renderSize.y
      
          # The inverse aspect ratio is used to calculate the camera zoom.
          iAspect: float = renderSize.y / renderSize.x
      
          # Create a plane object of random size.
          plane: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Oplane))
          planeSize: c4d.Vector = mxutils.g_random.RandomVector(
              lower=c4d.Vector(256), upper=c4d.Vector(4096))
          plane[c4d.PRIM_PLANE_WIDTH] = planeSize.x
          plane[c4d.PRIM_PLANE_HEIGHT] = planeSize.y
          plane[c4d.PRIM_AXIS] = c4d.PRIM_AXIS_ZP
      
          # Create an orthographic camera.
          camera: c4d.BaseObject = mxutils.CheckType(c4d.BaseObject(c4d.Ocamera))
          camera[c4d.CAMERA_PROJECTION] = c4d.Pparallel
          camera.SetMg(c4d.Matrix(off=c4d.Vector(0, 0, -1)))
      
          # Cinema 4D norms its orthographic camera zoom to fit 1024 units on the widest axis at 100%.
          # I.e., for the standard 16:9 viewport, it will fit a 1024x576 rectangle in the viewport, no
          # matter the actual viewport size (what counts is the aspect ratio).
      
          # We calculate the zoom factor so that the plane fits in the viewport by calculating the zoom
          # both for the x and y axes, and then taking the minimum of the two.
          cameraZoom: float = min(1024.0 / planeSize.x, 1024.0 / planeSize.y * iAspect)
          camera[c4d.CAMERA_ZOOM] = cameraZoom
      
          # Insert things into the document.
          doc.InsertObject(plane)
          doc.InsertObject(camera)
          viewport: c4d.BaseDraw = doc.GetActiveBaseDraw()
          viewport.SetSceneCamera(camera)
      
          print(f"Created plane of size ({planeSize.x}, {planeSize.y}) and camera with a zoom of "
                f"'{cameraZoom:.2f}', fitting it into a viewport of size ({renderSize.x}, {renderSize.y}).")
      
          c4d.EventAdd()
      
      
      if __name__ == '__main__':
          main()
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: Dynamic desription issue

      You do not have to do that; I only recommended this for the case when you happen to have to override a dynamic parameter of the same ID with a new GUI/datatype, then you should also set a new default value. But as I said and as you can see here at the Python dynamic description example (in C++ we have a similar one), Cinema 4D will do the bookkeeping for you. Here we read the value of Dynamic REAL 6 at 1106.

      4361ae57-21f3-47d2-87e4-84709bf6f809-image.png

      Once we have modified the description, in this case removed Dynamic REAL 6, Cinema 4D will update the data container for us and remove the data at 1106.

      2712461d-c0c7-4280-96f5-ca9be93dfd8b-image.png

      Cheers,
      Ferdinand

      posted in Cinema 4D SDK
      ferdinandF
      ferdinand
    • RE: How to implement an image control that can receive and generate image data drag events

      Hey @Dunhou,

      I am still not 100% clear about what you are trying to do. But I guess what you want to do is distinguish a single-drag -click, i.e., the user is dragging something, from a single click. The issue with that is that we are in your code inside a while loop which just polls the input state as fast as it can and not in message stream, where we only get events for state changes. So, this means unless there is Speedy Gonzales at the mouse, even the quickest of single clicks will produce more than one iteration in the loop.

      What is still unclear to me why you are doing all this, as knowing that the mouse is outside of the UA does not mean that we know if the user dropped the payload on an object. But this is how I would solve distinguishing a 'light click' (a single click) from a drag event.

      A cleaner solution might be to let the convenance function InputEvent be a convenance function and move to the source Message. There you should be issue start and stop events for drag operations. But since you want to start it yourself, we are sort of in a pickle. I would have to play around a bit with the code to see if there is a better way with Message,

      Cheers,
      Ferdinand

      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[c4d.BFM_INPUT_DEVICE] != c4d.BFM_INPUT_MOUSE and msg[c4d.BFM_INPUT_CHANNEL] != c4d.BFM_INPUT_MOUSELEFT:
              return False
          
          dragType: int = self._host.GetInt32(self._host.ID_DRAG_TYPE)
          mx = int(msg[c4d.BFM_INPUT_X])
          my = int(msg[c4d.BFM_INPUT_Y])
          mx -= self.Local2Global()["x"]
          my -= self.Local2Global()["y"]
      
          state = c4d.BaseContainer()
          self.MouseDragStart(c4d.BFM_INPUT_MOUSELEFT,mx,my,c4d.MOUSEDRAGFLAGS_DONTHIDEMOUSE|c4d.MOUSEDRAGFLAGS_NOMOVE)
          lastPos: tuple[float, float] | None = None
      
          while True:
              res, dx, dy, channels = self.MouseDrag()
              if res != c4d.MOUSEDRAGRESULT_CONTINUE:
                  break  
              
              self.GetInputState(c4d.BFM_INPUT_MOUSE, c4d.BFM_INPUT_MOUSELEFT, state)
              
              # This is how I debugged this, GetContainerTreeString (in the beta it might be already
              # contained) is a feature of a future version of the SDK.
              # print(f"{mxutils.GetContainerTreeString(state, 'BFM_')}")
      
              # State: Root (None , id = -1):
              # ├── BFM_INPUT_QUALIFIER (DTYPE_LONG): 0
              # ├── BFM_INPUT_MODIFIERS (DTYPE_LONG): 0
              # ├── BFM_INPUT_DEVICE (DTYPE_LONG): 1836021107
              # ├── BFM_INPUT_CHANNEL (DTYPE_LONG): 1
              # ├── BFM_INPUT_VALUE (DTYPE_LONG): 1
              # ├── BFM_INPUT_VALUE_REAL (DTYPE_REAL): 0.0001
              # ├── BFM_INPUT_X (DTYPE_REAL): 203.13671875
              # ├── BFM_INPUT_Y (DTYPE_REAL): 88.0390625
              # ├── BFM_INPUT_Z (DTYPE_REAL): 0.0
              # ├── BFM_INPUT_ORIENTATION (DTYPE_REAL): 0.0
              # ├── 1768977011 (DTYPE_REAL): 1.0
              # ├── BFM_INPUT_TILT (DTYPE_REAL): 0.0
              # ├── BFM_INPUT_FINGERWHEEL (DTYPE_REAL): 0.0
              # ├── BFM_INPUT_P_ROTATION (DTYPE_REAL): 0.0
              # └── BFM_INPUT_DOUBLECLICK (DTYPE_LONG): 0
      
              # I.e., we are unfortunately neither being issued a BFM_DRAGSTART nor an 
              # c4d.BFM_INTERACTSTART, I assume both or only emitted in the direct Message() loop.
      
              # But we can write code like this.
      
              # if state[c4d.BFM_INPUT_DOUBLECLICK]:
              #     print(f"Double click detected at {mx}, {my}")
              #     break
              # elif state[c4d.BFM_INPUT_VALUE] != 1:
              #     print(f"Mouse button not pressed anymore at {mx}, {my}")
              #     break
              # else:
              #     print(f"Non double click at {mx}, {my}")
      
              # The issue with this is that we are here just in a loop polling the current left button
              # state, not inside a message function where we get a state stream. So, for a single
              # click, we end up with somewhat like this, and here I made sure to click really fast
      
              # Non double click at 96.8515625, 58.37109375
              # Non double click at 96.8515625, 58.37109375
              # Non double click at 96.8515625, 58.37109375
              # Non double click at 96.8515625, 58.37109375
              # Mouse button not pressed anymore at 96.8515625, 58.37109375
      
              # And this is a short drag event.
      
              # Non double click at 84.875, 56.5859375
              # Non double click at 84.875, 56.5859375
              # Non double click at 84.875, 56.5859375
              # Non double click at 84.875, 56.5859375
              # Non double click at 84.59765625, 56.5859375
              # Non double click at 83.49609375, 56.94921875
              # Non double click at 83.49609375, 56.94921875
              # Non double click at 82.39453125, 57.3125
              # Non double click at 82.39453125, 57.3125
              # Non double click at 80.74609375, 58.1328125
              # Non double click at 80.74609375, 58.1328125
              # Non double click at 77.7265625, 58.6328125
              # ...
              # Non double click at -8.35546875, 80.16796875
              # Non double click at -8.35546875, 80.16796875
              # Non double click at -8.35546875, 80.16796875
              # Mouse button not pressed anymore at -8.35546875, 80.16796875
      
              # So they are very similar, and we cannot go by the pure logic "when the coordinates
              # do not change, we are in a drag event" because this is not an event stream, i.e., we
              # might poll the same input state multiple times, depending on how fast our #while loop
              # runs.
      
              # But what we could do, is postpone all actions until we see a change. In extreme cases,
              # where the user is swiping very fast with the mouse and then clicks on a tile, this might
              # fail.
              mx -= dx
              my -= dy
      
              currentPos: tuple[float, float] = (mx, my)
              if lastPos is None and currentPos != lastPos:
                  lastPos = currentPos
      
              # The mouse is not being pressed anymore.
              if not state[c4d.BFM_INPUT_VALUE]:
                  if currentPos != lastPos:
                      print("Drag event")
                  else:
                      print("Click event")
                  break
      
          return True
      
      
      Click event
      Drag event
      Click event
      Click event
      Click event
      Drag event
      
      posted in Cinema 4D SDK
      ferdinandF
      ferdinand