How to get FONT information and use it to create text objects in C++?
-
Hi,
I will try to explain just enough but not too much what I need this time.
In my classic API ObjectData generator plugin I have a font setting + height. There are more settings in real life, but they are not relevant for this example.CONTAINER Otestobject { NAME Otestobject; INCLUDE Obase; GROUP ID_OBJECTPROPERTIES { FONT C2T_FONT { OPEN; } REAL C2T_FONT_HEIGHT { UNIT METER; } } }
C2T_FONT is defined to be 1006, if it matters, and the height one 1007.
In the GetVirtualObjects method for this generator plugin, I create text objects. Right now I do one of the following:- Clone a Text Spline object.
- Clone a MoGraph Text object.
- Create a new Osplinetext object in code.
For this purpose I will focus on #3, as I am moving in that direction. For the future, I am also considering going a bit lower level and deleting 1 and 2, and replacing 3 with using GenerateText() to create the text splines directly instead of cloning an object.
My question is simply - how can I take the font information that the user has configured in the Attribute Manager for my object and use it in the creation of the text object when
- I am creating a new Osplinetext object like
BaseObject::Alloc (Osplinetext);
- Using the BaseObjects'
GenerateText()
function.
I am assuming both these methods of creating a text spline would need the same kind of parameters, but I have had a hard time finding out from the documentation how I am supposed to "feed" them the settings in order to get it right.
Thanks for any insights!
-
Hello @Havremunken,
Thank you for reaching out to us. I am not 100% sure I understand your question.
- In a
GetVirtualObjects
function you should prefer other cache building objects over something likeGenerateText
(which is a static function and not a member ofBaseObject
btw.), as this will leave the user with something more configurable when CSTO'ing your object. It also defers building that nested part of your cache to when it is actually needed. Osplinetext
will just take yourFontData
defined asC2T_FONT
directly in its PRIM_TEXT_FONT parameter.- When the Cinema API talks about a 'font container', e.g., for
cp
inGenerateText
, it usually means theBaseContainer
returned by FontData.GetFont. There are also some other ways on user areas and clip maps to retrieve such font information, which is why this terminology came to pass.
Cheers,
Ferdinand - In a
-
Hi Ferdinand, and thanks for your reply. I am 100% sure you hit the nail on the head in point 2. But let me reply by points.
-
Thanks for the input. In the fullness of time all the text splines will be put under an extrude object anyway, but you are of course right that using a text spline object will make it easier to change later. I aim to make it beneficial for a user of my plugin to keep it procedural as long as possible, but of course you have a valid point that you never know when the user will suddenly decide to press C.
-
This looks like exactly what I was looking for. Does this mean I can do this in two simple steps like
myPluginsBaseContainer->GetData(C2T_FONT)
and thennewTextSplineObjectsBaseContainer->SetData(PRIM_TEXT_FONT)
without doing a roundtrip through the FontData stuff? I.e. it doesn't matter what "shape" the font data is in, using GetData and SetData like this should just copy it correctly across to the new object? Or would I still need to roundtrip with GetFont and SetFont here? -
Thanks for the information. This could become useful for me as I need to calculate the "x-height" (literally the height of the lower case x) of fonts for layout purposes and to know where to put the baseline of text. In theory this could mean that the user ends up with many different fonts that needs to be aligned (and by aligned, I mean placed vertically in a manner that gives the least amount of "that looks wrong"). Having to calculate this for what could easily be a hundered or more text object is a waste, so I may want to "cache" that by checking if I already calculated the x-height for this combination of font and size already. In that case getting enough information about the font to uniquely identify it could mean that FontData.GetFont and understanding the data it contains would be useful.
-
-
Hey @Havremunken,
Does this mean I can do this in two simple steps like myPluginsBaseContainer->GetData(C2T_FONT) and then newTextSplineObjectsBaseContainer->SetData(PRIM_TEXT_FONT) without doing a roundtrip through the FontData stuff?
Close, but the copying behaviour of Cinema gets in the way with that. While something like
BaseContainer::GetContainer(i)
returns a copy of the container ati
,GetData/GetCustomDataType<T>
, andGetParameter
return a reference to the data ati
. So, we must manually draw a copy.I personally would use direct node parameter access, because there data will be copied out of the box for you. As untested pseudo-code:
iferr_scope; // Get a reference to the data of the node, i.e., that IS the data used by the node. BaseContainer bc = op->GetDataInstanceRef(); // Get the FontData from the node, this is unfortunately not a copy but a reference to the actual // data used by the node. GeData data; bc.GetParameter(ConstDescID(DescLevel(C2T_FONT), data)); // Draw a copy of it. GeData copy1 (data); // Set the font data in another's nodes data container. otherBc.SetParameter(ConstDescID(DescLevel(SOME_FONT_PARAM), copy1)); // The better route is IMHO to use direct Get/SetParameter access on the node. Here we do get a copy. GeData copy2; if (!op->GetParameter(ConstDescID(DescLevel(C2T_FONT)), copy2, DESCFLAGS_GET::NONE)) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not access font data."_s); if (!otherNode->SetParameter(ConstDescID(DescLevel(SOME_FONT_PARAM)), copy2, DESCFLAGS_SET::NONE)) return maxon::UnexpectedError(MAXON_SOURCE_LOCATION, "Could not set font data."_s);
Cheers,
Ferdinandedit: Okay, I checked,
FontData.Get/SetFont
does not do any copying whatsoever (I deleted the section in my code above which talked about it). It is therefore not a valid way to copy fonts. What you would have to do, is this:BaseContainer bc = fontData.GetFont(); BaseContainer copy (bc); otherFontData.SetFont(copy);
I have added a little warning on
FontData
, to highlight this behaviour ofGet/SetFont
. -
Thanks, Ferdinand! This removes most of my need for depending on references to other text objects to allow the user to customize font properties. I'll dive into this and verify that I can get this to work once I get in front of my C4D computer later today!
-
Ok, I tested the "better route" one to see if I could do it. I inserted the following code right after cellObj has been created (
BaseObject::Alloc(Osplinetext)
).// Start experiment auto obj = reinterpret_cast<BaseObject*> (this); if (obj != nullptr) { GeData copy; if (!obj->GetParameter (ConstDescID (DescLevel(C2T_FONT)), copy, DESCFLAGS_GET::NONE)) ApplicationOutput ("Could not GetParameter from 'us' object.@"); if (!cellObj->SetParameter (ConstDescID (DescLevel(PRIM_TEXT_FONT)), copy, DESCFLAGS_SET::NONE)) ApplicationOutput ("Could not SetParameter to cellObj.@"); } GeData numbah; numbah.SetFloat (50); cellObj->SetParameter (ConstDescID(DescLevel(PRIM_TEXT_HEIGHT)), numbah, DESCFLAGS_SET::NONE); // End experiment
The thing with setting the smaller height towards the end is to verify that the code is being run. It is (the produced text objects are 1/4 the size of the default 200 height), the ApplicationOutputs don't trigger so both GetParameter and SetParameter succeeds as such (and I have verified in the debugger that the code is going through the "happy path", executing what I want). However the generated text stays in the default font.
Is PRIM_TEXT_FONT the wrong parameter to set? Or am I misunderstanding some other fundamental thing here?
I tried looking into the BaseContainer of the created cellObj, and it does contain a parameter with ID 2117, which corresponds to PRIM_TEXT_FONT, so it IS there, leading me to wonder if I am setting the wrong thing.
Any insights welcome, thanks!
-
Hello @Havremunken,
Your question is quite ambiguous as you do not provide executable code. I am assuming you are still inside
ObjectData::GetVirtualObjects
. So, in general this works and is very straight forward as demonstrated by the little Python Generator object demo shown below.But what stands out in your code, is this line:
auto obj = reinterpret_cast<BaseObject*> (this);
As this can never be a valid statement in public Cinema API code as you cannot own the implementation of anything you can (legally) reinterpret cast into a
BaseObject
. So, unless you have there some really weird code likeGeListNode* this = op
which shadows the implicitthis
pointer, this does not make any sense, sincethis
would be then of typeObjectData
, i.e., the plugin hook, which is not at an inheritance relation or even in the same memory location as node representing that hook, i.e., theBaseObject
.When you want to get the node associated with a
NodeData
hook, you can just call NodeData::Get, in mostNodeData
and derived types functions you also get passed in the node. E.g.,GetVirtualObjects
gets passed inop
, theBaseObject
which is representing the called hook.Because
this
must be by defintion not null when a method runs, andreinterpret_cast
disables all safety checks,if (obj != nullptr)
should always eval astrue
. But sinceauto obj = reinterpret_cast<BaseObject*> (this);
is undefined behaviour,obj
will be just some garbage memory, the!obj->GetParameter
call should then result in a crash.Aside from your problem at hand:
- never use
reinterpret_cast
unless explicitly required (which more or less just the case when you get message data as a void pointer). - Printing text and having subsequent code are very poor means of debugging. See Debugging the SDK for a beginner friendly overview of how to attach a debugg to Cinema 4D I even used
GetVirtualObjects
as an example ).
I could speculate here more, but that would all be a bit pointless, as I am not knowing what you are doing exactly. The TLDR is: The code you have given us should run but crash Cinema 4D.
Cheers,
Ferdinand - never use
-
Hi Ferdinand,
Thanks for getting back to me. I'll change the reinterpret_cast - to be honest that was suggested by ReSharper (an addon for Visual Studio) as an "improvement" to my old C-style (BaseObject*) cast, so I just went with it. This call was in a method on the plugin object called by GetVirtualObjects that didn't have the BaseObject* as a param, that is why I tried a quick and dirty way to access the BaseContainer.
None of the code above is production code, of course, just an experiment to see if I could set the font. And the BaseContainer for the cellObj was given the PRIM_TEXT_FONT after running this code. And just for completeness, this didn't crash Cinema 4D. But that is perhaps because I am running it in the debugger in the first place.
I will rearchitect a bit and clean it up, hopefully that should make the text object accept the new font settings.
-
Just to be clear, you should not do any casting on
this
^^Either use the passed in
BaseObject
op
. Or when you are somewhere else, where no node is being passed in (very rare), it would be this, asGet
returns a genericGeListNode
.BaseObject* const op = static_cast<BaseObject>(Get());
And, yes, C++ style casting is a good thing, we strongly recommend it. But other than
static_cast
andconst_cast
,reinterpret_cast
does not offer any safety features, its whole point is being an unbound and unsafe cast which can produce total garbage.The advantage of
reinterpret_cast<T>(foo)
over a C-style(T)foo
is thatreinterpret_cast
makes it verbose that the author intentionally meant an unsafe cast here, e.g., castingvoid*
to aBaseObject*
, while(T)foo
could also be something which is meant to be a safe cast, e.g.,static_cast
, which is just obfuscated by a non-verbose C-style cast.Cheers,
Ferdinand -
I can confirm that you were right (nothing unexpected there!) - this is the second time in a couple of weeks where casting has bit me in the behind. I guess I was too spoiled by C# and need to keep my head turned on in the future..
For anyone from the future reading this: The problem with the code above was READING the value, not WRITING it back to the object. That part was ok. Either passing on for instance
op
as a parameter or usingGet()
(if you're on the right object) to get at it is the right move.Thanks again!