Python Generator creating Text object, then getting bounding box
-
Hi all,
I am new to programming in C4D, but not programming in general. Trying to wrap my head around an issue.
I have a Python Generator (which may at some point be turned into a full plugin, if it turns out to have any value) with a scripts that generates some text objects (fully extruded text objects, not just splines), depending on user data settings that the user can play with.
The objects are generated as expected, and become visible in the scene after my script returns the root object. However, the several text objects are all right on top of each other. I can set their relative position easily, using user data values, or calculated values, but I want to go one step further; I want to lay them out in relation to each other.
Which means that once they are generated, I need to get the bounding box of each of the text elements.
I have understood that I can get the size in three dimensions using something like
text.GetRad() * 2
However, this always returns an empty vector to me ([0,0,0]).
If I make a text object in the Object Manager (without making it editable), and access it in the console using something like
Text.GetRad()
I get a vector accurately reporting the extents of the object, which I could use with The Math to do the calculations I need to.This tells me that I don't need to worry about making them editable.. So didn't they really get a bounding box yet, even if I added them to the scene? Do I need to let Cinema 4D perform a run loop or something in order to process my objects?
I did see a web page mentioning doing something like this:
doc.ExecutePasses(None, True, True, True, c4d.BUILDFLAGS_INTERNALRENDERER)
But the only result of running that command is that Cinema 4D freezes for several minutes, so that feels like the wrong thing to do.
I've also seen some bits about getting a cached version - GetCache() returns "None" for me it seems. The text object has no children.
Is there a surefire way to get the bounding box of a text object inside the generator function, before it returns?
Thanks for any insights!
-
Hello @havremunken,
welcome to the Plugin Cafรฉ and thank you for reaching to us. We would recommend reading the Forum Guidlines for future questions, as they go over the form of a question and the forum features as for example Ask-as-question. I have marked your posting as a question in this case for you. It would also be best if you would provide more code for future questions, as this often makes questions less ambiguous.
With that being said, from our understanding you want to retrieve the bounding box radius of some objects. But you receive the null vector for objects where it should not be the null vector. You talk about a Python Generator, which could either refer to the pythonic concept of generators, i.e., a function that does
yield
, or to the Cinema 4D concept of a Python Generator object which does build a cache for an object in a scene with Python. We assume the latter is the case. We also assume the problems you do encounter happen in themain()
function of said Python Generator object.ObjectData
plugins that are generators and the Python Generator object build caches for objects in the object manager. You can think of the return values ofObjectData.GetVirtualObjects
,ObjectData.GetContour
and themain()
function of a Python Generator Object as being an invisible part of your scene hierarchy. The trick here is that caches can also contain caches. So, you can write a Python Generator object that just returnsreturn c4d.BaseList2D(c4d.Ocube)
. Which means that you return an object which contains instructions on how to build that object, not the actual finished object itself.The evaluation of caches is done when an object is being inserted into a scene and Cinema 4D then executes the cache pass on it. When you simply instantiate an object, its caches will not be built. As you have already found out yourself, you can manually build caches with
BaseDocument.ExecutePasses
, but you do not disclose whatdoc
is. It should not be the document the Python Generator object is part of, but a temporary document. Depending on what you are trying to do, you also cannot return these objects you did build the caches on as the cache of your Python Generator. Technically it is fine, but when you want to modify them after you have built the caches on them, these changes will not be reflected in the cache of your object, as Cinema will not rebuild caches for objects which already provide fully built caches in their cache building methods. Find at the end of the posting a short example script which outputs its own height in its cache as a text spline.Cheers,
FerdinandThe result:
The code:"""Example for cache-building which evaluates its own output. To be run as a Python Generator object script in Cinema 4D. """ import c4d def main(): """ """ # Create the spline and the extrude object. spline = c4d.BaseList2D(c4d.Osplinetext) extrude = c4d.BaseList2D(c4d.Oextrude) if None in (spline, extrude): raise MemoryError("Could not allocate objects for cache.") # Setup the little rig and set the text of the spline to a test value # so that we can measure it. spline.InsertUnder(extrude) spline[c4d.PRIM_TEXT_TEXT] = "Height: 12345" # Create a temporary document, insert a clone of the rig and execute the # cache pass on it. temp = c4d.documents.BaseDocument() clone = extrude.GetClone(c4d.COPYFLAGS_NONE) temp.InsertObject(clone) temp.ExecutePasses(None, False, True, True, c4d.BUILDFLAGS_NONE) # The caches have been built at this point, so we can measure the clone, # which relies for generators on the cache. Then write the height of the # text into the original spline. cloneSpline = clone.GetDown() spline[c4d.PRIM_TEXT_TEXT] = f"Height: {cloneSpline.GetRad().y}" # And return the rig as the cache for the Python Generator object. return extrude
-
Hi Ferdinand,
Thank you very much for your reply! I tried marking the post as a question but I guess I messed up. Will do better next time, and include code.
You are right - I am using the Python Generator object in Cinema 4D as a starting point, and my work is being done in main() or in functions being called from main().
In my case, I am building quite a few text objects that I need to lay out - a common case would be in the order of 180 - 250 of them. I'm basically building this script/plugin because I don't want to have to do that manually.
I really appreciate your code example. I will adapt this to my case and use it, seems straightforward enough. But I do have a few questions that I hope you could answer, to help me understand this better.
First question: Efficiency
In your example, everything is fairly simple. A spline in an extrude in a new document, execute the passes, get the info. Simple as can be. However, as mentioned, I can have MANY text objects, and I need to collect things like max width and max height out of all of them. What would be most efficient?
A. For each of the original text objects, create a temp document, insert a clone, execute cache pass, GetRad.
B. Create temp document only once, loop through original text objects, inserting a clone of them, executing cahce pass, GetRad.
C. Create temp document only once, dump all text object clones at once, execute cahce pass only once, loop through and GetRad.
My normal understanding of programming would lead me to believe option C is the most efficient. Only one document would be created, and we would execute the cache pass only once, albeit with more clones populated into the document. Resource wise it seems like a no-brainer, but I have no idea how C4D is coded and optimized. Would it even make a difference? Could option C make things worse in certain situations?
And additionally: Do I need to dispose of the temporary BaseDocument created when I am done, or will it simply disappear when the main() method completes and it presumably goes out of scope?
Second question: Recreating objects
As mentioned, my script is right now inside a Python Generator C4D object. It has some user data that acts as parameters for the objects being created. Some of these are simple layout parameters, such as horizontal and vertical "padding" that will be added on the respective sides of the bounding box of each object as part of my layout pass.
Changing these values in theory should only move the objects, not recreate them. However, there is no way Cinema 4D can know that - the only thing it knows about is my main() function. Whenever anything changes, it is called, which causes the whole structure to be recreated from scratch. This works, of course, but it means that if I pull on a slider, for each mouse movement main() is called and I generate ~200 objects. The user experience of this is... sluggish.
Is this an inherent limitation of the Python Generator C4D object? Or is there a way to say "If THESE parameters change, rerun all of main, but if only THESE parameters run, call another function that will just adjust the layout"?
Would this be the same if I moved from the Python Generator C4D object to a "full" python plugin? Could I then split the plugin instance into "functionality that generates geometry" and "functionality that arranges and lays out the already created geometry"?
Thank you very much for taking the time to help a n00b get going with this!
And a happy holiday season to everyone reading this.
-
Hey @havremunken,
@havremunken said in [Python Generator creating Text object, then getting bounding box]
A. For each of the original text objects, create a temp document, insert a clone, execute cache pass, GetRad.
B. Create temp document only once, loop through original text objects, inserting a clone of them, executing cahce pass, GetRad.
C. Create temp document only once, dump all text object clones at once, execute cahce pass only once, loop through and GetRad.
It is C, you only need one temporary document, and you can build all the caches for multiple objects at once. In my example above there are technically already two caches being built, one for the extrude object and one for the spline. If you care about performance, you probably also do not want to follow my example and only build the caches for what you need. I.e., you should just insert the spline. The cloning of objects is also only required when you want to build another cache based on the result of the temporary cache build, e.g., me writing back the height in my example. When you leave out the cloning and build the caches directly for
extrude
and then runspline[c4d.PRIM_TEXT_TEXT] = "Blah"
you will see that your final result still says"Height: 12345"
as Cinema will see that you returned a fully populated cache and ignore it, even though you changed the text from"Height: 12345"
to"Blah"
. Caches can be complex, and we currently do not have a manual which explains them well, but you can either run the C++ Active Object plugin from our C++ SDK to gain insight or read the posting and use the little script I once posted here.And additionally: Do I need to dispose of the temporary BaseDocument created when I am done, or will it simply disappear when the main() method completes and it presumably goes out of scope?
What technically will happen is that once the scope of
main()
is left, the reference count oftemp
will be zero. The Python garbage collector would then technically deallocatetemp
at some point. With our C++ SDK backend and how we especially handle scripting objects like the Python Generator object things get more complicated, but there is no amazing insight to be gained here. Just treat it like PythonChanging these values in theory should only move the objects, not recreate them. However, there is no way Cinema 4D can know that - the only thing it knows about is my main() function [...]
No, caches should be built every time from scratch. You can get the cache of the thing you are building a cache for with
op.GetCache()
in the scripting object andnode.GetCache()
in a plugin, but that is usually only done to evaluate if the cache must be rebuild at all, not to modify it. If it should not be rebuilt, then you simply return what you received fromGetCache()
. There is technically little which prevents you from modifying an existing cache, but that is then out of scope of support, and you must know yourself what you are doing. What is often also underestimated is how often Cinema 4D reallocates scene elements. It happens all the time. So, if you build your cache once more when the user clicked "a little bit to the left, please" does not matter. It is more important that you do not rebuild the cache for the 100 cache building calls in between these two clicks where the input data is exactly the same. But Python has already cache throttling build in with ObjectData.OptimizeCache() and the Python Generator object options. There are some other techniques with HierarchyHelp but they are only feasible in C++.Cheers,
Ferdinand -
Many thanks for your help and thorough explanations, Ferdinand! Much appreciated!
I'll go create a temp document and clones, and don't expect much more resistance from this particular issue. Thanks again, and have a happy holiday season!