SOLVED CAD Normal Tag flipped after Polyon merge (join) and axis repositioning - how realign?

Hello @mogh,

just as a heads up, this request has not been forgotten, I have been working on it. There are just more hoops to jump through, than I originally did anticipate. You will get your answer here soon.

Cheers,
Ferdinand

Hi @mogh,

sorry for taking my sweet time here, but I had to untangle some NormalTag stuff first. Your normals are not flipped, but since you do transform your geometry, you also have to transform the normals by the same transform. Below you will find a script which does that and is based on your script. I have also posted a version which avoids this whole slightly overcomplicated setup here.

Cheers,
Ferdinand

"""Demonstrates how to manipulate NormalTags.

Your normals are not flipped, but actually have to be transformed by the same
transform we did apply to the vertices (i.e. geometry). Because normals are
related to the local point/vertex space of a PolygonObject and when we shuffle
there things around, the normals won't be *right* anymore.

My code is all in the function adjust_point_node() and will handle both the 
vertex as well as normal transforms. Which can be quite computationally heavy 
due to all the shoving around of data. The advantage here is that this code 
will have nicer axis.
"""

# Version 1.4 Striped version
# This Script needs Null-Objects Called "Axis" with polygon objects to run

import c4d
import array


def GetNextObject(op):
    if not op:
        return
    if op.GetDown():
        return op.GetDown()
    while op.GetUp() and not op.GetNext():
        op = op.GetUp()
    return op.GetNext()


def get_all_objects(op):
    allachsen_list = list()
    all_objects_list = list()
    while op:
        if op.GetName() == 'Achsen-Objekt' or op.GetName() == 'Axis':
            allachsen_list.append(op)
        all_objects_list.append(op)
        op = GetNextObject(op)
    return all_objects_list, allachsen_list


def JoinCommand(doc, op):
    res = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_JOIN,
                                        list=[op],
                                        mode=c4d.MODELINGCOMMANDMODE_ALL,
                                        doc=doc)

    # Checks if the command didn't failed
    if res is False:
        raise TypeError("return value of Join command is not valid")
    elif res is True:
        print("Command successful. But no object.")
    elif isinstance(res, list):
        if c4d.GetC4DVersion() < 21000:
            res[0].SetAbsPos(c4d.Vector())
        op.Remove()
        # Returns the first item containing the object of the list.
        return res[0]


