UNSOLVED Set Ngons with Python

I got Ngons translation map and I'd like to change other Polygon object made of Cpolygons according to that map.

Is there a way to do it without replacing polygon object with new one?

Nope.

The Python API does not properly support Ngons. You can read a good deal of information by using

PolygonObject.GetNGonTranslationMap(self, ngoncnt, polymap)
PolygonObject.GetNgonCount(self)
PolygonObject.GetSelectedNgons(self, sel)
PolygonObject.GetHiddenNgons(self, sel)

and some Ngon edge functions, but you don't get to set the Ngon.
If you look into the C++ API, you will find

Pgon* GetAndBuildNgon(void)
NgonBase* GetNgonBase()

but these functions, as well as the classes Pgon and NgonBase (and indirectly referenced PgonEdge), do not exist in the Python API.

(I'm not totally sure what exactly you want to achieve here but there are some other threads about Ngons on this forum that you may like to consult.)

Hello @karpique,

thank you for reaching out to us. While @Cairyn's answer is technically true (thanks for the answer), it practically is not.

As mentioned in the original thread, this is possible via MCOMMAND_MELT. As also already mentioned there, the SMC based approach is rather cumbersome to carry out, but can be taken if absolutely necessary. I still would recommend using C4DAtom.CopyTo() and removing stuff that is not needed. Below you will find an (partial) example for using MCOMMAND_MELT in this context.

Cheers,
Ferdinand

PS: I am still not at work the following week, so this thread will have to wait a bit with follow-ups, as this is the same question as the original thread.

"""Example for copying polygons and ngons from one polygon object to another.

Run this with a polygon object selected in the script manger. The approach 
shown here is MCOMMAND_MELT based and slightly flawed, for details see the
note in CopyNgonGroupings(). But it can be fixed with a little bit of work.
 As I said in the original thread, it is possible to do this, but rather 
 cumbersome to do so.

As discussed in:
    https://plugincafe.maxon.net/topic/13458/
"""

import c4d


def CopyNgonGroupings(source, target):
    """Copies over ngon information from source to target.

    Copies over ngon information via ngon maps and MCOMMAND_MELT. This is
    done in one go, i.e., all polygons are melted in one operation.

    Note:
        This approach of doing it in one go has the disadvantage that it will
        also melt two neighboring, i.e., touching, ngons in the source object
        together. Since there is per group ngon information in the translation
        maps, we could also carry this out iteratively to avoid that problem.
        However, then one has to deal with the fact that the polygon indices
        will shift with each operation. Which is not impossible to solve, but
        cumbersome.

    Args:
        source (c4d.PolygonObject): The object to copy ngon groupings from.
        target (c4d.PolygonObject): The object to copy ngon groupings to.
    
    Returns:
        c4d.PolygonObject: The reference to target which has been modified.
    """
    # The polygon to ngon information, this returns a tuple(int, List[int]),
    # where int is the number of ngons and List[int] contains the mappings.
    # I.e., (2, [0, 0, 0, 1, 1, 1]) means that there are two ngon in the
    # mesh an that the polygon with indices 0 to 2 belong to the ngon 0 and
    # the polygon with indices 3 to 5 belong to the ngon 1.
    ngonCount, polyTranslation = source.GetPolygonTranslationMap()
    if ngonCount < 1:
        return False

    # We already can use the data provided by GetPolygonTranslationMap(),
    # but since there is GetNGonTranslationMap() we could use this to get the
    # data into more convenient form. Where the polygons are already grouped
    # as lists of polygons that form an ngon. Noteworthy is that polygons that 
    # do not belong to an ngon appear as single item lists in this list. For 
    # the example from above, the data could look like this:
    #
    #   [[0, 1, 2], [3, 4, 5], [6], [7]]
    #
    # So, the first two list entries [0, 1, 2] and [3, 4, 5] convey the same
    # information we did establish above. But this also tells us that there
    # are the polygons with the index 6 and 7 which are NOT part of an ngon,
    # since they are the only polygon in their ngon group.
    ngonTranslation = source.GetNGonTranslationMap(ngonCount, polyTranslation)

    # Get the current polygon selection state of the target.
    selection = target.GetPolygonS()
    selection.DeselectAll()

    # We select all polygons in the target that are part of an ngon. Since the
    # translation map contains all polygons, including the ones that are not
    # part of an ngon, we step over them, as we would otherwise melt the whole
    # mesh.
    [selection.Select(pid) for group in ngonTranslation if len(group) > 1
     for pid in group]

    # Carry out the SMC.
    if not c4d.utils.SendModelingCommand(
            command=c4d.MCOMMAND_MELT,
            list=[target],
            mode=c4d.MODELINGCOMMANDMODE_POLYGONSELECTION):
                RuntimeError("Failed to execute melt command on target mesh.")

    return target


def main():
    """Entry point.
    """
    # Check if the selected object is a PolygonObject.
    if not isinstance(op, c4d.PolygonObject):
        raise TypeError("Please select a PolygonObject")

    # Create a Opolygon node of matching size
    copy = c4d.PolygonObject(op.GetPointCount(), op.GetPolygonCount())

    # Copy over manually the vertex and polygon data.
    copy.SetAllPoints(op.GetAllPoints())
    for i, item in enumerate(op.GetAllPolygons()):
        cpoly = c4d.CPolygon(item.a, item.b, item.c, item.d)
        copy.SetPolygon(i, cpoly)
    copy.Message(c4d.MSG_UPDATE)

    # Before we move on, we move the copy a little bit, so that we can see
    # better what the outcome is, when we later insert it.
    mg = copy.GetMg()
    mg.off += c4d.Vector(500, 0, 0)
    copy.SetMg(mg)

    # Copy over the ngon information.
    copy = CopyNgonGroupings(op, copy)

    # Insert the new object into the document and let Cinema crunch on it.
    doc.InsertObject(copy)
    c4d.EventAdd()


if __name__ == '__main__':
    main()

@ferdinand thanks for answer, this solution works fine!
Actually because of bug I thought that melt command only returns new object instead of changing current one. Thanks again!

Is it 100% safe to select all polygons to melt at once? What if two or more Ngons are neighbours — will that worked out?

Hello @karpique,

no, it is not as described in the previous posting, the script. In order to avoid melting adjacent ngons together, you will have to do it iteratively. Which is in itself not a big problem. But doing so, will change with each iteration the polygon indices of the target mesh, since you do "loose" some to ngons. To solve that, i.e., to be able to melt the next iteration of polygon indices, you will have to hash the polygons by something else than their id.

As stated in the forum guidelines, we cannot provide full solutions here. You could hash the polygons by their arithmetic mean of their vertices for example. This will work well for all manifold meshes, but not for non-manifold meshes. There you would have to also take polygon neighborhood into account for example. This is a bit cumbersome to do, but should be possible, but I cannot do it for you.

Cheers,
Ferdinand

Dear community,

I had a little Sunday evening session with the problem discussed here in the thread, how to create neighboring Ngons in Python, and wrote a narrative example for it. The problem is not rocket science, but since the topic of creating Ngons has been addressed multiple times for Python, I thought I give it a go. Please note that the solution described here is a pure data structure problem. A question type which lies out of scope of support for the SDK Team. I am providing this solution as a private person, and this is not a commitment of ours to do this regularly.

Everything else should be explained in the script.

Cheers,
Ferdinand

A copy of an Ngon mesh created with the hashing/indexing approach. The mesh contains neighboring Ngons which would have been melted together with the more brutish approach provided in my prior posting:

e4d938ba-dd64-45fa-b45b-9bf7f1b319b7-image.png

The code:

"""Example for copying polygons and ngons from one polygon object to another.

Run this with a polygon object selected in the script manger. The approach
shown here is MCOMMAND_MELT based and implements the indexing/hashing of 
polygons described earlier in this thread. As with all things, this comes at
a cost. The runtime and space complexity of CreateNgonGroupings is rather high
due to all the indexing that has to be done. 

Which is why I would still recommend using C4DAtom.CopyTo() when possible and
removing unwanted data after that. But if one wants to create ngons from 
scratch and index them in a sane way after the polygons have been constructed,
this would be a viable approach.

As discussed in:
    https://plugincafe.maxon.net/topic/13458/
"""

import c4d


def CreateNgonGroupings(op, groups):
    """Creates ngons defined by groups in the passed object.

    The melting of polygons in carried out via SMC and two indices of which 
    one is an inverted index to keep track of shifting polygon indices. The 
    Achilles heel of this implementation is its complexity, since it has to 
    store these two indices and also rebuild one of them with each iteration. 
    Which will become a performance issue if the passed mesh is rather large
    and the operation has to be very fast.

    Args:
        op (c4d.PolygonObject): The object to create ngons for.
        groups (list[list[int]]): The ngon grouping to create, e.g., 
         [[0, 1], [2, 3]] would mean to create two ngons, one from the 
         polygons 0 and 1, and one from the polygons 2 and 3.

    Raises:
        ArgumentError: On invalid argument types.
        IndexError: When a polygon index within groups is out of bounds.
        RuntimeError: When SMC failed.
    """
    # Some mild input validation.
    if not isinstance(op, c4d.PolygonObject):
        raise ArgumentError(
            f"Expected op to be of type PolygonObject. Received: {type(op)}")
    if not isinstance(groups, (list, tuple)):
        raise ArgumentError(
            f"Expected groups to be of type list. Received: {type(groups)}")

     # The polygon count of the object.
    polyCount = op.GetPolygonCount()

    # Okay, so we are facing here a problem. Imagine we have an object with
    # six polygons, which should be melted together into three ngons, e.g.:

    #   polygonIndices = [0, 1, 2, 3, 4, 5]
    #   ngonGroups = [[0, 1], [2, 3], [4, 5]]

    # We can carry out the melting of these ngons synchronously, but not
    # iteratively. But when we do it synchronously, we will melt also 
    # neighboring ngons. 

    # When we try to do it iteratively and create the ngon [0, 1], the 
    # remaining polygon indices will be [2, 3, 4, 5] figuratively and 
    # [0, 1, 2, 3] literally, since we converted the polygons with the indices
    # [0, 1] into an ngon, causing the remaining polygons "to move up".

    # The solution to this problem is to identify polygons by something else
    # than their order in which they appear, i.e., their numeric index. There
    # are many ways we could do this, we could for example hash them by a mean
    #  of their vertex positions. But to ensure collision resistance, it is
    # probably best to stay as close as possible to the real data. Which is
    # why we build a "polygon index to polygon vertex indices"-index in the 
    # form of { int: tuple[int] }.

    # Where int is the polygon index and tuple[int] is the vertex data stored
    # in the CPolygon associated with that polygon index. The only case where
    # this should fail, is the case of a mesh which contains the same polygon
    # twice, i.e., two polygons connected to the same vertices in the same
    # order. This case can only be captured by an ordered index if I am not
    # overlooking something, i.e., is unsolvable by anything else than an
    # index number.

    polygonIndex = {
        index: (cpoly.a, cpoly.b, cpoly.c, cpoly.d)
        for index, cpoly in enumerate(op.GetAllPolygons())
    }

    # For a mesh consisting out of six polygons (like our first example from
    # above), polygonIndex could look like the following:

    #   polygonIndex = {0: (0, 2, 3, 1),
    #                   1: (2, 4, 5, 3),
    #                   2: (4, 6, 7, 5),
    #                   3: (6, 8, 9, 7),
    #                   4: (8, 10, 11, 9),
    #                   5: (10, 12, 13, 11)}

    # With this index we can carry out the melting of ngons consecutively,
    # when we build an inverted index with each iteration, e.g., something
    # like the following:

    #   invertedIndex = {(0, 2, 3, 1): 0,
    #                    (2, 4, 5, 3): 1,
    #                    (4, 6, 7, 5): 2,
    #                    (6, 8, 9, 7): 3,
    #                    (8, 10, 11, 9): 4,
    #                    (10, 12, 13, 11): 5}

    # So, when we wanted to select the polygons with the index 0, we would
    # retrieve the value in polygonIndex for the key 0. Which would be (0, 2,
    # 3, 1), which we then would use to retrieve the real index for that
    # polygon. Which would be also 0, since our polygon index and inverted
    # index where identical. But when we imagine the polygons with the indices
    # 0 and 1 being melted away, we would end up in in the next iteration with
    # an inverted index  looking like the following:

    #   invertedIndex = {(4, 6, 7, 5): 0,
    #                    (6, 8, 9, 7): 1,
    #                    (8, 10, 11, 9): 2,
    #                    (10, 12, 13, 11): 3}

    # So, when we now do the same drill to select the polygons which where
    # indexed with the numbers 2 and 3 in the original mesh, and are still
    # stored in this form in our polygon index, we will see that the inverted
    # index will spit out the updated polygon indices 0 and 1 this time.

    # Now we iterate over all the polygon groups that have been passed in 
    # order to build ngons for them.
    for indexGroup in groups:
        # Test if the passed indices do not exceed the polygon count.
        if any([pid > polyCount - 1 for pid in indexGroup]):
            raise IndexError(
                f"The polygon index group {indexGroup} is out of bounds.")
        # For each iteration we build an inverted index for polygon index to
        # CPolygon vertex index relations for the current mesh state. For the
        # first iteration this could be optimized out, since the mesh has not
        # been modified yet.
        invertedIndex = {
            (cpoly.a, cpoly.b, cpoly.c, cpoly.d): index
            for index, cpoly in enumerate(op.GetAllPolygons())
        }
        # We use both index tables to translate the polygon indices in the
        # current index group, effectively selecting polygons over their
        # vertex indices and the order in which the vertices appear.
        realIndices = [invertedIndex[polygonIndex[pid]] for pid in indexGroup]

        # Get the current polygon selection state of the object.
        selection = op.GetPolygonS()
        # Flush the polygon selection of the object.
        selection.DeselectAll()
        # Select the polygons in the current group.
        [selection.Select(pid) for pid in realIndices]

        # Carry out the melting operation via SendModelingCommand.
        if not c4d.utils.SendModelingCommand(
                command=c4d.MCOMMAND_MELT,
                list=[op],
                mode=c4d.MODELINGCOMMANDMODE_POLYGONSELECTION):
            RuntimeError("Failed to execute melt command on input mesh.")


def main():
    """Entry point.
    """
    # Check if the selected object is a PolygonObject.
    if not isinstance(op, c4d.PolygonObject):
        raise TypeError("Please select a PolygonObject")

    # The polygon to ngon information, this returns a tuple(int, List[int]),
    # where int is the number of ngons and List[int] contains the mappings.
    # I.e., (2, [0, 0, 0, 1, 1, 1]) means that there are two ngon in the
    # mesh and that the polygons with indices 0 to 2 belong to the ngon 0 and
    # the polygons with indices 3 to 5 belong to the ngon 1.
    ngonCount, polyTranslation = op.GetPolygonTranslationMap()

    # We already can use the data provided by GetPolygonTranslationMap(),
    # but since there is GetNGonTranslationMap() we could use this to get the
    # data into more convenient form. Where the polygons are already grouped
    # as lists of polygons that form an ngon. Noteworthy is that polygons that
    # do not belong to an ngon appear as single item lists in this list. For
    # the example from above, the data could look like this:
    #
    #   [[0, 1, 2], [3, 4, 5], [6], [7]]
    #
    # So, the first two list entries [0, 1, 2] and [3, 4, 5] convey the same
    # information we did establish above. But this also tells us that there
    # are the polygons with the index 6 and 7 which are NOT part of an ngon,
    # since they are the only polygon in their ngon group.
    ngonGroups = op.GetNGonTranslationMap(ngonCount, polyTranslation)
    # Remove these single entry groups to not let CreateNgonGroupings() run
    # unnecessarily in circles.
    ngonGroups = [item for item in ngonGroups if len(item) > 1]

    # Create a polygon object of matching size to copy the polygon data to.
    copy = c4d.PolygonObject(op.GetPointCount(), op.GetPolygonCount())
    copy.SetName(f"Copy of {op.GetName()}")

    # Copy over manually the vertex and polygon data.
    copy.SetAllPoints(op.GetAllPoints())
    for i, item in enumerate(op.GetAllPolygons()):
        cpoly = c4d.CPolygon(item.a, item.b, item.c, item.d)
        copy.SetPolygon(i, cpoly)
    copy.Message(c4d.MSG_UPDATE)

    # Before we move on, we move the copy a little bit, so that we can see
    # better what the outcome is, when we later insert it.
    mg = copy.GetMg()
    mg.off += c4d.Vector(500, 0, 0)
    copy.SetMg(mg)

    # Create the ngon information.
    CreateNgonGroupings(copy, ngonGroups)

    # Insert the new object into the document and let Cinema crunch on it.
    doc.InsertObject(copy)
    c4d.EventAdd()


if __name__ == '__main__':
    main()

Hi,

I am back at work now, if any questions remain, please feel free to ask them.

Cheers,
Ferdinand