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
    • Register
    • Login
    1. Home
    2. Cairyn
    • Profile
    • Following 0
    • Followers 8
    • Topics 40
    • Posts 387
    • Best 109
    • Controversial 0
    • Groups 0

    Cairyn

    @Cairyn

    169
    Reputation
    449
    Profile views
    387
    Posts
    8
    Followers
    0
    Following
    Joined Last Online

    Cairyn Unfollow Follow

    Best posts made by Cairyn

    • RE: Select the Children of a Selected Object. Then, Store it in a List

      Your two lines

      GetObjects(kid)
      objList += kid
      

      make no sense. The first one calls the recursion but then throws the result away as it is not stored anywhere (objList is a local variable for the function, not a global, and does not exist beyond the context of the current method execution). The second tries to extend a list by a single object instead of another list (the operator += works like extend, not like append) and fails because it cannot iterate through a single object. (Even if you had used the append method or listified kid it would still fail logically because you have already added the kid in the recursion call and then threw it all away.)

      What you need to do is extend the local objList by the result list of the recursion call:

      objList += GetObjects(kid)
      

      or in context,

      import c4d
      from c4d import gui
      
      def GetObjects(obj):
            objList = [obj]
            for kid in obj.GetChildren(): 
                objList += GetObjects(kid)
            return objList # should return the parent/selected obejct and all its chidlren
      
      def main():
          test = doc.SearchObject("shoulder_jnt")          
          ikJointChain = GetObjects(test)
          print ikJointChain
      
      if __name__=='__main__':
          main()
      
      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Universal "Undo" to Rule them All?

      Now we are at a point where angels fear to tread...

      I can't say what exactly the Start/EndUndo system is doing when not properly used in one level, but it definitely doesn't work in a way that supports nesting. Here I just tried a small modification of my previous scripts:

      import c4d
      
      def main():
          doc.StartUndo();
          obj = doc.GetFirstObject();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
      
          doc.StartUndo();
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
      
          doc.StartUndo();
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
      
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
      
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          c4d.EventAdd();
      
      if __name__=='__main__':
          main()
      

      I used the same sequence of five renames, but I nested rename 2+3 into a new Start/EndUndo without closing the outer first.
      If you apply Ctrl-Z on that, you will find that the first Ctrl-Z reverses the last three renames (NOT just the last 2), one more Ctrl-Z reverses the second rename, and one more the first. That means that an undo step goes back to the last StartUndo (not EndUndo) that the programmer has set. Any nesting is ignored.

      I did a few more nesting experiments but I'm not posting them here as they all point to the same behavior. I do not get any errors though, even if I completely botch the sequence - two StartUndo in a row, AddUndo before StartUndo, AddUndo after EndUndo... it actually still works, but with the little limitation that each StartUndo that I issue will break up the sequence and require a new Ctrl-Z to undo. (I am not going into full reverse engineering here now - it would be nice if the documentation could tell us how error cases and nesting are actually handled, though.)

      That means that if you have commands that internally perform an Start/EndUndo, you will not be able to get out of the additional Ctrl-Z. Same, I guess, if you issue a StartUndo inside of a function that you wish to reuse (although that may be a problem more suitable for libraries).

      You may argue that this is a design flaw. A nested Start/EndUndo could actually be treated like a single subcommand, with a Ctrl-Z working only on the highest level - which would revert the results of a functionality from the view of the outermost Start/EndUndo, while all nested Start/EndUndo pairs on lower levels do not affect the reversal (so you could use a CallCommand including its internal Undos without enforcing another Ctrl-Z in the end). I'm just not sure whether C4D could handle this conceptually, as it would be easy to lose yourself in a tree of Start/EndUndos throughout the program.

      Anyway. Your best bet may actually be to skip the CallCommands if possible and perform the necessary change by yourself, so you have a better control over the Undo sequences.
      Or you can issue a feature request for a CallCommand with an additional parameter that allows you to suppress the Start/EndUndos that are added internally.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Save variables during current session (until C4D is closed)

      Since the OP has asked explicitly for scripts, I would like to add: @m_adam 's "Global" method will not work for scripts, as their Python scope is (apparently) only valid for the current execution. It does work for Python tags and XPresso Python nodes where the scope seems to exist for the lifetime of the tag.

      If you work with tags/nodes and have actual variables to store in the globals, you need to account for the timing and the frequency of execution (e.g. do not rely that the next execution is actually the next frame). This will not apply for one-time executions like an initialization, but even there you may want to determine an anchor point (e.g. at frame 0) where the value is reset or calculated for the first time.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Universal "Undo" to Rule them All?

      @bentraje I am not using Maya so I can't say anything about that. As a programmer though, I know that there ain't nothing like no free launch... uh, lunch. If Maya is able to undo/redo automatically, then the Python API needs to have that functionality built in (automatic call of something like StartUndo at the beginning, EndUndo at the end, and every API call has an AddUndo integrated. That is possible, yes. You have to pay a price for it though, both in development time (of the API programmers) and in execution time. Maybe that is a good tradeoff; I can't judge that. It's certainly easier for the Python programmer

      Personally, I admit that I am a lazy pig and often don't integrate any undo in my scripts. But it is not that difficult as you make it sound right now. You would only have to add an AddUndo at points where you really make a change to the object tree (or related internal data). The actual calculations are unaffected. If you have a really complicated script, it would be best if you create it in layers, with the changes made to the object tree being at as high level in the call hierarchy as possible. This allows for safe exits and controlled error situations. (But we're far off topic now.)

      No, you would not need to press Ctrl-Z five times. Look at the following:

      import c4d
      
      def main():
          doc.StartUndo();
          obj = doc.GetFirstObject();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          c4d.EventAdd();
      
      if __name__=='__main__':
          main()
      

      (warning: If you execute this script you MUST have 5 objects at top level, or the thing will crash.)
      Try it, it will rename the objects; one Ctrl-Z is enough to revert all changes.
      Now compare this with the following:

      import c4d
      
      def main():
          doc.StartUndo();
          obj = doc.GetFirstObject();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          doc.StartUndo();
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          doc.StartUndo();
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          doc.StartUndo();
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          doc.StartUndo();
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          c4d.EventAdd();
      
      if __name__=='__main__':
          main()
      

      Here, each AddUndo is encapsulated into a Start/EndUndo block. Now use the script and undo - you will find that each undo only reverts one rename!
      So, the Start/EndUndo blocks are the ones that control the "container" that is reverted by Ctrl-Z (as mentioned in my previous post). If you want to revert everything at once, just encapsule the whole execution of the script in the main() function with a Start/EndUndo block. It is not necessary to repeat this block with every method you write.

      Now have a look at the following:

      import c4d
      
      def main():
          doc.StartUndo();
          obj = doc.GetFirstObject();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          obj = obj.GetNext();
          doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
          obj.SetName("Renamed!");
          doc.EndUndo();
          obj = obj.GetNext();
          obj.SetName("Renamed!");
          obj = obj.GetNext();
          obj.SetName("Renamed!");
          c4d.EventAdd();
      
      if __name__=='__main__':
          main()
      

      Execute and undo... Ooops! Only three renames were reverted! As obvious from the script, two renames are outside of the Start/EndUndo block and have no AddUndo either, so Ctrl-Z will not affect these.

      I leave further exploration to you, but I fear you will not get a super simple undo automation as in Maya.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Universal "Undo" to Rule them All?

      @bentraje Nope. Only the Start/EndUndo containers determine how often you need to hit Ctrl-Z. Not the AddUndos that you issue in between, or the type of these AddUndos. Look at the following:

      import c4d
      
      def main():
          doc.StartUndo()
          cube = c4d.BaseObject(c4d.Ocube)
          doc.InsertObject(cube, None, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
          sphere = c4d.BaseObject(c4d.Osphere)
          doc.InsertObject(sphere, cube, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, sphere)
          plat = c4d.BaseObject(c4d.Oplatonic)
          doc.InsertObject(plat, None, sphere)
          doc.AddUndo(c4d.UNDOTYPE_NEW, plat)
          doc.EndUndo()
          c4d.EventAdd()
      
      if __name__=='__main__':
          main()
      

      This code creates a hierarchy of three objects, each with their own AddUndo but all in the same Start/EndUndo block. That means all three objects will be removed at one single Ctrl-Z. Try it.

      Also, if you cut out the generation of the objects and put it into a method, this will change nothing, it will just make the Start/EndUndo bracket in your code more visible:

      import c4d
      
      def create():
          cube = c4d.BaseObject(c4d.Ocube)
          doc.InsertObject(cube, None, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
          sphere = c4d.BaseObject(c4d.Osphere)
          doc.InsertObject(sphere, cube, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, sphere)
          plat = c4d.BaseObject(c4d.Oplatonic)
          doc.InsertObject(plat, None, sphere)
          doc.AddUndo(c4d.UNDOTYPE_NEW, plat)
              
      def main():
          doc.StartUndo()
          create()
          doc.EndUndo()
          c4d.EventAdd()
      
      if __name__=='__main__':
          main()
      

      So you can just do that and put the Start/EndUndo bracket in the top call, and then work your way through your code by only using the proper AddUndos.

      Now you will ask, why the different undo types? This helps C4D to determine the extent of change that will be done (mostly, AddUndo comes before the change...), and therefore minimize the amount of data stored in the undo list. For example, if you only change a flag on a polygon object with 100 000 polys, then it would be an excessive waste to store the full object in the undo list. It's quite sufficient to store the BaseContainer, or even a single value from that container, to revert and redo the change. (Keep in mind that you don't just want to undo but at times also to redo an undone operation.)

      If you know what the AddUndo really stores for future Undos and Redos, you can save yourself some AddUndos. Try the following code:

      import c4d
      
      def create():
          cube = c4d.BaseObject(c4d.Ocube)
          doc.InsertObject(cube, None, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
          sphere = c4d.BaseObject(c4d.Osphere)
          doc.InsertObject(sphere, cube, None)
          plat = c4d.BaseObject(c4d.Oplatonic)
          doc.InsertObject(plat, None, sphere)
              
      def main():
          doc.StartUndo()
          create()
          doc.EndUndo()
          c4d.EventAdd()
      
      if __name__=='__main__':
          main()
      

      There is only one AddUndo while three objects are generated. But since the two other new objects are linked as children of the first new object, the whole substructure will behave as one - if you undo, you will still see all three objects disappear, and - more important! - if you redo, all three will appear again!

      Now, I have no access to the C4D undo code, but obviously the undo cuts the new object from the hierarchy including everything that is attached to it, and keeps the structure intact for a later redo.

      If you had linked one of the child objects somewhere else, you would need an additional AddUndo for it, naturally. E.g. if you create the Platonic as top level object, it would no longer be affected by the Undo of a parent object.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Python Tag vs Expresso for Set Driver/Driven Behavior?

      @bentraje The Python tag must be evaluated in every redraw of the scene (not just on every frame but far more often; try this by insert a Print in the code...) because it may not just depend on variables but on values from the scene (e.g. "if the object X is placed farther away than 1000 units then move object Y to..."). Keeping track of original values and comparing them with current values like this is nigh impossible (requiring analysis of code semantics), so the tag will be simply re-evaluated whenever encountered.

      You can insert an "early exit" in the tag, of course, by testing for changed variables yourself (as you as programmer have more information on the semantics than the Python interpreter/compiler). But you will need to store the values to compare with somewhere, and you will need to take into account that the execution may not happen in frame sequence (e.g. when the user scrubs the timeline backwards).

      As for the original question, I tend to put calculations into Python nodes inside the XPresso because plain math calc creates TONS of XPresso nodes that tend to become very confusing. On the other side I keep XPresso nodes whenever I want to see the affected objects in the circuit directly, or when I need a GUI for adapting something (e.g. Range Mapper with spline interface), or when the node does complicated things that I don't want to reprogram in Python (Hair intersection? ... nah, never used that 😉 ). When the whole thing is easier to maintain as a Python tag in the end, I put it into the tag...

      So it's not so much a question of whether the execution is faster, it's pure convenience of the result for me.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Retrieve the World Position of the Current Selection (from points or edges or faces)?

      You are probably in need of GetModelingAxis() which reacts to object selections or polygon/edge/point selections.

      Here's how I determine the pivot for CollieMouse, which is supposed to be the same pivot that the built-in transformation functionality is using:

      	BaseObject* objcenter = doc->GetRealActiveObject(nullptr, nullptr); // GetActiveObject() fails with multiple selections?
      	if (objcenter != nullptr)
      	{
      		Matrix centermat = objcenter->GetModelingAxis(doc);
      		pivot = centermat.off;
      	}
      

      Note: This is R19 C++, but I can see GetModelingAxis in R20 Python as well.

      If this is not what you're looking for, you can take a point as vector, transform this vector by the object's world matrix, and get the world "matrix" for the point. If you need more than one point, use the average of those points.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Python: Global Variable vs Plugin ID

      Actually, Python works differently from C++. A "global" variable there is only global for the current execution context. In Python, variables are not declared; Python creates them when they are first used ("first" meaning, there is no such variable in the current execution context already).

      Where a C++ global variable is created at compile time and represents one location in memory throughout, Python globals are created at runtime in their context. Cinema 4D has one context per Python element, so if you are accustomed to C++ globals, the following will irritate you:

      • Create two objects with a Python tag
      • Write the following code into the tags:
      import c4d
      
      def main():
          global x
      
          if doc.GetTime().GetFrame(doc.GetFps()) == 0:
              x = 1
              print "Tag 0 x set to 1"
          else:
              print "Tag 0 x = ", x
              x += 1
      

      for one tag, and

      import c4d
      
      def main():
          global x
      
          if doc.GetTime().GetFrame(doc.GetFps()) == 0:
              x = 0.5
              print "Tag 1 x set to 1/2"
          else:
              print "Tag 1 x = ", x
              x += 1
      

      for the other. Obviously, the first tag counts on full integers and the second on halfs, both are reset on frame 0.

      The output amounts to

      Tag 0 x set to 1
      Tag 1 x set to 1/2
      Tag 0 x =  1
      Tag 1 x =  0.5
      Tag 0 x =  2
      Tag 1 x =  1.5
      Tag 0 x =  3
      Tag 1 x =  2.5
      Tag 0 x =  4
      Tag 1 x =  3.5
      Tag 0 x =  5
      Tag 1 x =  4.5
      Tag 0 x =  6
      Tag 1 x =  5.5
      Tag 0 x =  7
      Tag 1 x =  6.5
      Tag 0 x =  8
      Tag 1 x =  7.5
      Tag 0 x =  9
      Tag 1 x =  8.5
      Tag 0 x =  10
      Tag 1 x =  9.5
      

      Obviously, although both tags claim a global x, they access different x's. That's because their contexts are different. Which also means that they cannot exchange data through globals this way. (You can try with an external module that you import, but I don't have time to verify that at the moment.)

      If all you want is to store a value between executions of a tag, this is easy to implement this way. However:

      • You must make sure that Python creates the variable at some point. Here, I did that through a first value setting at frame 0. Which also means that every time the timeline passes through 0, the value is reset.
      • You must pay attention to the evaluation of the object tree. I chose a tag here since the tag is evaluated with the tree every time. That is NOT the case for a generator, as there are caching mechanisms at work.
      • You must take care of situations like "jumping around in the timeline", replaying backwards, setting the time explicitly through some other plugin, and such.
      posted in General Talk
      CairynC
      Cairyn
    • RE: Universal "Undo" to Rule them All?

      @bentraje Meh, I just noticed my first code is not a good example because of what I later wrote. Since the top object of the created hierarchy is already determining the fate of the subobjects, the additional AddUndos have no true function. That does not make it wrong but it does not properly prove what I claim 😉

      Try the following:

      import c4d
      
      def create():
          cube = c4d.BaseObject(c4d.Ocube)
          doc.InsertObject(cube, None, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
          sphere = c4d.BaseObject(c4d.Osphere)
          doc.InsertObject(sphere, None, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, sphere)
          plat = c4d.BaseObject(c4d.Oplatonic)
          doc.InsertObject(plat, None, None)
          doc.AddUndo(c4d.UNDOTYPE_NEW, plat)
              
      def main():
          doc.StartUndo()
          create()
          doc.EndUndo()
          c4d.EventAdd()
      
      if __name__=='__main__':
          main()
      

      Here, the three objects are created independently on top level, yet still all disappear with one CTRL-Z.

      (And again, this is true for all types of AddUndo.)

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Retrieve the World Position of the Current Selection (from points or edges or faces)?

      @bentraje Yeah, the second part was meant just in case you have need for a manual process. GetModelingAxis() takes multiselections of elements into account.
      However, I believe GetModelingAxis() works by selected mode (as you can have different selections for points, polys, and edges in parallel depending on whether the point, poly, edge, or object mode is active. So if you explicitly need the point selection and use the method in a script, you may want to switch to the point mode before using GetModelingAxis(). (Also, the actual modeling axis can be changed by the tools, so caution is in order. Personally, this is the behavior I needed, so I didn't make any big experiments.)

      posted in Cinema 4D SDK
      CairynC
      Cairyn

    Latest posts made by Cairyn

    • RE: 3D Mouse Slider Focus

      @charly CollieMouse rotates around the current selection or the explicit rotation center (the one you set with the INS shortcut). It will not rotate around the mouse pointer (I think... I implemented so many modes over time that I tend to forget the options...). The mouse pointer only exists in 2D space anyway so the actual rotation would happen around a projection of the mouse position into the scene, and if you move the mouse, the rotational center would change... not sure whether that is even a usable mode.

      The mode I'd recommend would be around the explicit center: Position the mouse, press INS (or whatever shortcut you use), and then use the 3D mouse to rotate around that. C4D shows the explicit center as green cross so you always know where it is.

      At the moment there is no R2023 version of CollieMouse anyway, so the point is moot. But you can reach me under cairyn (at) tigress (dot) com if you require specialty development.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: Custom GUI Linkbox checking for Accepted types?

      @ferdinand Thanks for the reply! Ah, of course, I should have considered that Command is actually the last message I get for that operation. This Message stuff totally got me on the wrong track.

      Naturally, resetting the link and thereby cancelling and reverting the user's drag or pick operation is not as nice as showing a "stop sign" mouse pointer during the operation. Since the ACCEPT clause in a Resource allows setting accepted types, I wonder why there is no API functionality allowing the same. It's not even in the C++ implementation. I suppose people should use external resources anyway, but seeing stuff in the API makes functionality clearer to me at least.

      Regarding PyCapsule, no, I didn't happen across that previously, I just took code from some other thread on this forum to get into the message. I was lazy and looked for PyCapsule in the Cinema 4D API doc, and assumed it undocumented when it wasn't found. Totally my fault, I should have checked the web too. Never program in an unfamiliar scope past midnight! 😹

      Nice code btw.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • Custom GUI Linkbox checking for Accepted types?

      Hello; (followup to previous Custom GUI question)

      setting up a linkbox in a dialog through a CUSTOMGUI_LINKBOX is easy enough programmatically, and it works fine. However, by default that linkbox accepts any BaseList2D class, object, material, or tag (and more).

      If I were in a Resource, I could use the ACCEPT notation to define the accepted types, but I want to keep the whole micro-plugin in one file, so I am building the GUI through Python code... and the LinkBoxGui class has no Accept method, nor an Accept key for the setup BaseContainer, nor is there anything in the class hierarchy that looks like it.

      On this forum, I found on older post telling me to evaluate the MSG_DESCRIPTION_CHECKDRAGANDDROP message. This apparently needs to be done in the dialog's Message() function, and yes, I do receive this message:

      import c4d
      import ctypes
      import _ctypes
      
      
      PLUGIN_ID = 1000001 # this is a test ID, get your own!
      
      ID_BUTTON_CLOSE = 1001
      ID_BUTTON_CHECK = 1002
      ID_LINKBOX = 1003
      
      
      class LinkDialog(c4d.gui.GeDialog):
      
          def CreateLayout(self):
              self.SetTitle("Link Test")
              self.GroupBorderSpace(10, 10, 10, 10)
      
              if self.GroupBegin(id=0, flags=c4d.BFH_SCALEFIT, cols=1, title="", groupflags=0):
      
                  bc = c4d.BaseContainer()
                  bc[c4d.LINKBOX_HIDE_ICON] = False
                  bc[c4d.LINKBOX_NO_PICKER] = False
                  bc[c4d.LINKBOX_LAYERMODE] = False
      
                  self.linkControl = self.AddCustomGui(ID_LINKBOX, c4d.CUSTOMGUI_LINKBOX,
                          name="A link", flags = c4d.BFH_SCALEFIT | c4d.BFV_FIT,
                          minw=10, minh=10, customdata=bc)
                  self.GroupEnd()
      
              if self.GroupBegin(id=0, flags=c4d.BFH_CENTER, cols=2, title="", groupflags=0):
                  self.GroupBorderSpace(0, 20, 0, 0)
                  self.AddButton(ID_BUTTON_CLOSE, c4d.BFH_SCALEFIT, name="Close")
                  self.AddButton(ID_BUTTON_CHECK, c4d.BFH_SCALEFIT, name="Check")
                  self.GroupEnd()
      
              return True
      
          def Command(self, messageId, bc):
              if messageId == ID_BUTTON_CLOSE:
                  self.Close()
              if messageId == ID_BUTTON_CHECK:
                  obj = self.linkControl.GetLink(c4d.documents.GetActiveDocument())
                  if obj == None:
                      print("No object linked!")
                  else:
                      print("Name: ", obj.GetName())
                      print("Class: ", type(obj))
                      print("# of direct children: ", len(obj.GetChildren()))
              return True
      
          def Message(self, msg, result):
              if msg.GetId() == c4d.MSG_DESCRIPTION_CHECKDRAGANDDROP: # = numerically 26
                  ctrlID = msg.GetLong(c4d.LINKBOX_ACCEPT_MESSAGE_CONTROL_ID)
                  print (ctrlID)
                  if ctrlID == ID_LINKBOX:
                      print ("Type ID: ", msg[c4d.LINKBOX_ACCEPT_MESSAGE_TYPE]) # 201, not MSG_DESCRIPTION_CHECKDRAGANDDROP !
                      print ("Element: ", msg[c4d.LINKBOX_ACCEPT_MESSAGE_ELEMENT])
                      print ("Element type: ", type(msg[c4d.LINKBOX_ACCEPT_MESSAGE_ELEMENT]))
                      print ("Accept: ", msg[c4d.LINKBOX_ACCEPT_MESSAGE_ACCEPT])
                      # ptr = ctypes.pythonapi.PyCapsule_GetPointer(msg[c4d.LINKBOX_ACCEPT_MESSAGE_ELEMENT], None)
                      obj = self.linkControl.GetLink(c4d.documents.GetActiveDocument())
                      if obj != None:
                          print (obj.GetName())
                          if not isinstance (obj, c4d.BaseObject):
                              print ("Delete dragged object!")
                              self.linkControl.SetLink(None)
                          else:
                              print ("Accepted.")
              return c4d.gui.GeDialog.Message(self, msg, result)
              
              
      class LinkCommandData(c4d.plugins.CommandData):
      
          dialog = None
      
          def Execute(self, doc):
              if self.dialog is None:
                  self.dialog = LinkDialog()
              return self.dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=PLUGIN_ID, defaulth=40, defaultw=200)
      
          def RestoreLayout(self, sec_ref):
              if self.dialog is None:
                  self.dialog = LinkDialog()
              return self.dialog.Restore(pluginid=PLUGIN_ID, secret=sec_ref)
      
      
      if __name__ == "__main__":
          c4d.plugins.RegisterCommandPlugin(
                      id=PLUGIN_ID,
                      str="Link custom GUI test",
                      help="Demonstrates Link field usage",
                      info=0,
                      dat=LinkCommandData(),
                      icon=None)
      

      Unfortunately, that post seems to be too old (8 years or so) to be of use, because it says I would receive this message after the value in the control is changed. That is not the case.

      The message appears once when I use the Pick functionality, and many times when I drag an object into the link box. Apparently, I receive this message as long as I hover the mouse over the link field.

      It is unclear under the circumstances which of these message is chronologically the last one (on releasing the mouse button). The object returned by GetLink() on the control is always the old/previous object, not the one I drag into the link field. The dragged object from the message itself, received as "Element", is a PyCapsule (for Python3).

      So, how to proceed? To inspect the dragged object, I need to unwrap it from the PyCapsule; sadly, that class is not documented. I understand that I need to call PyCapsule_GetPointer somehow, and that's where my understanding ends (trying samples from this forum all end in a crash of C4D).
      Then, I am probably(?) required to store a key/value in result (a BaseContainer)... and return False? (Returning False alone does not do anything.)

      And is the evaluation of this method the same for Pick and for Drag activities? Am I even in the correct message?

      posted in Cinema 4D SDK r23 python
      CairynC
      Cairyn
    • RE: Using AddCustomGui for primitive controls

      @manuel Thanks for the notes, there seems to be a lot that requires additional explanation beyond the API description. I may ask more specific questions later...

      Regarding the DateTime, I got a weird error when I try to show the extended GUI: the clock and calendar fields get duplicated:

      20221124__05__Screenshot_DateTime_04.jpg

      This happens upon calling

      self.dateTimeControl.SetLayoutMode(c4d.LAYOUTMODE_MAXIMIZED)

      or if you like the full script,

      import c4d
      from c4d import gui
      from datetime import datetime
      
      ID_BUTTON_CLOSE = 1001
      ID_DATETIME = 1002
      
      class CustomDialog(c4d.gui.GeDialog):
      
          def __init__(self):
              self.customcontrol = None
      
          def CreateLayout(self):
              self.SetTitle("Custom Control Test")
              self.GroupBorderSpace(10, 10, 10, 10)
      
              if self.GroupBegin(id=0, flags=c4d.BFH_SCALEFIT, cols=1, title="", groupflags=0):
      
                  bc = c4d.BaseContainer()
                  bc[c4d.DATETIME_TIME_CONTROL] = True
                  bc[c4d.DATETIME_DATE_CONTROL] = True
      
                  self.dateTimeControl = self.AddCustomGui(ID_DATETIME, c4d.DATETIME_GUI,
                          name="DateTime",
                          flags = c4d.BFH_SCALEFIT | c4d.BFV_FIT,
                          minw=10, minh=10, customdata=bc)
                  self.dateTimeControl.SetLayoutMode(c4d.LAYOUTMODE_MAXIMIZED)
                  dt = datetime.strptime('16.07.2011 03:37:12',"%d.%m.%Y %H:%M:%S")
                  dtd = c4d.DateTimeData()
                  dtd.SetDateTime(dt)
                  self.dateTimeControl.SetDateTime(dtd)
                  self.GroupEnd()
      
              if self.GroupBegin(id=0, flags=c4d.BFH_CENTER, cols=2, title="", groupflags=0):
                  self.GroupBorderSpace(0, 20, 0, 0)
                  self.AddButton(ID_BUTTON_CLOSE, c4d.BFH_SCALEFIT, name="Close")
                  self.GroupEnd()
      
              return True
      
          def Command(self, messageId, bc):
              if messageId == ID_BUTTON_CLOSE:
                  self.Close()
              return True
      
      def main():
      
          dlg = CustomDialog()
          dlg.Open(dlgtype=c4d.DLG_TYPE_MODAL_RESIZEABLE)
          dt = dlg.dateTimeControl.GetDateTime().GetDateTime()
          print (dt.year, dt.month, dt.day)
          print (dt.hour, dt.minute, dt.second)
      
      if __name__=='__main__':
          main()
      

      (and no, it doesn't work in a CommandData plugin either...)
      I am still on R23.1 and have no current access to more recent versions, so this may actually be an error that has already been corrected, and it's no longer a valid question...

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • Using AddCustomGui for primitive controls

      Hello;
      using AddCustomGui for complex controls like Quicktab or Linkbox seems easy enough, as you get a specific class derived from BaseCustomGui. However, in the description for GeDialog::AddCustomGui there are numerous "Symbol ID"s listed that do not have a specific class but result in a BaseCustomGui being returned: CUSTOMGUI_REAL, CUSTOMGUI_STATICTEXT, CUSTOMGUI_TIME, etc.

      Mostly they seem to correspond with certain primitives (which already have their own dedicated Add functions, e.g. CUSTOMGUI_REALSLIDER and AddEditSlider), but sometimes there is no separate Add function, as e.g. for CUSTOMGUI_PROGRESSBAR or CUSTOMGUI_MATRIX.

      I have no idea how to work with these.

      For complex controls, I just create the GUI:

      self.customcontrol = self.AddCustomGui(ID_CUSTOM, c4d.CUSTOMGUI_QUICKTAB, 
      name="A quick tab", 
      flags = c4d.BFH_SCALEFIT | c4d.BFV_FIT, minw=120, minh=30, 
      customdata=bc)
      

      and work from there with the returned class. No issues here.

      If I try the same for a primitive control like CUSTOMGUI_REALSLIDER, no GUI appears in the dialog at all (the return value is BaseCustomGui, not None, so the call did not fail at least). I cannot find any documentation on parameters to be set through the passed BaseContainer, if that's the issue.

      You may say "use AddEditSlider" but there is no AddTime or AddProgressBar so I would expect AddCustomGui to work.

      I seem to overlook something important. What do these "Symbol ID"s document, and how do I use them?

      posted in Cinema 4D SDK python r23
      CairynC
      Cairyn
    • RE: Look At Camera Tag and text orientation

      (If the hierarchy is not so much of an issue, you could also just set a null as parent of the text. The null will then get the look-at-camera tag so its z-axis points at the camera. The text will be a child of the null, and you turn it by 180° so its z axis points away from the camera in the opposite direction as its parent. I am not totally sure the look-at-camera is always well behaved, though.)

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: SetTime() and document updating

      There are predefined variables available inside the execution context of the Python tag. op is the tag itself (just like doc is the document). So you can just write op[c4d.ID_USERDATA, 2]
      obj = op.GetObject() is the object the tag is on, e.g. the null. It makes sense to place the Python tag appropriately, so you don't have to use SearchObject, which is an "expensive" operation.

      Note that in my sample code I use doc.GetFirstObject() to get the cube, which is of course bad code and you shouldn't do that 😉

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: SetTime() and document updating

      @brucek5

      Well, first of all you wouldn't need Event Notification if the button is on the Python tag itself, instead of the null. For its own controls, the message function already receives all events:

      import c4d
      
      def message(msg_type, data):
          if msg_type == c4d.MSG_DESCRIPTION_COMMAND:
              print (data)
              desc_id = data['id']
              print (desc_id, " received")
      
      def main():
          pass
      

      (to be used in a Python tag with one button)

      This is still called very often but at least doesn't require the whole notification shebang which is listed as private in the documentation, and as long as you use early return methods, it's not bad.
      There may be reasons why you'd want the GUI attached to the null but in this case I would readily put it on the Python tag.

      As far as Python scripts go: Yes, the advantage of a Python tag is that the code is bound to the document/project/file, which is not the case with a script, which resides separately in the script folder, or a full Python plugin, which goes in the plugin folder. There are arguments for both ways to store Python code - the script or plugin is available for all projects (if properly generalized); the plugin even allows setting up a GUI that can be embedded in the layout.

      In your case, I am honestly not sure whether I recommend a script or plugin; I guess I personally would realize it as such (being a script fan 😁 ) but if the code is tightly bound to your project, doing it within the tag is just as fine, as long as you execute the code as reaction to a button (in the message function) and not as part of the scene evaluation (in the main function).

      Setting up your own dialog (or even plugin, for which you'd need a unique plugin ID to be obtained from Maxon) indeed requires a bit more work than the bare user data. Here's a sample script (not plugin) that opens a very simple dialog with two buttons:

      import c4d
      from c4d import gui, utils
      
      ID_EDIT_BUTTONGROUP = 1007
      ID_EDIT_OKBUTTON = 1008
      ID_EDIT_CANCELBUTTON = 1009
      
      class settingsDialog(c4d.gui.GeDialog):
      
          def __init__(self):
              c4d.gui.GeDialog.__init__(self)
              self.exitok = False
      
      
          def CreateLayout(self):
              self.SetTitle("Several text fields")
              if self.GroupBegin(ID_EDIT_BUTTONGROUP, c4d.BFH_CENTER, 2, 0):
                  self.AddButton(ID_EDIT_OKBUTTON, c4d.BFH_LEFT, name="OK")
                  self.AddButton(ID_EDIT_CANCELBUTTON, c4d.BFH_LEFT, name="Cancel")
              self.GroupEnd()
              return True
      
      
          def Command(self, id, msg):
              if id==ID_EDIT_OKBUTTON:
                  print ("OK clicked")
                  self.exitok = True
                  self.Close()
              if id==ID_EDIT_CANCELBUTTON:
                  print ("Cancel clicked")
                  self.exitok = False
                  self.Close()
              return True
      
      
      def main():
          sDialog = settingsDialog()
          if sDialog.Open(c4d.DLG_TYPE_MODAL_RESIZEABLE):
              print ("Exit:", sDialog.exitok)
      
      
      if __name__=='__main__':
          main()
      

      I do have lessons for script dialogs galore here (section 10):
      https://www.patreon.com/cairyn
      and also simple command plugins that allow you to embed the dialog in the GUI, but that is not really the core of the question. (There are also examples here on the forum if you search for GeDialog, I assume.)

      Back to the original topic:
      I have set up a simple example with a cube falling on a plane through dynamics.
      The Python tag contains a button that, if clicked, executes the scene for frames 0-80 and creates keyframes for rotation and translation of the cube. The tag's code looks like this:

      import c4d
      
      def SetKey (currentTime, obj, parameterDesc, value):
          track = obj.FindCTrack(parameterDesc)
      
          if track == None:
              # create track for this parameter
              track = c4d.CTrack(obj, parameterDesc)
              obj.InsertTrackSorted(track)
          curve = track.GetCurve() # always present
          currentKey = curve.FindKey(currentTime) # returns dictionary "key" (Key), "idx" (Index)
          if currentKey:
              key = currentKey['key'] # key reference to change
              doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, key)
              key.SetValue(curve, value)
              curve.SetKeyDirty()
          else:
              defkey, dub = doc.GetDefaultKey() # new key to insert
              key = defkey.GetClone() # needed?
              key.SetValue(curve, value)
              key.SetTime(curve, currentTime)
              key.SetInterpolation(curve, c4d.CINTERPOLATION_SPLINE)
              curve.InsertKey(key, True)
              doc.AddUndo(c4d.UNDOTYPE_NEW, key)
      
      def message(msg_type, data):
          if msg_type == c4d.MSG_DESCRIPTION_COMMAND:
              print (data) # irrelevant since there is only one button
              fps = doc.GetFps()
              theCube = doc.GetFirstObject()
              for frame in range(81):
                  doc.SetTime(c4d.BaseTime(frame, fps))
                  doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_INTERNALRENDERER)
                  print ("Rot:", theCube[c4d.ID_BASEOBJECT_REL_ROTATION])
                  print ("Pos:", theCube[c4d.ID_BASEOBJECT_REL_POSITION])
      
                  descX = c4d.DescID(
                          c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                          c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
                  )
                  descY = c4d.DescID(
                          c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                          c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
                  )
                  descZ = c4d.DescID(
                          c4d.DescLevel(c4d.ID_BASEOBJECT_REL_ROTATION, c4d.DTYPE_VECTOR, 0),
                          c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)
                  )
                  val = theCube[descX]
                  SetKey (doc.GetTime(), theCube, descX, val)
                  val = theCube[descY]
                  SetKey (doc.GetTime(), theCube, descY, val)
                  val = theCube[descZ]
                  SetKey (doc.GetTime(), theCube, descZ, val)
                  descX = c4d.DescID(
                          c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                          c4d.DescLevel(c4d.VECTOR_X, c4d.DTYPE_REAL, 0)
                  )
                  descY = c4d.DescID(
                          c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                          c4d.DescLevel(c4d.VECTOR_Y, c4d.DTYPE_REAL, 0)
                  )
                  descZ = c4d.DescID(
                          c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, c4d.DTYPE_VECTOR, 0),
                          c4d.DescLevel(c4d.VECTOR_Z, c4d.DTYPE_REAL, 0)
                  )
                  val = theCube[descX]
                  SetKey (doc.GetTime(), theCube, descX, val)
                  val = theCube[descY]
                  SetKey (doc.GetTime(), theCube, descY, val)
                  val = theCube[descZ]
                  SetKey (doc.GetTime(), theCube, descZ, val)
      
      def main():
          pass
      

      As you see, the scene evaluation is triggered in the aforementioned way by setting the frame and then calling ExecutePasses, which will realize the dynamics. Then the keyframes are generated for the new cube position. You can then switch off dynamics and use only the keyframes. (I am using BUILDFLAGS_INTERNALRENDERER here but I am not sure whether this makes a difference.)

      You can play with that; I suppose without knowing your exact code I can't help much more.

      EDIT: Here's the scene for your convenience:
      Python tag triggers scene evaluation.c4d

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: SetTime() and document updating

      Ah, I see now you wrote "tag based". Since the tag is evaluated within the pass execution, I don't think you can call ExecutePasses() at all (but then, you shouldn't be able to SetTime() either? Are you executing the script as response to a button press, or during the regular execution of the tag?) I am now a bit confused about the logic of it all. You create keyframes, so why is this a tag instead of a regular script that you would only trigger once?

      If you want to create keyframes that reflect a certain time-based state, you could run through the scene in a script (not tag) and ExecutePasses from there, running the script only when necessary. This would essentially be a form of baking your dynamic target values into keyframes.

      posted in Cinema 4D SDK
      CairynC
      Cairyn
    • RE: SetTime() and document updating

      That's correct, SetTime() doesn't perform an evaluation of the scene. You need to call BaseDocument.ExecutePasses() to do that.

      posted in Cinema 4D SDK
      CairynC
      Cairyn