def adjust_point_node(node, transform):
    """Corrects points and normals on a point object after running the join
    command in the scenario specific to PC13004.

    Args:
        node (c4d.PointObject): The PointObject to transform.
        transform (c4d.Matrix): The transform to apply.

    Raises:
        TypeError: When either `node` or `transform` are not of expected type.
    """
    def read_normal_tag(tag):
        """`Reads a `c4d.NormalTag` to a list of c4d.Vector.

        Args:
            tag (c4d.NormalTag): The tag to read the normals from.

        Returns:
            list[c4d.Vector]: The red in normals. There are polygon_count * 4 
             normals, i.e. each vertex has a normal for each polygon it is
             attached to. The normals are stored in polygon groups.

        Raises:
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressR()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        data = array.array('h')
        data.frombytes(buffer)
        # Convert the int16 representation of the normals to c4d.Vector.
        return [c4d.Vector(data[i-3] / 32000.0,
                           data[i-2] / 32000.0,
                           data[i-1] / 32000.0)
                for i in range(3, len(data) + 3, 3)]

    def write_normal_tag(tag, normals, do_normalize=True):
        """`Writes a list of c4d.Vector to a `c4d.NormalTag`.

        Args:
            tag (c4d.NormalTag): The tag to write the normals to.
            normals (list[c4d.Vector]): The normals to write.
            do_normalize (bool, optional): If to normalize the normals.
             Default to `True`.

        Note:
            Does not ensure that `normals` is only composed of c4d.Vector.

        Raises:
            IndexError: When `normals` does not match the size of `tag`.
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressW()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        if do_normalize:
            normals = [n.GetNormalized() for n in normals]

        # Convert c4d.Vector normals to integer representation.
        raw_normals = [int(component * 32000.0)
                       for n in normals for component in (n.x, n.y, n.z)]

        # Catch input data of invalid length.
        count = tag.GetDataCount()
        if count * 12 != len(raw_normals):
            msg = (f"Invalid data size. Expected length of {count}. "
                   f"Received: {len(raw_normals)}")
            raise IndexError(msg)

        # Write the data back.
        data = array.array('h')
        data.fromlist(raw_normals)
        data = data.tobytes()
        buffer[:len(data)] = data

    # --- Start of outer function --------------------------------------------
    if (not isinstance(node, c4d.PointObject) or
            not isinstance(transform, c4d.Matrix)):
        msg = f"Illegal argument types: {type(node)}{type(transform)}"
        raise TypeError(msg)

    # Transform the points of the node.
    points = [p * ~transform for p in node.GetAllPoints()]
    node.SetAllPoints(points)
    node.Message(c4d.MSG_UPDATE)
    node.SetMg(transform)
    # Transform its baked normals.
    tag = node.GetTag(c4d.Tnormal)
    if isinstance(tag, c4d.NormalTag):
        # We just have to apply the same transform here (without the 
        # translation).
        normals = [~transform.MulV(n) for n in read_normal_tag(tag)]
        write_normal_tag(tag, normals)
    else:
        msg = f"Could not find normal data to adjust for: {node}."
        print(f"Warning - {msg}")


def joinmanagment(n):
    # "Axis" null will be not alive in a few steps get everything we need
    # from it
    if n.GetUp():
        parent = n.GetUp()
    else:
        print("No Parent To Axis Null. Probably not save to run this script.")
        c4d.StatusClear()
        c4d.StatusSetText(
            'No Parent found! - Probably mo CAD import Doc. Script Stopped.')
        exit()
        return False

    parentmg = n.GetMg()
    newobject = JoinCommand(doc, n)  # combine the poly objects

    if not newobject.IsAlive():
        raise TypeError("Object is not alive.")
        return False

    newobject.SetName(str(parent.GetName()))
    newobject.InsertUnder(parent)
    newobject.SetMg(newobject.GetMl())
    adjust_point_node(newobject, parentmg)


def main():
    c4d.CallCommand(13957)  # Konsole löschen
    op = doc.GetFirstObject()

    c4d.StatusSetSpin()
    all_objects, allachsen = get_all_objects(op)  # get two lists
    null_counter = len(allachsen)

    if null_counter == 0:  # check if found something to do.
        c4d.StatusClear()
        c4d.StatusSetText('No Axis Objects found, nothing to do here.')
        print("No Axis Objects found, nothing to do here.")
        exit()

    counter = len(all_objects)
    secondcounter = 0
    c4d.StatusSetText('%s Objects are processed.' % (null_counter))

    for n in allachsen:
        secondcounter += 1
        c4d.StatusSetBar(100*secondcounter/counter)  # statusbar
        if joinmanagment(n) == False:
            break

    c4d.StatusClear()
    c4d.EventAdd()  # update cinema 4d
    print('END OF SCRIPT')
    return True


if __name__ == '__main__':
    main()

Hi @mogh,

sorry for taking my sweet time here, but I had to untangle some NormalTag stuff first. Your normals are not flipped, but since you do transform your geometry, you also have to transform the normals by the same transform. Below you will find a script which does that and is based on your script. I have also posted a version which avoids this whole slightly overcomplicated setup here.

Cheers,
Ferdinand

"""Demonstrates how to manipulate NormalTags.

Your normals are not flipped, but actually have to be transformed by the same
transform we did apply to the vertices (i.e. geometry). Because normals are
related to the local point/vertex space of a PolygonObject and when we shuffle
there things around, the normals won't be *right* anymore.

My code is all in the function adjust_point_node() and will handle both the 
vertex as well as normal transforms. Which can be quite computationally heavy 
due to all the shoving around of data. The advantage here is that this code 
will have nicer axis.
"""

# Version 1.4 Striped version
# This Script needs Null-Objects Called "Axis" with polygon objects to run

import c4d
import array


def GetNextObject(op):
    if not op:
        return
    if op.GetDown():
        return op.GetDown()
    while op.GetUp() and not op.GetNext():
        op = op.GetUp()
    return op.GetNext()


def get_all_objects(op):
    allachsen_list = list()
    all_objects_list = list()
    while op:
        if op.GetName() == 'Achsen-Objekt' or op.GetName() == 'Axis':
            allachsen_list.append(op)
        all_objects_list.append(op)
        op = GetNextObject(op)
    return all_objects_list, allachsen_list


def JoinCommand(doc, op):
    res = c4d.utils.SendModelingCommand(command=c4d.MCOMMAND_JOIN,
                                        list=[op],
                                        mode=c4d.MODELINGCOMMANDMODE_ALL,
                                        doc=doc)

    # Checks if the command didn't failed
    if res is False:
        raise TypeError("return value of Join command is not valid")
    elif res is True:
        print("Command successful. But no object.")
    elif isinstance(res, list):
        if c4d.GetC4DVersion() < 21000:
            res[0].SetAbsPos(c4d.Vector())
        op.Remove()
        # Returns the first item containing the object of the list.
        return res[0]


def adjust_point_node(node, transform):
    """Corrects points and normals on a point object after running the join
    command in the scenario specific to PC13004.

    Args:
        node (c4d.PointObject): The PointObject to transform.
        transform (c4d.Matrix): The transform to apply.

    Raises:
        TypeError: When either `node` or `transform` are not of expected type.
    """
    def read_normal_tag(tag):
        """`Reads a `c4d.NormalTag` to a list of c4d.Vector.

        Args:
            tag (c4d.NormalTag): The tag to read the normals from.

        Returns:
            list[c4d.Vector]: The red in normals. There are polygon_count * 4 
             normals, i.e. each vertex has a normal for each polygon it is
             attached to. The normals are stored in polygon groups.

        Raises:
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressR()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        data = array.array('h')
        data.frombytes(buffer)
        # Convert the int16 representation of the normals to c4d.Vector.
        return [c4d.Vector(data[i-3] / 32000.0,
                           data[i-2] / 32000.0,
                           data[i-1] / 32000.0)
                for i in range(3, len(data) + 3, 3)]

    def write_normal_tag(tag, normals, do_normalize=True):
        """`Writes a list of c4d.Vector to a `c4d.NormalTag`.

        Args:
            tag (c4d.NormalTag): The tag to write the normals to.
            normals (list[c4d.Vector]): The normals to write.
            do_normalize (bool, optional): If to normalize the normals.
             Default to `True`.

        Note:
            Does not ensure that `normals` is only composed of c4d.Vector.

        Raises:
            IndexError: When `normals` does not match the size of `tag`.
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressW()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        if do_normalize:
            normals = [n.GetNormalized() for n in normals]

        # Convert c4d.Vector normals to integer representation.
        raw_normals = [int(component * 32000.0)
                       for n in normals for component in (n.x, n.y, n.z)]

        # Catch input data of invalid length.
        count = tag.GetDataCount()
        if count * 12 != len(raw_normals):
            msg = (f"Invalid data size. Expected length of {count}. "
                   f"Received: {len(raw_normals)}")
            raise IndexError(msg)

        # Write the data back.
        data = array.array('h')
        data.fromlist(raw_normals)
        data = data.tobytes()
        buffer[:len(data)] = data

    # --- Start of outer function --------------------------------------------
    if (not isinstance(node, c4d.PointObject) or
            not isinstance(transform, c4d.Matrix)):
        msg = f"Illegal argument types: {type(node)}{type(transform)}"
        raise TypeError(msg)

    # Transform the points of the node.
    points = [p * ~transform for p in node.GetAllPoints()]
    node.SetAllPoints(points)
    node.Message(c4d.MSG_UPDATE)
    node.SetMg(transform)
    # Transform its baked normals.
    tag = node.GetTag(c4d.Tnormal)
    if isinstance(tag, c4d.NormalTag):
        # We just have to apply the same transform here (without the 
        # translation).
        normals = [~transform.MulV(n) for n in read_normal_tag(tag)]
        write_normal_tag(tag, normals)
    else:
        msg = f"Could not find normal data to adjust for: {node}."
        print(f"Warning - {msg}")


def joinmanagment(n):
    # "Axis" null will be not alive in a few steps get everything we need
    # from it
    if n.GetUp():
        parent = n.GetUp()
    else:
        print("No Parent To Axis Null. Probably not save to run this script.")
        c4d.StatusClear()
        c4d.StatusSetText(
            'No Parent found! - Probably mo CAD import Doc. Script Stopped.')
        exit()
        return False

    parentmg = n.GetMg()
    newobject = JoinCommand(doc, n)  # combine the poly objects

    if not newobject.IsAlive():
        raise TypeError("Object is not alive.")
        return False

    newobject.SetName(str(parent.GetName()))
    newobject.InsertUnder(parent)
    newobject.SetMg(newobject.GetMl())
    adjust_point_node(newobject, parentmg)


def main():
    c4d.CallCommand(13957)  # Konsole löschen
    op = doc.GetFirstObject()

    c4d.StatusSetSpin()
    all_objects, allachsen = get_all_objects(op)  # get two lists
    null_counter = len(allachsen)

    if null_counter == 0:  # check if found something to do.
        c4d.StatusClear()
        c4d.StatusSetText('No Axis Objects found, nothing to do here.')
        print("No Axis Objects found, nothing to do here.")
        exit()

    counter = len(all_objects)
    secondcounter = 0
    c4d.StatusSetText('%s Objects are processed.' % (null_counter))

    for n in allachsen:
        secondcounter += 1
        c4d.StatusSetBar(100*secondcounter/counter)  # statusbar
        if joinmanagment(n) == False:
            break

    c4d.StatusClear()
    c4d.EventAdd()  # update cinema 4d
    print('END OF SCRIPT')
    return True


if __name__ == '__main__':
    main()

thank you zipit,

as I am totally fine with a less heavy version (from the other thread) I would mark this as solved.

I still want to Thank you for all the other people which might need the actuall NormalTag manipulation in the future.

kind regards
mogh

Hi Ferdinand,

sadly i have to resurect this topic hence I still have problem with the normals when the AXIS changes.

I boiled down a script which moves the axis of selected polygon object to the parent (which is a common task in deply nested cad data )

anyway the axis is not a problem but I still have wrong shading because the normals are not transformed corectly ...

I attached a simple exaple file where a nut is orientaded off axis and results in wrong normals when executed the script.

thanks in advance

normal_example.c4d

import c4d, array
#from c4d import Vector as V
#https://plugincafe.maxon.net/topic/13004/cad-normal-tag-flipped-after-polyon-merge-join-and-axis-repositioning-how-realign/6

#new Task:
#Corrects points and normals on a point object after placing the polygon axis to the same as the parent 

def adjust_point_node(node, transform):
    """Corrects points and normals on a point object after running the join
    command in the scenario specific to PC13004.

    Args:
        node (c4d.PointObject): The PointObject to transform.
        transform (c4d.Matrix): The transform to apply.

    Raises:
        TypeError: When either `node` or `transform` are not of expected type.
    """
    def read_normal_tag(tag):
        """`Reads a `c4d.NormalTag` to a list of c4d.Vector.

        Args:
            tag (c4d.NormalTag): The tag to read the normals from.

        Returns:
            list[c4d.Vector]: The red in normals. There are polygon_count * 4 
             normals, i.e. each vertex has a normal for each polygon it is
             attached to. The normals are stored in polygon groups.

        Raises:
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressR()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        data = array.array('h')
        data.frombytes(buffer)
        # Convert the int16 representation of the normals to c4d.Vector.
        return [c4d.Vector(data[i-3] / 32000.0,
                           data[i-2] / 32000.0,
                           data[i-1] / 32000.0)
                for i in range(3, len(data) + 3, 3)]

    def write_normal_tag(tag, normals, do_normalize=True):
        """`Writes a list of c4d.Vector to a `c4d.NormalTag`.

        Args:
            tag (c4d.NormalTag): The tag to write the normals to.
            normals (list[c4d.Vector]): The normals to write.
            do_normalize (bool, optional): If to normalize the normals.
             Default to `True`.

        Note:
            Does not ensure that `normals` is only composed of c4d.Vector.

        Raises:
            IndexError: When `normals` does not match the size of `tag`.
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressW()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        if do_normalize:
            normals = [n.GetNormalized() for n in normals]

        # Convert c4d.Vector normals to integer representation.
        raw_normals = [int(component * 32000.0)
                       for n in normals for component in (n.x, n.y, n.z)]

        # Catch input data of invalid length.
        count = tag.GetDataCount()
        if count * 12 != len(raw_normals):
            msg = (f"Invalid data size. Expected length of {count}. "
                   f"Received: {len(raw_normals)}")
            raise IndexError(msg)

        # Write the data back.
        data = array.array('h')
        data.fromlist(raw_normals)
        data = data.tobytes()
        buffer[:len(data)] = data

    # --- Start of outer function --------------------------------------------
    if (not isinstance(node, c4d.PointObject) or
            not isinstance(transform, c4d.Matrix)):
        msg = f"Illegal argument types: {type(node)}{type(transform)}"
        raise TypeError(msg)

    # Transform the points of the node.
    #points = [p * ~transform for p in node.GetAllPoints()]
    
    # This works the polygon object stays in place now
    
    mat = op.GetMg()
    points = op.GetAllPoints()
    for i,p in enumerate(points):
        points[i] = ~transform * mat * p #Note that the multiplication sequence needs to be read from the right to the left.
        
    node.SetAllPoints(points)
    node.Message(c4d.MSG_UPDATE)
    
    # these where my desperate efforts to fix it 
    #------------------------------------------------------------------------    
    node.SetMg(transform)
    nodelocalmatrix = node.GetMl()    
    nodematrix = node.GetMg()
    
    # Transform its baked normals.
    tag = node.GetTag(c4d.Tnormal)
    if isinstance(tag, c4d.NormalTag):
        # We just have to apply the same transform here (without the 
        # translation).
        
        # original Code
        #normals = [~transform.MulV(n) for n in read_normal_tag(tag)] 
        
        # my tests
        normals = [~transform.MulV(n) for n in read_normal_tag(tag)]
        
        write_normal_tag(tag, normals)
    else:
        msg = f"Could not find normal data to adjust for: {node}."
        print(f"Warning - {msg}")

