UNSOLVED C4D Parent constraint python

Hi.

I am trying to make a little script that will parent constraint first selected object to second selected object (only selecting 2 objects)

The code so far is this:

import c4d
from c4d import gui

objList = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER | c4d.GETACTIVEOBJECTFLAGS_CHILDREN)


tag = c4d.BaseTag(c4d.Tcaconstraint)
objList[0].InsertTag(tag)
tag()[c4d.ID_CA_CONSTRAINT_TAG_PARENT] = True
tag[30001] = objList[1]
c4d.EventAdd()

The issue however is a slight offsett in the position of the child object when running this script.
I tested doing it manualy - apply tag, activate parent, select object as target. When doing it manualy i get no offsett on the objects they all stay in palce as intended. BUT!
The local offsett get populated with values. While when running the script these values instead of getting applied in the Local offset of the tag, it gets populated in the freeze transform in the coordinates on the object.....

So the question is, am i missing something in this script to prevent it from nuddging?

  • Cinema 4D 2023
  • Windows 10 64x

@Mats

Have you tried setting the maintain offset for a safe measure?
tag[c4d.ID_CA_CONSTRAINT_TAG_PSR_MAINTAIN] = True

@bentraje Good tip, but doesnt work sadly, still offsetting 😞

Python_Parentconstraint_C.gif

@mats

Maybe you should call the Set Inital State button of the tag and set the local offset vector as the target's negative vector.
Here's the code:

import c4d

objList = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER | c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
tag = c4d.BaseTag(c4d.Tcaconstraint)
obj1, obj2 = objList[:2]
obj1.InsertTag(tag)
c4d.CallButton(tag, c4d.ID_CA_CONSTRAINT_TAG_SET_INITIAL_STATE) # SET INITIAL STATE
tag[c4d.ID_CA_CONSTRAINT_TAG_PARENT] = True
tag[30009, 1000] = obj2[c4d.ID_BASEOBJECT_REL_POSITION] * -1  # Local Offset P
tag[30001] = obj2  # Target
c4d.EventAdd()

@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 there C4DAtom.__call__, the line should be tag[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 and 2023.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,
Ferdinand

The result:
constrain_cubes.gif

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 using MakeTag, c4d will do some internal operations I guess:

tag: c4d.BaseTag = child.MakeTag(c4d.Tcaconstraint)

While using BaseTag constructor, then InsertTag, 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.
Screenshot 2022-11-11 at 09.58.10.png

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.

46036ad7-8f70-465f-b517-27b41b88a191-image.png
615a79ac-c08a-42e8-a1fa-b089e0bf141f-image.png

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,
Ferdinand

The 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 the set 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:

e729e764-f5a5-4479-a76b-84c21868ca51-image.png

I have updated my answer above.

Cheers,
Ferdinand

@ferdinand
Thanks, you are the best. This is exactly the internal operation I'm finding.