SOLVED How to fix the bug of objects position in script

Guys! Please, help. I cannot fix this problem.

The main idea of ​​this script is:

  1. add selected objects and materials of these objects to the layer
  2. insert all objects under null.
    But position of objects changes. How to fix it?

: video

import c4d

def main():

    doc.StartUndo()


    layer = c4d.documents.LayerObject()
    # Set layer name (another option tp set the name)
    # Get the invisible root layer
    layerRoot = doc.GetLayerObjectRoot()
    # Insert the layer under the parent
    layer.InsertUnder(layerRoot)

    name = layer.GetName()
    rvalue = c4d.gui.RenameDialog(name) # rename
    print(rvalue)
    layer [c4d.ID_BASELIST_NAME] = rvalue
    #layer.DelBit(c4d.BIT_ACTIVE)

    null_obj = c4d.BaseObject(c4d.Onull)
    null_obj [c4d.ID_LAYER_LINK] = layer
    null_obj [c4d.ID_BASELIST_NAME] = rvalue
    doc.InsertObject(null_obj)


    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, layer)
    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, null_obj)

    s = doc.GetSelection()
    vp = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE)


    for obj in vp:

        print obj

        obj [c4d.ID_LAYER_LINK] = layer
        obj.InsertUnder(null_obj)

        obj_mat_tags = [t for t in obj.GetTags() if t.IsInstanceOf(c4d.Ttexture)]

        for tag in obj_mat_tags:

            mat = tag.GetMaterial()
            print mat
            mat [c4d.ID_LAYER_LINK] = layer


    doc.EndUndo()
    c4d.EventAdd()

if __name__ == '__main__':
    main()

I use r21 version of c4d.

Hello @roman,

Thank you for reaching out to us. R21 has left the SDK support cycle, which primarily means we will not debug again such versions anymore and also do not fix bugs for it anymore. I have provided below a solution for your problem which has been tested with R25 because of that, but it should run fine in R21.

Your problem primarily is rooted in you simply reparenting the objects. Objects store their transformation - their position, scale and orientation - as a matrix relative to their parent. So, when you have the objects a, b, c with the local positions (0, 100, 0), (0, 50, 0), (0, 0, 0) and b being parented to a, then the effective global position of b is (0, 100, 0) + (0, 50, 0) = (0, 150, 0). When you then parent b to c, its position will change from (0, 150, 0) to (0, 50, 0) since c only contributes the null vector to the position of its children. The same principle applies to the scale and orientation stored in the local transform matrix of an object. You were also missing some undo-steps, at least I assumed you did not skip them intentionally.

The topic is also covered in the Python API Matrix Manual.

Cheers,
Ferdinand

The result:
reparent.gif

The script:

"""Moves the selected objects to a new layer and parent object.

Your problem primarily is rooted in you simply reparenting the objects. 
Objects store their transformation - their position, scale and orientation -  
as a matrix relative to their parent. So, when you have the objects `a, b, c` 
with the local positions `(0, 100, 0), (0, 50, 0), (0, 0, 0)` and `b` being 
parented to `a`, then the effective global position of `b` is `(0, 100, 0) + 
(0, 50, 0) = (0, 150, 0)`. When you then parent `b` to `c`, its position will 
change from `(0, 150, 0)` to `(0, 50, 0)` since `c` only contributes the null 
vector to the position of its children. The same principle applies to the 
scale and orientation stored in the local transform matrix of an object. You 
were also missing some undo-steps, at least I assumed you did not skip them 
intentionally.
"""

import c4d

def main():
    """Entry point.
    """
    # Start an undo stack item.
    doc.StartUndo()

    # Add a new top-level layer
    newLayer = c4d.documents.LayerObject()
    layerRoot = doc.GetLayerObjectRoot()
    newLayer.InsertUnder(layerRoot)
    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, newLayer)

    # Let the user name the new layer
    newName = c4d.gui.RenameDialog(newLayer.GetName())
    newLayer [c4d.ID_BASELIST_NAME] = newName

    # Create a null object to parent the objects moved to the new layer to.
    newLayerNull = c4d.BaseObject(c4d.Onull)
    newLayerNull [c4d.ID_LAYER_LINK] = newLayer
    newLayerNull [c4d.ID_BASELIST_NAME] = newName
    doc.InsertObject(newLayerNull)
    doc.AddUndo(c4d.UNDOTYPE_NEWOBJ, newLayerNull)

    # Iterate over the selected objects and attach them both to the new layer
    # and layer null-object.
    for item in doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_NONE):

        # Set the layer of the current item, detach it from its previous 
        # parent and store its global matrix.
        doc.AddUndo(c4d.UNDOTYPE_CHANGE, item)
        item [c4d.ID_LAYER_LINK] = newLayer
        itemMg = item.GetMg()
        item.Remove()

        # Attach the object to the null and set its global matrix to the old
        # value.
        item.SetMg(itemMg)
        item.InsertUnder(newLayerNull)

        # Get all materials attached with material tags to the item.
        isTex = lambda item: item.IsInstanceOf(c4d.Ttexture)
        for material in [t.GetMaterial() for t in item.GetTags() if isTex(t)]:
            doc.AddUndo(c4d.UNDOTYPE_CHANGE, material)
            material[c4d.ID_LAYER_LINK] = newLayer

    # Close the undo item and push an update event to Cinema 4D.
    doc.EndUndo()
    c4d.EventAdd()

if __name__ == '__main__':
    main()