def main():
    c4d.CallCommand(13957)  # Konsole löschen
    if op is None or not op.CheckType(c4d.Opoint):
        return False

    doc.StartUndo()
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, op)

    targetMat = op.GetUp().GetMg()
    
    adjust_point_node(op, targetMat)
    
    op.Message(c4d.MSG_UPDATE)
    c4d.EventAdd()
    doc.EndUndo()
    
if __name__=='__main__':
    main()

Hello @mogh,

thank you for reaching out to us. I think you have your axis inverted. At least that will fix your nut example for me. I frankly would rewrite this from scratch (you can reuse the read and write normal tags functions) when you want to do something differently. I remember I had to jump through some hoops hold up by your outer code, when I wrote adjust_point_node. But I think it did work then, didn't it?

Long story short: removing the inversion works for me, but that whole script is very duct tapey, since it assumes that there will always be a parent and that you only want to tranfer its axis. If you are fine with these restrictions, the script is probably fine.

Cheers,
Ferdinand

The result:
11324324.png
The code:

import c4d, array
#from c4d import Vector as V
#https://plugincafe.maxon.net/topic/13004/cad-normal-tag-flipped-after-polyon-merge-join-and-axis-repositioning-how-realign/6

#new Task:
#Corrects points and normals on a point object after placing the polygon axis to the same as the parent 

