C4D Parent constraint python
-
@iplai Thank you.
That seems to work. I assume c4d is doing this under the hood when we do the parent constraint manualy.
This is probably something one need to consider regarding other constraint aswell i assume.Appreciate the comments in the solution code.
-
Hey @mats,
Thank you for reaching out to us and please excuse the radio silence from our side. We were quite busy with the 2023.1 release in the last week, but I will have a look at your problem tomorrow.
Cheers,
Ferdinand -
Hey @mats,
I ran your code, and I cannot reproduce your problem. The line
tag( [c4d.ID_CA_CONSTRAINT_TAG_PARENT] = True
is not really wrong but makes not too much sense as you are calling thereC4DAtom.__call__
, the line should betag[c4d.ID_CA_CONSTRAINT_TAG_PARENT] = True
.I also added undo steps to your script, but it was working without them for me too. Maybe I am misunderstanding what you mean with 'When doing it manually, I get no offset on the objects, they all stay in place as intended.', but for me there is no 'offset'.
I ran the code on
2023.0.0
and2023.1.0
to the same effect, find my code below. If the problem does persist for you, I will have to ask you to explain what you would consider wrong with the result.Cheers,
FerdinandThe result:
The code:
"""Constraints the two currently selected objects with a parent-child constraint. """ import c4d import typing doc: c4d.documents.BaseDocument # The active document op: typing.Optional[c4d.BaseObject] # The active object, None if unselected def main() -> None: """ """ # Get the object selection in order and bail when not exactly two objects are selected. selection: list[c4d.BaseObject] = doc.GetActiveObjects( c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER | c4d.GETACTIVEOBJECTFLAGS_CHILDREN) if len(selection) != 2: raise RuntimeError("Please select exactly two objects.") # Unpack the selection into a parent and child variable, rename the two objects, and add undos # for the operation. parent, child = selection doc.StartUndo() doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent) doc.AddUndo(c4d.UNDOTYPE_CHANGE, child) parent.SetName(f"{parent.GetName()} (Parent)") child.SetName(f"{child.GetName()} (Child)") # Create the tag. tag: c4d.BaseTag = child.MakeTag(c4d.Tcaconstraint) if tag is None: raise MemoryError("Could not allocate tag.") # Add an undo for the tag being added (must be done after the operation) and add another undo # for the tag being modified (not really necessary here, but good form). doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, tag) doc.AddUndo(c4d.UNDOTYPE_CHANGE, tag) # Constraint #child to #parent. tag[c4d.ID_CA_CONSTRAINT_TAG_PARENT] = True tag[30001] = parent doc.EndUndo() c4d.EventAdd() if __name__ == '__main__': main()
-
@ferdinand @Mats
I compared the two pieces of code and found the key diffence is the lines of creating the Constraint Tag:
When you usingMakeTag
, c4d will do some internal operations I guess:tag: c4d.BaseTag = child.MakeTag(c4d.Tcaconstraint)
While using
BaseTag
constructor, thenInsertTag
, no internal operations are done:tag = c4d.BaseTag(c4d.Tcaconstraint) child.InsertTag(tag)
In my experience, for the most cases of tags, both of them work properly, apart from the above exception.
-
Hey @iplai,
That seems unlikely because all the method does, is free you from the burden of having to insert the tag yourself.
and when I change the relevant portion of my script to this,
tag: c4d.BaseTag = c4d.BaseTag(c4d.Tcaconstraint) if tag is None: raise MemoryError("Could not allocate tag.") child.InsertTag(tag)
the outcome is still the same. As I said, there is no real substantial difference between my code and the one from @Mats, I just rewrote the script to be cleaner.
If anything could cause issues here, then it is the missing context guard of @Mats script. The context guard is not irrelevant, and you must use it, as it prevents the code from being executed outside of the
__main__
context. But I do not really see how this should come into play here, since the Script Manager is executing the whole module.Even when I use the code from @Mats, everything is working fine for me.
Cheers,
Ferdinand -
@ferdinand
I verified the scene again, the two methods do the same thing indeed. Forgive my mistakes last post.
The problem is when the parent is not at original position, after apply the script, the offset arises.
The selection order might also affect the result. I'm trying to figure it out. -
Hey @iplai,
Thank you for your help. Yes, that is indeed it. The constraint tag seems to be writing the inverse offset of the parent global transform into the constraint offset when the parent is set manually.
You can emulate this by adding this line to the script:
data: dict[str, c4d.DescID] = {"id": c4d.DescID(c4d.ID_CA_CONSTRAINT_TAG_PARENT_LOCALTRANFORM_UPDATEBUTTON)} tag.Message(c4d.MSG_DESCRIPTION_COMMAND, data)
Find the full script below.
edit:
Cheers,
FerdinandThe full script:
import c4d import typing doc: c4d.documents.BaseDocument # The active document op: typing.Optional[c4d.BaseObject] # The active object, None if unselected def main() -> None: """ """ # Get the object selection in order and bail when not exactly two objects are selected. selection: list[c4d.BaseObject] = doc.GetActiveObjects( c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER | c4d.GETACTIVEOBJECTFLAGS_CHILDREN) if len(selection) < 2: raise RuntimeError("Please select at least two objects.") # Unpack the selection into a parent and child variable, rename the two objects, and add undos # for the operation. parent, child = selection[:2] doc.StartUndo() doc.AddUndo(c4d.UNDOTYPE_CHANGE, parent) doc.AddUndo(c4d.UNDOTYPE_CHANGE, child) parent.SetName(f"{parent.GetName()} (Parent)") child.SetName(f"{child.GetName()} (Child)") # Create the tag. tag: c4d.BaseTag = child.MakeTag(c4d.Tcaconstraint) if tag is None: raise MemoryError("Could not allocate tag.") # Add an undo for the tag being added (must be done after the operation) and add another undo # for the tag being modified (not really necessary here, but good form). doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, tag) doc.AddUndo(c4d.UNDOTYPE_CHANGE, tag) # Constraint #child to #parent and write the inverse offset of #parent as # the constraint offset. tag[c4d.ID_CA_CONSTRAINT_TAG_PARENT] = True tag[30001] = parent data: dict[str, c4d.DescID] = {"id": c4d.DescID(c4d.ID_CA_CONSTRAINT_TAG_PARENT_LOCALTRANFORM_UPDATEBUTTON)} tag.Message(c4d.MSG_DESCRIPTION_COMMAND, data) doc.EndUndo() c4d.EventAdd() if __name__ == '__main__': main()
-
@ferdinand
The Constrain Tag may not only influence position, but also rotation, scale. I think theset inital state
button should be called as mentioned in my first reply:c4d.CallButton(tag, c4d.ID_CA_CONSTRAINT_TAG_SET_INITIAL_STATE) # SET INITIAL STATE
tag[30009,1000] = -parent.GetRelPos() #? tag[30009,1001] = -parent.GetRelScale() #? tag[30009,1002] = -parent.GetRelRot() #?
-
Hey @iplai,
to cover the case where either both the parent and child or only one of them is embedded in a hierarchy, i.e., where the local matrix is unequal to the global matrix, or when you generally want to address the other transforms as scale or rotation, and do not want to do the math yourself, it is best to send a description message for
ID_CA_CONSTRAINT_TAG_PARENT_LOCALTRANFORM_UPDATEBUTTON
to the tag, as this is was effectively triggers the method computing these values. This will then cover all cases:I have updated my answer above.
Cheers,
Ferdinand -
@ferdinand
Thanks, you are the best. This is exactly the internal operation I'm finding.