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. kvb
    3. Posts
    K
    • Profile
    • Following 0
    • Followers 0
    • Topics 5
    • Posts 19
    • Best 0
    • Controversial 0
    • Groups 0

    Posts made by kvb

    • RE: GeDialog Timer Always Printing to Console?

      Well, that was a fast solution... couldn't it have emerged BEFORE I hit submit?!? Haha, the solution was easy... I was returning True on the Timer function. Took a look at the python memory viewer example and noticed they don't return a value on it. Removed the return from my Timer() function and all is working as expected.

      def Timer(self, msg):
          print("Timer")
      

      That's it:)

      posted in Cinema 4D SDK
      K
      kvb
    • GeDialog Timer Always Printing to Console?

      Hey Folks,
      Getting some odd behavior from a GeDialog's Timer and I've been troubleshooting this all day with no success. I thought it was due to a lot of different things, including some python libraries I had been using, but when I boiled it down to the most basic example and tried testing in a c4d without those libraries installed, I still get this very odd behavior of "(null)" being printed to the console over and over again if my Timer is active.

      If I uncomment the print("Timer") line it just prints "(null)Timer". The "(null)" calls always show up on the same line, unless I print from Timer(), then the print statement adds a new line break.

      Issues are the same in 2023 and S26, whether I have the 3rd party python libraries installed or not. The following example is pure vanilla. No libs, just a boilerplate dialog that runs a timer and has 4 static text fields.

      Thanks for your help!

      import c4d
      from c4d import gui
      
      PLUGIN_ID = 1234567  # Replace with your unique plugin ID from PluginCafe
      
      class MyDialog(gui.GeDialog):
      
          ID_GROUP1 = 1000
          ID_GROUP2 = 2000
          ID_STATIC_TEXT1 = 1001
          ID_STATIC_TEXT2 = 1002
          ID_STATIC_TEXT3 = 2001
          ID_STATIC_TEXT4 = 2002
      
          def CreateLayout(self):
              self.SetTitle("My Custom GeDialog")
      
              # Group 1
              if self.GroupBegin(self.ID_GROUP1, c4d.BFH_SCALEFIT, 2, 1, "Group 1"):
                  self.AddStaticText(self.ID_STATIC_TEXT1, c4d.BFH_LEFT, name="Static Text 1")
                  self.AddStaticText(self.ID_STATIC_TEXT2, c4d.BFH_LEFT, name="Static Text 2")
              self.GroupEnd()
      
              # Group 2
              if self.GroupBegin(self.ID_GROUP2, c4d.BFH_SCALEFIT, 2, 1, "Group 2"):
                  self.AddStaticText(self.ID_STATIC_TEXT3, c4d.BFH_LEFT, name="Static Text 3")
                  self.AddStaticText(self.ID_STATIC_TEXT4, c4d.BFH_LEFT, name="Static Text 4")
              self.GroupEnd()
      
              return True
          
          def Timer(self, msg):
              print("Timer")
              return True
          
          def InitValues(self):
              self.SetTimer(100)
              print("InitValues")
              return True
      
      
      class MyCommandData(c4d.plugins.CommandData):
      
          dialog = None
      
          def Execute(self, doc):
              if self.dialog is None:
                  self.dialog = MyDialog()
      
              return self.dialog.Open(dlgtype=c4d.DLG_TYPE_ASYNC, pluginid=PLUGIN_ID, defaultw=300, defaulth=150)
      
          def RestoreLayout(self, sec_ref):
              if self.dialog is None:
                  self.dialog = MyDialog()
      
              return self.dialog.Restore(pluginid=PLUGIN_ID, secret=sec_ref)
      
      
      if __name__ == "__main__":
          c4d.plugins.RegisterCommandPlugin(PLUGIN_ID, "My Custom GeDialog", 0, None, "A custom GeDialog example", MyCommandData())
      
      

      Timer-no-prints.jpg Timer-with-print.jpg

      posted in Cinema 4D SDK s26 2023 python
      K
      kvb
    • RE: Vertex Maps on Generators

      Apologies, I thought my mention of the Voronoi Fracture's ability to generate vertex map tags (itself being a generator that deals with variable geometry) would be clear enough, but I suppose a casual mention doesn't suffice for an app this complex, haha!

      Yeah, it's really all about overcoming that immutable nature of the vertex map tag. I'm generating them on parametric objects and I need them to be able to adapt to changing point counts. Just like they're able to do on editable polygon objects. Which means I have to create a new tag each time the host object's geometry changes and update anywhere else it's linked. That's where TransferGoal() comes in.

      Where my function was failing was a result of TransferGoal() not only updating the BaseLinks, but also the variables within my code. So in my function, once TransferGoal()is called, both vmap and replacement_tag variables end up referencing the same tag (the new one), leaving me without a variable filled with the old tag to delete.

      Passing True as the second argument left my variables alone and only updated the links, essentially making it work perfectly. So passing True or False to TransferGoal() didn't make any difference as far as the BaseLinks were concerned. Those always updated correctly. If I had to guess I would say it has to do with the memory addresses of the objects being transferred... which is just more reason to avoid ignoring that little instruction in the sdk;) I'll trust the devs on matters of memory handling lol.

      Turns out it's quite easy to work around. Since I insert the new tag directly after the old tag I can simply use GetPred() to find the old tag and successfully delete it.

      As far as your question as to whether I would consider this desirable or expected behavior, I would say no. I would much prefer that my in-code variables remain as they were before calling TransferGoal, because as it currently is I'm left with two distinct variables that hold the same object. In other words, I'm left with redundancy and it ends up preventing me from doing more to the old BaseList2D object. In my case, it happens to be relatively easy to re-find and re-reference that BaseList2D object, but that might not always be the case.

      But yeah, now I have a working skeleton function (that doesn't break the rules lol). Just gotta bulk it up with appropriate checks and I should be good to go!

      Thanks for your help @zipit!

      Cheers!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • RE: Vertex Maps on Generators

      Thanks @zipit!
      While being diligent in my response I actually started to notice a pattern to what I was seeing with my "recreate the tag" workaround. I believe it's related to python's "call by assignment" nature coupled with how TransferGoal() works.
      When I would call TransferGoal() how the sdk instructs me to do (always passing False for the second argument) it would result in the variable that holds the old vertex map tag updating to now hold the newly created tag (since it is now in the baselink from which the old tag was referenced). If I pass True to that second argument, my old tag variable continues to reference the old tag and everything works as expected.

      The question now is, what's the reasoning behind the sdk marking that argument as private and instructing us to always set it to False?

      https://developers.maxon.net/docs/py/2023_2/modules/c4d/C4DAtom/GeListNode/BaseList2D/index.html?highlight=transfergoal#BaseList2D.TransferGoal

      Cheers!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • Vertex Maps on Generators

      I'm pretty sure I know the answer to this, but I just need to ask to make sure I'm not missing something.

      I'm trying to do something similar to how the Voronoi Fracture can generate selections and vertex maps. Selections have been going swell, its these pesky vertex maps! My plugin happens to be a tag, not a generator... not sure if that distinction matters in this context.

      I don't know how c4d updates vertex maps when their host object's geometry changes, but it certainly doesn't happen when that host object is parametric. Once the geometry changes a replacement tag needs to be generated and the old vertex map's goals transferred to it. While I've been able to get this much of it working, once I attempt to delete the original tag it all falls apart. Even if I could get this working, I still don't know where I could safely call the following function in a NodeData plugin:

      def RefreshVertexMapTag(vmap, obj, point_cnt):
          # Make a brand new vertex map tag
          replacement_tag = obj.MakeVariableTag(c4d.Tvertexmap, point_count, vmap)
          # Disable the tag so that c4d doesn't try to update it
          replacement_tag[c4d.EXPRESSION_ENABLE] = False
          # transfer all goals to the new tag
          vmap.TransferGoal(replacement_tag, False)
          # Remove the original tag
          vmap.Remove()
      

      This very simplified function is based on a workaround posted by Maxime last year, simply to illustrate what it would entail to expose a vertex map on a generator who's data is of a variable size, noting that it is not suitable for production because it does some cross-threaded things that could crash c4d.

      I've even tried updating the byte sequence manually using GetAllLowlevelDataW(), but since that's dealing with a memoryview it naturally doesn't work.

      So, I've finally resigned to giving up on this effort, but not without at least asking if it's possible. Can a vertex map be updated to respond to the changing geometry of a parametric object/generator in a way that is safe to use in a NodeData plugin?

      To recap:

      • SetAllHighlevelData(): throws an error when the passed data's size differs

      • GetAllLowlevelDataW(): basically same result as above, except quietly (no error, just never applies any of the changes to the byte sequence)

      • Fully replace tag: Works until you try to delete the old tag, then none of it works... but even if it did it's prone to crashing c4d.

      Thanks in advance for either confirming the bad news... or for swooping in to save the day (if that's possible!)

      Cheers!
      Kevin

      posted in Cinema 4D SDK r23 python
      K
      kvb
    • RE: MoData.GetData()?

      Oh, man... I thought I hit reply on this days ago! Sorry!

      So, MoData tag container didn't work, that's not where the MoData is stored. Well, there seems to be MoData there, just always empty... but I think that's because the mograph stuff uses the messaging system so heavily that just grabbing a basecontainer doesn't cut it.

      @zipit said in MoData.GetData()?:

      Hi,

      I was a bit overworked when I wrote this test, which is why I did made a little booboo in the code above with rather dire consequences. I have fixed this now and the timings are now correct (aside from the general diceyness of the whole test). But the general consensus is the same. BaseContainer is not slow and a good choice when we need key, value pairs. One should also keep in mind, that I did employ simplifications for list both in the insert and delete case, if we want to truly delete and insert arbitrary elements from/to a list, this type is terrible, as it has to rebuild all data each time.

      Cheers,
      zipit

      Thanks for revising this. Even if the basecontainer times technically look worse by comparison, in context I can still see it's no slouch! I've moved forward with this approach and so far I'm getting great performance with breaking down and stashing away the array's data into basecontainers. Still wondering if it wouldn't be better to go with a Read/Write/CopyTo implementation, but I honestly don't see a need here. It's plenty fast enough and doesn't even need to be accessed often.

      @m_magalhaes said in MoData.GetData()?:

      hi,

      it's a bit hard to tell as i still don't understood what you were trying to achieve.
      Do you want to save the mograph data and use them in c4d as a kind of library, or do you want to export them to a 3rd Party software ?

      Cheers,
      Manuel

      I need mograph cache tag functionality without the mograph cache tag. So I need to store MoData and be able to have it persist through file saves/loads, copying to a new document/rendering, duplication, etc. Given the potential size of the data in question (especially once you add animation into the mix) I want to make sure I'm doing this in the most efficient manner possible.
      Given that all the datatypes that contain the MoData aren't directly supported by the normal storage methods in c4d (basecontainers and hyperfiles), I was concerned that breaking all the data into its individual components to store it away wouldn't be efficient. I'm seeing now that I had little to be concerned about. Performance is great so far!

      I think I've even cleared my final hurdle, which was creating an array of MoDatas. My problem there was trying to use a BaseArray when what I needed was a PointerArray. Since PointerArray's take ownership of the pointed object, freeing the allocated MoDatas is as clean and simple as calling Reset() on the Pointer Array:) Injecting my MoData into the effector pipeline is equally simple by doing a CopyTo() or MergeData()... currently I'm doing that in ModifyPoints, but maybe I should be doing it in InitPoints? I'll figure that part out, but right now I'm just happy to have it all working!

      Thank you again zipit and Manuel!

      posted in Cinema 4D SDK
      K
      kvb
    • RE: MoData.GetData()?

      @m_magalhaes said in MoData.GetData()?:

      hi,

      thanks a lot @zipit for your time.

      About the BaseContainer functions, on C++ that's the same story. Those functions are/were used internally to send data to other part of Cinema 4D (like dynamics for example)

      All will be marked as private as it's kind of useless for 3rd party developers.

      Cheers,
      Manuel.

      My god, did I really just space out on what private means... ignore me, I'm an idiot lol. That is a shame though, because those functions sound like they do exactly what I'm trying to do. Neither the Modata datatype nor the data arrays themselves are supported directly by either basecontainer or hyperfile. It seems there are really only two options then:

      • Break them down into their individual elements and build them back up. Or deal with Get/SetMemory(), which I'd rather avoid... unless that's, I don't know, a good idea maybe?

      • Store away a clone of the mograph generator, but that's gonna have a bunch of extraneous data that I don't need, which I'd like to avoid... unless... the MoData tag itself? Yes, that might work! How could I forget that the MoData is stored on a hidden tag designed specifically for that purpose?!? That fact literally inspired my plugin's design!

      Apparently my brain does not have a perfect hash function and suffers from a terribly high load factor, haha!

      Thanks!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • RE: MoData.GetData()?

      Wow... well it's hard to argue with those results! Looking back at the posts that led me down this rabbit hole it seems the info may have been outdated and/or was based on casual assumptions.
      Looking again at the research that followed those finds I can see how I may have fallen into the trap of looking to confirm those results instead of actually expanding my understanding. Thanks for the incredibly enlightening post!

      As far as what I'm trying to do, let's just say I need mograph cache functionality but can't rely on the mograph cache tag itself;) Luckily I'll no longer have the bottleneck of python... and nothing wrong with considering the path of least resistance (will likely be my initial test case, actually).

      Thanks again!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • RE: MoData.GetData()?

      @zipit said in MoData.GetData()?:

      Hi,

      the correct method to access the individual arrays of a MoData object would be GetArray. Assuming that is what you are trying to do. You will also have to specify the array type you want to access, MODATA_MATRIX for the particle matrices for example.

      If you want to keep a copy of a MoGraph state, I would clone the generator in Python (unfortunately we cannot instantiate MoData in Python directly). You could also do this manually, but you should keep in mind that most particle data is presented as mutable objects, so you would have to explicitly clone them or otherwise you will just reference them.

      import c4d
      
      def main():
          """
          """
          # Get the MoData for the selected BaseObject, if there is either
          # no selection or it does not yield some MoData, get out.
          if not isinstance(op, c4d.BaseObject):
              return
      
          modata = c4d.modules.mograph.GeGetMoData(op)
          if modata is None:
              return
      
          # Iterate over the matrices of our MoData. If you want other data,
          # e.g. colors, weights, etc., you will have to iterate over these
          # by using their respective symbol. Passing NOTOK will yield no data,
          # I am not sure why MAXON did make this the method's default argument.
          for matrix in modata.GetArray(c4d.MODATA_MATRIX):
              print matrix
      
          # We could cache the array's of our MoData individually, but an easier
          # approach in Python would be just to clone the generator which hosts
          # the MoData.
      
          # This would be the object op in this example.
          generator = modata.GetGenerator()
          # Clone that object.
          cache = generator.GetClone()
          # So that we can later on access its data.
          cached_data = c4d.modules.mograph.GeGetMoData(cache)
          print cached_data
      
      if __name__=='__main__':
          main()
      

      On a side note: I have not done any extensive tests on the performance of BaseContainer, but they are just integer key hash maps that allow for the dynamic typing of their values. And hash maps are very efficient for larger data sets, especially when it comes to access, which is probably why MAXON used them as a basis for basically everything in Cinema.

      Cheers,
      zipit

      Thanks @zipit, I'm aware of the usual way of getting and setting mograph data. Maxon's gone out of their way to consolidate these arrays in manageable number of basecontainers and I want to take advantage of that.

      I'm not sure about storing an entire clone of the generator itself would be the most efficient method, and I'm trying to be as efficient as possible, but I don't know, to be honest. If I can get it down to as simple a method of storing away just the modata, then I'll be in good shape. But it's certainly more concise than breaking down the data into it's component parts and trying to stash it away in a basecontainer or hyperfile. Certainly worth a look-see:)

      Regarding basecontainer efficiency, I've actually been hearing the opposite in a few posts, that it wasn't suited for large data sets. Now, this may have been expressed in a general sense as, without a perfect hash function, they can get very slow when the entry count gets very large. In the end it depends on the hash function. Wikipedia describes a few different perfect hash approaches, so Maxon is probably employing one of those. I say all that as a simple superuser with an internet connection and a penchant for getting in over my head, not a developer... that's about as deep as my knowledge on hash mapping goes haha! But ultimately, I don't want to rely on a possibility of reading/writing hundreds of thousands of individual basecontainer entries.

      @m_magalhaes said in MoData.GetData()?:

      hi,

      After asking the dev, this is used internally and should be marked as private. Same goes for all modata functions that return a BaseContainer.
      (GetDataIndexInstance, GetDataInstance)

      Cheers,
      Manuel

      Thanks, Manuel. Private, eh? Which would mean I need access to the SetData() function of Modata, which isn't available in python. This just so happens to have become a C++ plugin, which has access to that function. Would I be correct in assuming that I can store these containers in my own basecontainer, then retrieve them later by passing them into a modata?

      Thanks!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • MoData.GetData()?

      MoData.GetData(self, id=NOTOK)
      Get a copy of the array’s container.

      Cool... how do I use the basecontainer? I can't seem to access any data from it? My goal is to store snapshots of mograph clone arrays for later use. I'm not even sure if this is the correct approach, but it seems convenient to to be able to just save a single basecontainer instead of a series of arrays that might be cumbersome to try and stash away.

      I know that using baseconainers to store/retrieve large datasets isn't very efficient (i.e. don't use it like it's an array). I intend to use Read/Write/CopyTo, since I might be attempting to store entire mograph animations, and I gather writing successive frames as single basecontainers to a hyperfile is better than saving each one as a subcontainer in a basecontainer.

      Thanks in advance!
      Kevin

      posted in Cinema 4D SDK python
      K
      kvb
    • RE: Identify Critical Stop Please?

      @m_magalhaes said in Identify Critical Stop Please?:

      hello,

      For your next threads, please help us keeping things organised and clean. I know it's not your priority but it really simplify our work here.

      • Q&A New Functionality.

      About the error it's related to FileName. But we need more information to help more.
      Be careful about the other thread, it may happen that we add stuff and the line will not refer to the correct DataType anymore.

      Cheers,
      Manuel

      Dammit! No wonder my code keeps breaking... I can't even follow simple forum instructions lol! But hey, at least I got it down to one link. Haha, progress!

      Yes, Filename, that actually makes perfect sense, so I'm pretty confident I've conquered this error. You're right that I shouldn't rely too strongly on those line numbers. Hell, the next bug fix could make that info obsolete, let alone a reference from years ago. But sometimes you can glean value from outdated info. Maybe in the future line 277 doesn't refer to Filename, but if that error pops up and all my Filename calls look to be in good order I might be able to make some guesses that increase my luck in identifying the issue faster. For instance, I might look at string or static text parameters first, thinking that like datatypes may have been grouped together in the source code. If 329 doesn't refer to baselink in the future I might start looking at in/exclude boxes or shader links first for the same reason. Maybe the list of datatypes in the description manual in the sdk is ordered how they are in ge_container.h and I can use that to choose my next lead. Shot in the dark type stuff, for sure, but maybe gets me generally in the right direction.

      You know what, this may have to do with me tracking down a weird behavior where Init() seemed to be the cause of every parameter invoking a MSG_DESCRIPTION_POSTSETPARAMETER message on any parameter change... but I have both debugging AND pandemic fog on my brain, so I don't even know exactly which thing I was doing wrong that may have been the culprit, so there's no point getting into all that... doesn't help that a VS Code update seems to like breaking my local git repo's randomly, so that makes reviewing previous code fun. In any case, the error hasn't popped up at all after ensuring all my base container calls were proper and there were no missing parameter initializations. Not that it was a readily reproduce-able error, just one that would pop up sometimes after a heavy testing session... so I had to do a lot of those to ensure it's really gone.

      But it does raise a question. Since the Filename object doesn't exist in python, what's the best way to initialize it? Docs say it's a string, but I saw something in a forum post that someone was initializing their filename parameter directly through the base container. I figured this had to do with how the python wrapper is internally converting the path to Filename object or something, so that's what I've been doing, but that WAS from 2013. So which is the proper way to initialize it?

      self.InitAttr(node, str, c4d.ID_FILENAME_PARAMETER)
      

      or

      data.SetFilename(c4d.ID_FILENAME_PARAMETER, "")
      

      And even if it's the former, do I set its default value through the basecontainer or in the normal way like so:

      node[c4d.ID_FILENAME_PARAMETER] = ""
      

      @r_gigante said in Identify Critical Stop Please?:

      Hi @kvb, with regard to the scope and purpose of the CriticalStop I warmly recommend you to have a look at:

      • FAQ
      • CriticalStop()
      • Debug and Output Functions

      These pages properly explain how the CriticalStop is used in Cinema 4D, how it can be used in plugins and what to expect when it's hit.

      In you case, as already pointed out by @m_magalhaes , it's likely you're using a GeData::GetFilename() with a GeData whose type was not a Filename. Please recheck your code and if you don't spot the issue, provide us with an example or snippet to reproduce it.

      Cheers, R

      Thanks for the links. They'll come in handy on my next project, which will be in c++ (and I will be abusing the hell out of all the debugging options!).

      Could it have been caused by the filename returning None? Is that even a thing or just something I noticed while I was missing any initialization on my Filename parameter? I can't see it being anything else... unless an empty string could trigger that error?

      Thanks all!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • RE: Identify Critical Stop Please?

      Hey,
      Yeah, it is a bit vague, but that's because the only people who could possibly answer would know exactly what that is without any context behind it. It is in the crash report, btw, not the console, and other than a timestamp, this is the complete error code.
      Some examples of others encountering similar problems:
      https://developers.maxon.net/forum/topic/9850/13264_cinema-crashed-when-selecting-gradient
      https://developers.maxon.net/forum/topic/10339/13832_debugger-critical-stop-

      Yes, I'm properly initializing my attributes with the proper datatypes and getting/setting them in the appropriate fashion... though I may be missing one somewhere. I'll have to continue to scour my code... but knowing what this particular critical stop is referring to (like those other threads I linked) would be very helpful.

      Thanks!

      posted in Cinema 4D SDK
      K
      kvb
    • Identify Critical Stop Please?

      Anyone ever come across this... or a dev with direct access to the info care to shed light on this? I was just doing some final testing before sending this thing off and finally getting the code out of my head when this popped up:

      Critical: Stop [ge_container.h(277)]
      

      I know it means I'm accessing data in a base container incorrectly. But not knowing what kind (and lacking a proper debugger in python) it's becoming a real pain to reproduce and track this down. If I knew the data type in question it would certainly help narrow it down. I know from some threads here on the forum that if the error referenced line 247 it was a vector, and 329 is a baselink. Unfortunately, no one's come along with an error referencing line 277, so here I am:)

      Thanks!
      Kevin

      posted in Cinema 4D SDK python r21
      K
      kvb
    • RE: Generators, Materials, Undo's, Oh My!

      @m_magalhaes said in Generators, Materials, Undo's, Oh My!:

      hello,
      thanks a lot @zipit for all of your time πŸ™‚

      @kvb
      Maybe there are some cases where this will go wrong but we don't see them at the moment πŸ™‚
      We talked about it this morning and while we can't guaranty it's going to work we can't said it's bad.

      I'm not a big fan of the workflow but I'm happy if you are.

      Cheers,
      Manuel

      Thanks Manuel, I'll be sure to report any issues I encounter. I tried to mark your post as the correct response but mistakenly used the options sub-menu above your post, which marked my post as correct and I don't seem to be able to change it.

      Thanks!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • RE: Generators, Materials, Undo's, Oh My!

      @zipit said in Generators, Materials, Undo's, Oh My!:

      Hi,

      I did not read all your code, but first of all you should replace statements like doc = c4d.documents.GetActiveDocument() with something like doc = node.GetDocument(). The methode is BaseList2D.GetDocument, you need to retrieve the document your node is attached to, not the active document. The reason is that nodes are not only being execute in the active document, but also other documents, for rendering the document is getting cloned for example. In these cases you would operate on the wrong document.

      Apart from this: I am a bit surprised that you actually managed to sneak in some operations into an Undo action of the node with MSG_DESCRIPTION_POSTSETPARAMETER, sorry for my misleading info on that, but there is not much to say about this.

      This is very likely not intended by MAXON, so you will probably neither get advice nor support for this approach. The context seems somewhat safe, i.e. the chance that you accidentally add your operation to some other Undo context seems low, but since this is not documented and probably also not intended, there are no guarantees, especially considering the rather fragile nature that Undo stacks often have. I would encapsulate your AddUndo logic blocks by a condition that ensures that they are only executed on the main thread, to avoid any possible major f*** ups. Other than that there is not much that I would do, aside from not doing this at all.

      Cheers,
      zipit

      Yes, of course... I'm an idiot, haha. I think I even noticed that while tooling around the forums and totally spaced on making the change to my document calls.

      Ok, I found two more bits of information that make me feel more comfortable in my approach. I keep forgetting to also reference the C++ sdk when writing a plugin in python lol. First link is from there... the "Undo System Manual", actually... would certainly be nice if that made its way into the python docs;) Second link is Maxime confirming that wrapping other node changes with the built-in parameter undo is appropriate, including a link to an example that uses MSG_DESCRIPTION_POSTSETPARAMETER. While that example ends up sending a MSG_DESCRIPTION_COMMAND to another node, it was shared in the context of, and as a solution for, grouping other undo's into the built-in parameter undo's.

      https://developers.maxon.net/docs/cpp/2023_2/page_manual_undo.html

      https://developers.maxon.net/forum/topic/12493/undo-for-a-tagdata/2

      Hopefully, if I'm misunderstanding any of this or there's some other detail that's still missing from the equation, a dev will come along and correct the record. Otherwise, I'm feeling pretty confident in marking this one as solved.

      Thanks for all your help Zipit! You certainly helped me find some holes and I'll be sure to take your advice and shore this up so it's as formidable as Helm's Deep... ok, bad example... Helm's Deep without that blasted culvert!

      Thanks!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • RE: Generators, Materials, Undo's, Oh My!

      Here's some simplified code that hopefully illustrates how I'm approaching this. It's the function where I load and store the relevant image data into the generator's basecontainer (for later use in GVO), create a material, and make further alterations to it. There's also the Message function where I'm slipping my undo's into the node's built-in undo block. Is anything about this code wrong? Like I said, everything is working perfectly, but I know that just because something works doesn't mean it's necessarily correct. But the logic seems correct to me. C4D is giving me an open undo block in a function where I'm allowed to make scene alterations, where the preferred option of "avoid the undo's" results the most broken undo behavior, and then C4D conveniently closes the undo for me. Am I understanding this wrong?

      Thanks!
      Kevin

      def LoadImageData(self, node):
          """ This function is only ever called from within an
          open undo block in non-threaded functions.
          Stores image data into the generator's basecontainer
          and creates a material and inserts the image as a shader """
          doc = c4d.documents.GetActiveDocument()
          
          # Load the texture
          image_path = node[c4d.ID_USER_PROVIDED_IMAGE_FILE]
          if not image_path:
              return False
          bc = c4d.BaseContainer()
          bc.SetFilename(c4d.LOADTEXTURE_FILENAME, image_path.encode('utf-8'))
          tex = c4d.modules.bodypaint.SendPainterCommand(c4d.PAINTER_LOADTEXTURE, doc=doc, tex=None, bc=bc)
          if tex is False:
              return False
      
          # Save relevant image data to generator's basecontainer and close texture.
          doc.AddUndo(c4d.UNDOTYPE_CHANGE, node)
          node[c4d.ID_IMAGE_WIDTH] = float(tex.GetBw())
          node[c4d.ID_IMAGE_HEIGHT] = float(tex.GetBh())
          node[c4d.ID_IMAGE_NAME] = os.path.basename(image_path)    
          c4d.modules.bodypaint.SendPainterCommand(c4d.PAINTER_FORCECLOSETEXTURE, doc=doc, tex=tex, bc=c4d.BaseContainer())
      
          # Check for active material or create and set if None.        
          mat = node[c4d.ID_MATERIAL_LINK]
          if not mat:
              mat = c4d.Material()
              mat.SetName(node[c4d.ID_IMAGE_NAME])
              mat[c4d.MATERIAL_USE_REFLECTION] = False
              doc.InsertMaterial(mat)
              doc.AddUndo(c4d.UNDOTYPE_NEW, mat)
              node[c4d.ID_MATERIAL_LINK] = mat     
          
          # Set main image channel
          if node[c4d.ID_MAIN_IMAGE_CHANNEL] == c4d.ID_MAIN_IMAGE_CHANNEL_COLOR:
              chanID = c4d.MATERIAL_COLOR_SHADER
              mat[c4d.MATERIAL_USE_COLOR] = True
              mat[c4d.MATERIAL_USE_LUMINANCE] = False
          else:
              chanID = c4d.MATERIAL_LUMINANCE_SHADER
              mat[c4d.MATERIAL_USE_COLOR] = False
              mat[c4d.MATERIAL_USE_LUMINANCE] = True
      
          # Load the image into the channel's shader
          bitmap_shader = c4d.BaseShader(c4d.Xbitmap)
          bitmap_shader[c4d.BITMAPSHADER_FILENAME] = str(node[c4d.ID_USER_PROVIDED_IMAGE_FILE])
          mat[chanID] = bitmap_shader
          mat.InsertShader(bitmap_shader)
          doc.AddUndo(c4d.UNDOTYPE_NEW, bitmap_shader)
          node[c4d.ID_IMAGE_LOADED] = True
          
          return True
      
      def Message(self, node, type, data):
          doc = c4d.documents.GetActiveDocument()
          
          """ MSG_DESCRIPTION_INITUNDO and MSG_DESCRIPTION_USERINTERACTION_END included
              to illustrate how MSG_DESCRIPTION_POSTSETPARAMETER is received within
              an open undo block """
      
          if type == c4d.MSG_DESCRIPTION_INITUNDO:
              """ This message initiates a StartUndo() call. Marking the start of the built-in parameter undo block """
          
          if type == c4d.MSG_DESCRIPTION_POSTSETPARAMETER:
              """ This message is received between MSG_DESCRIPTION_INITUNDO and MSG_DESCRIPTION_USERINTERACTION_END.
                  Meaning it takes place during the open built-in parameter undo block. All undo's initiated
                  from within this message will be grouped with their respective parameter changes """
              
              """ Filename input on the generator object. Calls a function to load the image file, creating materials in the process.
                  Undo's occur within LoadImageData() """
              if data and data['descid'][0].id == c4d.ID_USER_PROVIDED_IMAGE_FILE:
                  if os.path.exists(node[c4d.ID_USER_PROVIDED_IMAGE_FILE]):
                      if self.LoadImageData(node, True) == False
                          return False
                      node.SetDirty(c4d.DIRTYFLAGS_DATA)
                  else:
                      return False
              
              """ Combo box to adjust material parameters """
              if data and data['descid'][0].id == c4d.ID_MAIN_IMAGE_CHANNEL:
                  if node[c4d.ID_IMAGE_LOADED] == True:
                      mat = node[c4d.ID_MATERIAL_LINK]
                      if mat:
                          doc.AddUndo(c4d.UNDOTYPE_CHANGE, mat)   
                          if node[c4d.ID_MAIN_IMAGE_CHANNEL] == c4d.ID_MAIN_IMAGE_CHANNEL_COLOR:
                              mat[c4d.MATERIAL_USE_COLOR] = True
                              mat[c4d.MATERIAL_USE_LUMINANCE] = False
                          else:
                              mat[c4d.MATERIAL_USE_COLOR] = False
                              mat[c4d.MATERIAL_USE_LUMINANCE] = True       
                      else:
                          return False       
              
              """ Combo box parameter that may create and add a camera to the scene """                   
              if data and data['descid'][0].id == c4d.ID_TEXTURE_PROJECTION:
                  if node[c4d.ID_TEXTURE_PROJECTION] == c4d.ID_TEXTURE_PROJECTION_CAMERA_MAPPING:
                      if node[c4d.ID_LINKED_CAMERA] is None:
                          cam = c4d.BaseObject(c4d.Ocamera)
                          image_name = node[c4d.ID_IMAGE_NAME]
                          if image_name and node[c4d.ID_IMAGE_LOADED] == True:
                              cam.SetName(image_name + " cam")
                              doc.InsertObject(cam, parent = node)
                              doc.AddUndo(c4d.UNDOTYPE_NEW, cam)
                              doc.AddUndo(c4d.UNDOTYPE_CHANGE, node)
                              node[c4d.ID_PLANESMART_CAMERA] = cam
                              fov = cam[c4d.CAMERAOBJECT_FOV]
                              cam.SetAbsPos(c4d.Vector(0, 0, -(node[c4d.ID_IMAGE_WIDTH] / 2 / math.tan(fov / 2)))) 
                              node.ChangeNBit(c4d.NBIT_OM1_FOLD, c4d.NBITCONTROL_SET)
                              node.SetDirty(c4d.DIRTYFLAGS_DATA)
                          else:
                              return False
      
          elif type == c4d.MSG_DESCRIPTION_USERINTERACTION_END:
              """This message initiates an EndUndo() call. Marking the end of the built-in parameter undo block"""       
      
          elif type == c4d.MSG_DESCRIPTION_COMMAND:
              """ Example of how I'm handling button input with undo's.  In this case creating render settings.
                  Uses a complete Undo block since it's received outside of the built-in parameter undo block. """
              if data and data['id'][0].id == c4d.ID_CREATE_RENDER_SETTINGS:
                  doc.StartUndo()
                  image_name = node[c4d.ID_IMAGE_NAME]
                  if image_name node[c4d.ID_IMAGE_LOADED] == True:
                      # Returns a tuple with a bool for pre-existance of render settings and the RenderData
                      rd_exists, rd = SearchRenderSettings(rs, image_name)
                      if rd_exists == False:
                          rd = doc.GetActiveRenderData().GetClone()
                      rd_bc = rd.GetDataInstance()
                      rd_bc[c4d.RDATA_XRES] = node[c4d.ID_IMAGE_WIDTH]
                      rd_bc[c4d.RDATA_YRES] = node[c4d.ID_IMAGE_HEIGHT]
                      # Have to set film aspect manually, not sure why
                      rd_bc[c4d.RDATA_FILMASPECT] = node[c4d.ID_IMAGE_WIDTH] / node[c4d.ID_IMAGE_HEIGHT]
                      if rd_exists == False:
                          # We don't want to inadvertently overwrite previous renders!
                          rd_bc[c4d.RDATA_PATH] = ""
                          rd.SetName(image_name)
                          doc.InsertRenderDataLast(rd)
                          doc.AddUndo(c4d.UNDOTYPE_NEW, rd)
                      doc.SetActiveRenderData(rd)
                  else:
                      return False
                  doc.EndUndo()
      
          return True
      
      
      posted in Cinema 4D SDK
      K
      kvb
    • RE: Generators, Materials, Undo's, Oh My!

      @m_magalhaes said in Generators, Materials, Undo's, Oh My!:

      Hello,

      I would like to understand what's the workflow behind your tool. Maybe you could jump on c++ and create a scenehook. (not hard at all)
      what kind of OS function are you calling ?

      For your next threads, please help us keeping things organised and clean.

      • Q&A New Functionality.
      • How to Post Questions especially the tagging part.

      I've added the tags and marked this thread as a question so when you considered it as solved, please change the state πŸ™‚

      Thanks! And apologies for not reviewing the forum rules, been a hot minute since I've posted and it was one of those "You've revised this a dozen times, if you don't post it now you'll be revising it forever" kind of things.

      Would love to be able to utilize a scenehook! But it's way too late to re-write this in C++:(

      @zipit said in Generators, Materials, Undo's, Oh My!:

      Hi,

      maybe I was unclear on that, but you cannot incorporate your scene modifications into an Undo created by Cinema 4D for the modification of a parameter of a node. You will always end up with two Undos, one created by Cinema 4D for the parameter modification and one created by you for the scene modifications. Because of the ambiguous scene state stuff I described above there is also the chance you might send the user into a loop ("... your generator node adds a material and an Undo for it, user invokes Undo, your generator is still in the same state, so it adds a material and an Undo for it, ...").

      On the material creation: If you need an unknown amount of up to n materials, but know n, you could just always create n materials and unhide/use them on a as needed basis. But you do not have to create all materials on instantiation. Technically you could also delay that by escaping the threaded context of GVO by for example using messages and a MessageData plugin. But see the last paragraph on that.

      On invoking OS functionalities from a threaded context: There isn't anything special about it in Cinema, at least I am not aware of anything special. It also applies to all data access for NodeData methods that are executed asynchronously, not just calls to the OS. If you want to access the same block of data from multiple asynchronously executed functions, you will need a semaphore for doing that safley.

      I would also keep in mind that generators have intentionally been designed for asynchronous execution and to encapsulate a part of a scene graph. So if you find yourself constantly bending and circumventing these concepts, you might be better of with another plugin type, like CommandData, ToolData or MessageData.

      Cheers,
      zipit

      But that's the thing... it is working! I'm getting perfect undo behavior, without any errors. I'm not creating a new undo block, I'm slipping extra AddUndo() calls between (and only ever between) the built in Start/EndUndo() calls of the node. I only get the behavior you describe (needing two or more undo's) if I add my own Start/EndUndo() calls.

      It's like, if this were a c++ problem it would be like I'm asking about some warnings I'm getting at compile. Usually fine, but definitely better if there are as few of them as possible. Except the warnings are coming from my head in the form of second guessing myself lol. I'm just here making sure I'm doing as little "wrong" as possible;) I can definitely see why Undo's are highly discouraged in node based plugins, as you can get yourself in trouble very easily if you're not careful. But I think I'm doing it safely, and even if it's not expressly enumerated in the sdk, according to its design... I think (why I'm here lol).

      The only os calls I'm making are os.path.basename() and os.path.exists(). As far as the potential number of materials needed, n > the number of dummy materials I feel comfortable making per instance of my plugin objectπŸ™‚

      Thanks!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • RE: Generators, Materials, Undo's, Oh My!

      Hey Zipit, thanks for the detailed reply! It really helps my understanding of all the little intricacies of the sdk;)

      Unfortunately, I don't think I can eliminate all of the undo's I need using GVO. I can't anticipate all the materials that will be needed given that their creation is based on user input through a filename parameter on my generator, so I can't create them during instantiation. That leaves me very few options since I can't create them during GVO.

      So how bad are those undo's? Because regardless of the material creation aspect I still have file i/o and os function calls to be concerned about (allowed during execution but I think os function calls are discouraged?). If those are fine I suppose I have a workaround if it's possible to create materials in the same manner as the virtual objects. The user wouldn't have access to them in the current scene, but I could open up the material freedom through specific override links. A bit more limiting than my current implementation, but might as well consider it if it's possible.

      I also have a couple of buttons that create a camera and render settings, those also include undo's (reacted to in MSG_DESCRIPTION_COMMAND, full start->add->end blocks since it lies outside of the built in node undo's).

      Thanks!
      Kevin

      posted in Cinema 4D SDK
      K
      kvb
    • Generators, Materials, Undo's, Oh My!

      Hey Folks!
      Two questions followed by the context.

      • Where's the best place to add/update materials when those changes are initiated from the parameters of a generator object plugin? MSG_DESCRIPTION_POSTSETPARAMETER? MSG_DESCRIPTION_CHECKUPDATE? SetDParameter()? Somewhere else?

      • Is it correct that in some instances the Start/EndUndo() calls aren't necessary, such as in the case of the previously mentioned messages/functions, since they are received/executed between MSG_DESCRIPTION_INITUNDO and MSG_DESCRIPTION_USERINTERACTION_END?

      I have a generator plugin written in python that has a few parameters that are meant to deal with generating/adjusting materials (a filename parameter and a couple of combo boxes). Grouping the creation/adjusting of materials with their parameter change into a single undo was proving difficult until I stumbled upon the above info on the exception to the "rule of undo's" (must call Start/EndUndo()).

      Then there were the crashes and the errors related to baseundo.cpp (I've included those below).

      Critical_Log
      	{
      		[12:41:19] CRITICAL: Stop [baseundo.cpp(1857)]
      		[12:41:19] CRITICAL: Stop [baseundo.cpp(1956)]
      		[12:41:19] CRITICAL: Stop [baseundo.cpp(1956)]
      		[12:41:19] CRITICAL: Stop [baseundo.cpp(1897)]
      		[12:49:45] CRITICAL: Stop [baseundo.cpp(1857)]
      		[12:49:45] CRITICAL: Stop [baseundo.cpp(1956)]
      		[12:49:45] CRITICAL: Stop [baseundo.cpp(1956)]
      		[12:49:45] CRITICAL: Stop [baseundo.cpp(1897)]
      	}
      

      I had myself convinced it was all related to those material affecting parameters and that I had misunderstood the way undo's work with parameter changes, which led me to the two questions above. In the end it didn't even have anything to do with any of that, just something stupid I was doing in Execute() until I rediscovered the threading information page in the sdk. So the problem, technically, is all fixed, but after all the experimenting while on the "wrong track" I'm left with 3 different places where I could put the code for those material changes and I'm just not sure which would be best.

      Thanks!
      Kevin

      posted in Cinema 4D SDK python
      K
      kvb