def adjust_point_node(node, transform):
    """Corrects points and normals on a point object after running the join
    command in the scenario specific to PC13004.

    Args:
        node (c4d.PointObject): The PointObject to transform.
        transform (c4d.Matrix): The transform to apply.

    Raises:
        TypeError: When either `node` or `transform` are not of expected type.
    """
    def read_normal_tag(tag):
        """`Reads a `c4d.NormalTag` to a list of c4d.Vector.

        Args:
            tag (c4d.NormalTag): The tag to read the normals from.

        Returns:
            list[c4d.Vector]: The red in normals. There are polygon_count * 4 
             normals, i.e. each vertex has a normal for each polygon it is
             attached to. The normals are stored in polygon groups.

        Raises:
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressR()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        data = array.array('h')
        data.frombytes(buffer)
        # Convert the int16 representation of the normals to c4d.Vector.
        return [c4d.Vector(data[i-3] / 32000.0,
                           data[i-2] / 32000.0,
                           data[i-1] / 32000.0)
                for i in range(3, len(data) + 3, 3)]

    def write_normal_tag(tag, normals, do_normalize=True):
        """`Writes a list of c4d.Vector to a `c4d.NormalTag`.

        Args:
            tag (c4d.NormalTag): The tag to write the normals to.
            normals (list[c4d.Vector]): The normals to write.
            do_normalize (bool, optional): If to normalize the normals.
             Default to `True`.

        Note:
            Does not ensure that `normals` is only composed of c4d.Vector.

        Raises:
            IndexError: When `normals` does not match the size of `tag`.
            RuntimeError: When the memory of the tag cannot be red.
            TypeError: When `tag` is not a `c4d.NormalTag`.
        """
        if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
            msg = f"Expected normal tag, received: {tag}."
            raise TypeError(tag)

        buffer = tag.GetLowlevelDataAddressW()
        if buffer is None:
            msg = "Failed to retrieve memory buffer for VariableTag."
            raise RuntimeError(msg)

        if do_normalize:
            normals = [n.GetNormalized() for n in normals]

        # Convert c4d.Vector normals to integer representation.
        raw_normals = [int(component * 32000.0)
                       for n in normals for component in (n.x, n.y, n.z)]

        # Catch input data of invalid length.
        count = tag.GetDataCount()
        if count * 12 != len(raw_normals):
            msg = (f"Invalid data size. Expected length of {count}. "
                   f"Received: {len(raw_normals)}")
            raise IndexError(msg)

        # Write the data back.
        data = array.array('h')
        data.fromlist(raw_normals)
        data = data.tobytes()
        buffer[:len(data)] = data

    # --- Start of outer function --------------------------------------------
    if (not isinstance(node, c4d.PointObject) or
            not isinstance(transform, c4d.Matrix)):
        msg = f"Illegal argument types: {type(node)}{type(transform)}"
        raise TypeError(msg)

    # I started cleaning up here a bit by running autopep on the script and
    # removing some clutter.

    mat = op.GetMg()
    points = op.GetAllPoints()
    for i,p in enumerate(points):
        points[i] = ~transform * mat * p
        
    node.SetAllPoints(points)
    node.Message(c4d.MSG_UPDATE) 
    node.SetMg(transform)
    
    nodelocalmatrix = node.GetMl()    
    nodematrix = node.GetMg()
    
    # Transform its baked normals.
    tag = node.GetTag(c4d.Tnormal)
    if isinstance(tag, c4d.NormalTag):
        # The only substantial change I made, I removed the inversion operator.
        # normals = [~transform.MulV(n) for n in read_normal_tag(tag)]
        normals = [transform.MulV(n) for n in read_normal_tag(tag)]
        write_normal_tag(tag, normals)
    else:
        msg = f"Could not find normal data to adjust for: {node}."
        print(f"Warning - {msg}")

def main():
    c4d.CallCommand(13957)  # Konsole löschen
    if op is None or not op.CheckType(c4d.Opoint):
        return False

    doc.StartUndo()
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, op)

    targetMat = op.GetUp().GetMg()
    
    adjust_point_node(op, targetMat)
    
    op.Message(c4d.MSG_UPDATE)
    c4d.EventAdd()
    doc.EndUndo()
    
if __name__=='__main__':
    main()

Thanks for the quick answer,

@ferdinand said in CAD Normal Tag flipped after Polyon merge (join) and axis repositioning - how realign?:

I think you have your axis inverted.

This was the first fix I tried, it seems to fix the nut, but not all parts, especially when they are tilted on all axis.
I guess I have to find better examples to get to the bottom of this.

kind regards
mogh

That's strange for me inverting the Matrix (using your posted script) does not fix the nut it flips still !
Why do we have different outcomes ? (R23 atm, R25 in the office not tested yet)

normal_examplev2.c4d

Hi again,

@ferdinand sorry to pester you ....

I recorded my outcome and it seems it does not make a difference if the normals are multiplied by the invers object matrix or the object matrix.

c4d_normal_tag_centeraxis.mp4
The lighting in the video should not change if I center the axis to the parent.

...
btw if anybody is curious R20 Version needs besides some .stringformat()

        #data.frombytes(buffer)
        data.fromstring(buffer) # python 2.7

        #data = data.tobytes()
        data = data.tostring() # python 2.7

kind regards

Hello @mogh,
precensende
I currently do not have the time to look into this, since you are asking us here effectively to debug your code (which is out of scope of support). The only thing that pops into my head is operator precedence when I look at your video. So, try (~foo).bar() instead of ~foo.bar(). I will see if I can take another look at your problem at the end of the week.

Cheers,
Ferdinand

Its getting stranger and I am still unclear whats happening ...
I changed the precedence of the

normals = [ (~transform).MulV(n) for n in read_normal_tag(tag) ]

Tested in
R20 - fail -> Normals flip ...
R23 - first test good moving to another parent Null second execution -> Normals flip .. iratic behavior depending on null location ?
R24 - first test good moving to another parent Null second execution ->Normals flip .. iratic behavior depending on null location ?
R25 - first test good moving to another parent Null second execution -> Normals flip .. iratic behavior depending on null location ?

kind regards

Hello @mogh,

so, I had a look at your problem and this all works very much as expected for me. As stated before, we cannot debug your code for you. You will have to do that yourself. When you can show in a reasonably simple and repeatable example that Cinema 4D does handle vector/linear algebra differently between versions, we will investigate that. Not for R20 though, as this is out of the SDK support cycle. I also seriously doubt that there is a bug in Vector or Matrix of this magnitude in the first place (but I could be wrong).

Below you can find my solution for the problem. We cannot debug your code for such math problems for you in the future.

Cheers,
Ferdinand

The test file: transferaxis.c4d
The result:
transferaxis.gif
The code:

"""Transfers the axis from one object to another.

Also handles vertices and normal tags, and provides a minimal GUI.
"""

import c4d
import array


def ReadNormalTag(tag: c4d.NormalTag) -> list[c4d.Vector]:
    """`Reads a `c4d.NormalTag` to a list of c4d.Vector.

    Args:
        tag: The tag to read the normals from.

    Returns:
        The red in normals. There are polygon_count * 4 normals, i.e. each 
        vertex has a normal for each polygon it is attached to. The normals 
        are stored in polygon groups.

    Raises:
        RuntimeError: When the memory of the tag cannot be red.
        TypeError: When `tag` is not a `c4d.NormalTag`.
    """
    if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
        msg = f"Expected normal tag, received: {tag}."
        raise TypeError(tag)

    buffer = tag.GetLowlevelDataAddressR()
    if buffer is None:
        msg = "Failed to retrieve memory buffer for VariableTag."
        raise RuntimeError(msg)

    data = array.array('h')
    data.frombytes(buffer)
    # Convert the int16 representation of the normals to c4d.Vector.
    return [c4d.Vector(data[i-3] / 32000.0,
                       data[i-2] / 32000.0,
                       data[i-1] / 32000.0)
            for i in range(3, len(data) + 3, 3)]


def WriteNormalTag(tag: c4d.NormalTag, normals: list[c4d.Vector],
                   normalize: bool = True) -> None:
    """`Writes a list of c4d.Vector to a `c4d.NormalTag`.

    Args:
        tag: The tag to write the normals to.
        normals: The normals to write.
        normalize: If to normalize the normals. Defaults to `True`.

    Note:
        Does not ensure that `normals` is only composed of c4d.Vector.

    Raises:
        IndexError: When `normals` does not match the size of `tag`.
        RuntimeError: When the memory of the tag cannot be red.
        TypeError: When `tag` is not a `c4d.NormalTag`.
    """
    if not (isinstance(tag, c4d.BaseTag) and tag.CheckType(c4d.Tnormal)):
        msg = f"Expected normal tag, received: {tag}."
        raise TypeError(tag)

    buffer = tag.GetLowlevelDataAddressW()
    if buffer is None:
        msg = "Failed to retrieve memory buffer for VariableTag."
        raise RuntimeError(msg)

    if normalize:
        normals = [n.GetNormalized() for n in normals]

    # Convert c4d.Vector normals to integer representation.
    raw_normals = [int(component * 32000.0)
                   for n in normals for component in (n.x, n.y, n.z)]

    # Catch input data of invalid length.
    count = tag.GetDataCount()
    if count * 12 != len(raw_normals):
        msg = (f"Invalid data size. Expected length of {count}. "
               f"Received: {len(raw_normals)}")
        raise IndexError(msg)

    # Write the data back.
    data = array.array('h')
    data.fromlist(raw_normals)
    data = data.tobytes()
    buffer[:len(data)] = data


def TransferAxisTo(node: c4d.BaseObject, target: c4d.BaseObject) -> None:
    """Moves the axis of node to target.
    """
    mgNode = node.GetMg()
    mgTarget = target.GetMg()

    # Set the transform of the node to the target transform.
    doc.StartUndo()
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, node)
    node.SetMg(mgTarget)

    # Exit when node is not a point object.
    if not isinstance(node, c4d.PointObject):
        doc.EndUndo()
        return None

    # Compute the transform delta between both objects with and without
    # translations and transform all vertices.
    mgDelta = ~mgTarget * mgNode
    mgOrientationDelta = c4d.Matrix(
        v1=mgDelta.v1, v2=mgDelta.v2, v3=mgDelta.v3)

    node.SetAllPoints([p * mgDelta for p in op.GetAllPoints()])
    node.Message(c4d.MSG_UPDATE)

    # Exit when node has no normal tag.
    normalTag = node.GetTag(c4d.Tnormal)
    if not isinstance(normalTag, c4d.NormalTag):
        doc.EndUndo()
        return None

    # Transform the normals in the normal tag.
    newNormals = [n * mgOrientationDelta for n in ReadNormalTag(normalTag)]
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, normalTag)
    WriteNormalTag(normalTag, newNormals, normalize=True)

    doc.EndUndo()


def main() -> None:
    """Entry point that runs the code when there is an active object.
    """
    if not isinstance(op, c4d.BaseObject):
        c4d.gui.MessageDialog("Please select an object.")
        return None

    # Get all the ancestor nodes of the selected node.
    ancestors, parent = [], op.GetUp()
    while parent:
        ancestors.insert(0, parent)
        parent = parent.GetUp()

    # Build and show a popup menu for all ancestors with their name and icon.
    bc, idBase = c4d.BaseContainer(), 1000
    for i, node in enumerate(ancestors):
        bc.InsData(idBase + i, f"{node.GetName()}&i{node.GetType()}&")
    res = c4d.gui.ShowPopupDialog(None, bc, c4d.MOUSEPOS, c4d.MOUSEPOS)

    # The user did abort the popup.
    if res == 0:
        return

    # Carry out the modification of the axis of op to target.
    target = ancestors[res - idBase]
    TransferAxisTo(op, target)
    c4d.EventAdd()


if __name__ == '__main__':
    main()

Thank you ferdinand for your personal time ...
will check the Code in the office tomorrow ...

kind regards
mogh

Seems to work ... fingers crossed...

you managed to implement the normal tag handling faster ! my script that cleans up CAD data went down from 60 seconds to 25 seconds !!!!

        11153152 function calls in 25.692 seconds

   Ordered by: cumulative time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.547    0.547   25.692   25.692 03 optimize Objects.py:354(main)
      889    1.696    0.002   21.027    0.024 03 optimize Objects.py:205(TransferAxisTo)
      889    0.016    0.000   10.535    0.012 03 optimize Objects.py:159(WriteNormalTag)
      889    0.005    0.000    6.767    0.008 03 optimize Objects.py:127(ReadNormalTag)
      889    6.750    0.008    6.750    0.008 03 optimize Objects.py:154(<listcomp>)
      889    6.137    0.007    6.137    0.007 03 optimize Objects.py:189(<listcomp>)
        1    0.000    0.000    3.051    3.051 03 optimize Objects.py:290(optimizepolyobject)
        1    3.051    3.051    3.051    3.051 {built-in method c4d.utils.SendModelingCommand}
      889    1.763    0.002    2.956    0.003 03 optimize Objects.py:186(<listcomp>)
      889    1.679    0.002    1.679    0.002 03 optimize Objects.py:237(<listcomp>)
      889    1.413    0.002    1.413    0.002 {method 'fromlist' of 'array.array' objects}
 11038804    1.193    0.000    1.193    0.000 {method 'GetNormalized' of 'c4d.Vector' objects}
      889    0.004    0.000    0.813    0.001 03 optimize Objects.py:245(removesinglenull)
      890    0.808    0.001    0.808    0.001 {built-in method c4d.CallCommand}
     1876    0.237    0.000    0.237    0.000 {method 'GetAllPoints' of 'c4d.PointObject' objects}
      889    0.157    0.000    0.157    0.000 03 optimize Objects.py:227(<listcomp>)
     1778    0.082    0.000    0.082    0.000 {method 'AddUndo' of 'c4d.documents.BaseDocument' objects}
     3763    0.006    0.000    0.047    0.000 03 optimize Objects.py:81(statusbar)
     3763    0.028    0.000    0.028    0.000 {built-in method c4d.StatusSetBar}
     8045    0.007    0.000    0.026    0.000 03 optimize Objects.py:38(GetNextObject)
      889    0.017    0.000    0.017    0.000 {method 'SetAllPoints' of 'c4d.PointObject' objects}
       21    0.017    0.001    0.017    0.001 {built-in method c4d.StatusSetText}
        2    0.002    0.001    0.016    0.008 03 optimize Objects.py:61(get_all_poly_objects)
        2    0.001    0.000    0.011    0.005 03 optimize Objects.py:52(get_all_objects)
    13109    0.011    0.000    0.011    0.000 {method 'GetUp' of 'c4d.GeListNode' objects}
      889    0.011    0.000    0.011    0.000 {method 'tobytes' of 'array.array' objects}
      889    0.010    0.000    0.010    0.000 {method 'frombytes' of 'array.array' objects}
    13696    0.008    0.000    0.008    0.000 {method 'GetNext' of 'c4d.GeListNode' objects}
    11327    0.005    0.000    0.005    0.000 {method 'GetDown' of 'c4d.GeListNode' objects}
      994    0.002    0.000    0.004    0.000 03 optimize Objects.py:90(rename)
     1938    0.004    0.000    0.004    0.000 {method 'GetChildren' of 'c4d.GeListNode' objects}
      889    0.004    0.000    0.004    0.000 {method 'Message' of 'c4d.C4DAtom' objects}
        1    0.000    0.000    0.004    0.004 03 optimize Objects.py:71(get_all_null_objects)
        2    0.003    0.002    0.003    0.002 {built-in method c4d.StatusSetSpin}
      889    0.002    0.000    0.002    0.000 {method 'SetMg' of 'c4d.BaseObject' objects}
     8575    0.002    0.000    0.002    0.000 {method 'CheckType' of 'c4d.C4DAtom' objects}
     1778    0.002    0.000    0.002    0.000 {method 'GetMg' of 'c4d.BaseObject' objects}
      889    0.001    0.000    0.001    0.000 {method 'GetTag' of 'c4d.BaseObject' objects}
      889    0.001    0.000    0.001    0.000 {method 'DelBit' of 'c4d.BaseList2D' objects}
     1038    0.001    0.000    0.001    0.000 {method 'Remove' of 'c4d.GeListNode' objects}
     4611    0.001    0.000    0.001    0.000 {built-in method builtins.len}
     3976    0.001    0.000    0.001    0.000 {method 'split' of 'str' objects}
      994    0.001    0.000    0.001    0.000 {method 'SetName' of 'c4d.BaseList2D' objects}
     4550    0.001    0.000    0.001    0.000 {built-in method builtins.isinstance}
      889    0.001    0.000    0.001    0.000 {method 'GetLowlevelDataAddressR' of 'c4d.VariableTag' objects}
      994    0.001    0.000    0.001    0.000 {method 'GetName' of 'c4d.BaseList2D' objects}
      889    0.001    0.000    0.001    0.000 {method 'GetLowlevelDataAddressW' of 'c4d.VariableTag' objects}
      889    0.001    0.000    0.001    0.000 {method 'GetDataCount' of 'c4d.VariableTag' objects}
     6844    0.001    0.000    0.001    0.000 {method 'append' of 'list' objects}
      889    0.000    0.000    0.000    0.000 {method 'SetBit' of 'c4d.BaseList2D' objects}
        8    0.000    0.000    0.000    0.000 {built-in method builtins.print}
       24    0.000    0.000    0.000    0.000 __init__.py:65(write)
        1    0.000    0.000    0.000    0.000 {built-in method c4d.StatusClear}
       24    0.000    0.000    0.000    0.000 __init__.py:46(MAXON_SOURCE_LOCATION)
       24    0.000    0.000    0.000    0.000 {built-in method _maxon_core.GetCurrentTraceback}
      994    0.000    0.000    0.000    0.000 {method 'replace' of 'str' objects}
       24    0.000    0.000    0.000    0.000 {built-in method _maxon_output.stdout_write}
        1    0.000    0.000    0.000    0.000 03 optimize Objects.py:261(savetorun)
        1    0.000    0.000    0.000    0.000 03 optimize Objects.py:31(gime_time)
        1    0.000    0.000    0.000    0.000 {method 'GetFirstObject' of 'c4d.documents.BaseDocument' objects}
        1    0.000    0.000    0.000    0.000 {built-in method c4d.EventAdd}
        1    0.000    0.000    0.000    0.000 {built-in method c4d.documents.GetActiveDocument}
        2    0.000    0.000    0.000    0.000 {built-in method c4d.GeGetTimer}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.divmod}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

as I understand my problem in my thinking was not rotating the orentation seperatly ....

mgOrientationDelta = c4d.Matrix(v1=mgDelta.v1, v2=mgDelta.v2, v3=mgDelta.v3)

Should I / we mark your last code as solution as its working better and is easier to understand ?!?

anyway thank you again and I am sorry to have caused any trouble ...
kind regards ....

Hey,

good to hear that it does work for you (for now) 😉 I do not think that the definition of mgOrientationDelta is the culprit. It is just another way of doing what was done before with .MulV, i.e., it is just a transform without the offset component. As I indicated in my last posting, I did not spend much time with debugging your code, I just copied over the tag stuff and wrote the rest from scratch.

I do not think that we do have to change anything here in the topic structure. But if you personally feel it would be better so, you can of course do it. At least I think you can, you have to open the topic tools button:

18e1f9a6-d363-44c1-87a5-14e4a50e058e-image.png

and there you should then have this button:

0faeb0a9-2343-4c7d-b971-4a059576700b-image.png

If you do not, well then only mods can do that 😄

Cheers,
Ferdinand