geometry_edges

Demonstrates handling edges in polygon objects at the example of splitting edges.

geometry_edges.py

"""Demonstrates handling edges in polygon objects at the example of splitting edges.

The script requires a polygon object with at least one edge selected. The selected edges will be 
split in the middle, into a triangle and a quadrangle, or, when the user is pressing the shift key, 
into three triangles.

Topics:
    * Edge selections and edge indices and their relation to polygons.
    * Geometry operation that require both removing and adding elements.
    * Updating geometry metadata such as selection, vertex maps, vertex colors, and UVW data, here
      at the example of maintaining the data of the split polygons.

Overview:

    When operating on edge selections, one must understand that edges are indexed over polygons. So,
    the edge AB of polygon P is not the same as the edge BA of polygon Q, even though both use the
    same points A and B. This effectively results in the raw edge index formula:

        rawEdgeIndex = polygonIndex * 4 + localEdgeIndex

    which must be often used when working with edges, to convert them into associated polygons or
    points. There exist convenience methods to convert between the raw edge indices and a more
    user-friendly notion of edge indices, such as `PolygonObject.GetSelectedEdges()`, but often
    we must use the raw indices directly, as the user-facing edge indices do not offer that conversion
    relationship.
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2026 MAXON Computer GmbH"
__date__ = "30/05/2025"
__license__ = "Apache-2.0 License"
__version__ = "2024.0.0+"

import c4d

doc: c4d.documents.BaseDocument  # The currently active document.
op: c4d.PolygonObject | None  # The primary selected object in `doc`. Can be `None`.

def main() -> None:
    """Called by Cinema 4D when the script is being executed.
    """
    # Make sure we have a polygon object selected with at least one edge selected. Then figure out
    # if the user is pressing the Shift key to toggle between quadrangle and triangle mode.
    if not isinstance(op, c4d.PolygonObject) or op.GetEdgeS().GetCount() < 1:
        return c4d.gui.MessageDialog("Please select a polygon object with at least one edge selected.")
    
    isQuadMode: bool = True
    state: c4d.BaseContainer = c4d.BaseContainer()
    if (c4d.gui.GetInputState(c4d.BFM_INPUT_KEYBOARD, c4d.KEY_SHIFT, state)):
        isQuadMode = state[c4d.BFM_INPUT_QUALIFIER] & c4d.QSHIFT != c4d.QSHIFT

    # Edges, as already hinted at in prior geometry examples, do only exist implicitly in the API.
    # Other than for points and polygons, there is no dedicated edge entity in the API. Instead, 
    # edges are defined implicitly by polygons. This also means that edges are indexed over the
    # polygons they belong to. I.e., the edge AB of polygon P is not the same as the edge BA of
    # polygon Q, even tough both use the same points A and B. So, when the user selects a singular 
    # edge in the UI, what is marked in the API as selected, could be one edge (only one polygon uses 
    # these two points forming the edge), two edges (two polygons use these two points to define an 
    # edge) or even more edges (in a non-manifold mesh where many polygons use these two points to 
    # form an edge).

    # There exist convenience methods to translate between the internal raw and the compressed 
    # user-facing edge indices, such as `PolygonObject.GetSelectedEdges()`. But in this case we will
    # use the raw edge indices, as we will have to handle all polygons associated with an edge anyway. 

    # Get the raw edge selection of the object and convert them into a list of raw selected edge 
    # indices. The total number of edges in a mesh is always four times the number of polygons, 
    # because Cinema 4D always stores meshes as sets of quads, even when some of the polygons are 
    # triangles or n-gons.
    activeEdgeSelection: c4d.BaseSelect = op.GetEdgeS()
    edgeCount: int = op.GetPolygonCount() * 4
    selectedEdges: list[int] = [
        i for i, value in enumerate(activeEdgeSelection.GetAll(edgeCount)) if value]

    # Get all the polygons, points, and UVW data of the polygon object.
    polygons: list[c4d.CPolygon] = op.GetAllPolygons()
    points: list[c4d.Vector] = op.GetAllPoints()
    uvwTag: c4d.UVWTag | None = op.GetTag(c4d.Tuvw)
    uvwData: list[list[c4d.Vector]] = [
        list(uvwTag.GetSlow(i).values()) for i in range(uvwTag.GetDataCount())] if uvwTag else []

    # To update geometry by both adding and removing elements, it is often easiest to first 
    # collect and remove all elements that must be removed, to only then add the new elements. 
    # This way we know the final index of everything when we add new data, which is of required. 
    # We therefore establish here #splitData, a container for all the data we have to split, i.e., 
    # remove and then replace with new data. The alternative would be trying to do this in place or
    # creating a 'new' list and then deleting and joining elements at the end. But both would come
    # with the issue that we do not know the final indices of things we add, as we have not yet
    # determined everything that has to be removed.

    # We will use this data structure to store things we have to split, i.e., remove and then later
    # replace by new things. While filling it, we will remove these things from #polygons and 
    # #uvwData.
    splitData: dict[int,                    # Index of the polygon to split.
                    tuple[                  # Data for this split operation.
                        int,                # Local edge in the polygon to split.
                        c4d.CPolygon,       # Polygon to split.
                        list[c4d.Vector]]   # UVW coordinates of the polygon.
                    ] = {}
    
    # --- Remove data to split ---
    
    # Iterate over our raw selected edge indices and and determine the polygon index and local edge
    # their associated with, to then update our data structures.
    for rawEdgeIndex in selectedEdges:
        pid: int = rawEdgeIndex // 4 # The polygon index for the raw edge index. It is the
                                     # integer division of the raw edge index by 4 because each
                                     # polygon has exactly 4 edges. 
                                     # E.g., rawIndex = 9 -> pid = 9 // 2 = 2
        eid: int = rawEdgeIndex % 4  # The index between 0 and 3 inside the polygon referenced by
                                     # the raw edge index. It is the modulo of the raw edge index 
                                     # and 4 because each polygon has exactly 4 edges, so the local 
                                     # edge index is the remainder of the index divided by 4.
                                     # E.g., rawIndex = 9 -> eid = 9 % 2 = 1, where 0 would then mean
                                     # AB, 1 would mean BC, 2 would mean CD and 3 would mean DA.

        # Make sure that we do not try to split the same polygon multiple times.
        if pid in splitData:
            return c4d.gui.MessageDialog(
                f"This edge selection attempts to split the polygon with the ID {pid} more than "
                "once. Doing that is not supported by this script. Please select only select one "
                "edge per polygon.")
        
        # Remove the polygon and uvw data from our things to keep and add them to the list of
        # our to be processed things.
        _pid: int = pid - len(splitData) # Shift the pid by the count of polygons we already removed
        poly: c4d.CPolygon = polygons.pop(_pid)
        uvw: dict[str, c4d.Vector] = uvwData.pop(_pid) if uvwData else []
        splitData[pid] = (eid, poly, uvw)

    # --- Add new data by processing the removed data ---

    # One thing we want to update is the edge selection. Because when we change the polygons, we
    # also change what #activeEdgeSelection means, as these are just indices based on polygon 
    # indices. So, we are going to build a new edge selection based on the new polygons we create.
    newEdgeSelection: list[int] = []

    # The lookup table for the points we create. We will use this to store the mid points of the
    # edges we split, so that we can later update vertex maps and vertex colors by linearly
    # interpolating the values of the two points that were used to create the mid point.
    pointData: dict[int,           # Index of the mid point.
                    list[int, int] # The indices of the two points that were used to create the mid 
                                   # point. I.e., when we later want to update vertex maps or vertex
                                   # colors, we can just linearly interpolate the values of these two
                                   # points to get the value for the mid point.
                    ] = {}

    # Start iterating over the data of the be split polygons we created before, and build the new
    # polygons and UVW data from it. We will also create a new point for the mid point of the edge
    # we split, and store the point indices of the two points that were used to create that mid
    # point in the #pointData dictionary. This way we can later update vertex maps and vertex colors
    # by linearly interpolating the values of the two points that were used to create the mid point.
    for _, (eid, poly, uvw) in splitData.items():
        
        # Get the point indices of the polygon and then shift them by the local edge index, so that
        # our edge #eid to split is always #a - #b. E.g. [A, B, C, D] shifted by eid = 1 --> 
        # [B, C, D, A] or by eid = 2 --> [C, D, A, B]. If you want to, you can also write this as an 
        # if-block.
        polyPointIndices: list[int] = [poly.a, poly.b, poly.c, poly.d]
        a, b, c, d = polyPointIndices[eid:] + polyPointIndices[:eid]

        # Calculate the mid point for the edge AB to split and add it to the list of points, unless 
        # that point already exists from another polygon we handled. #mid is the index of that
        # point to be used by polygons.
        mpoint: c4d.Vector = c4d.utils.MixVec(points[a], points[b], 0.5)
        mid: int = points.index(mpoint) if mpoint in points else len(points)
        pointData[mid] = [a, b]
        points.append(mpoint)

        # Create the new polygons and UVWs for edge we just split.

        lastPid: int = len(polygons) # The ID of the currently last polygon.

        # The user is not pressing Shift, we create a quad and a tri.
        if isQuadMode:
            # Add the new split polygons. As always, order of points - the winding direction - 
            # matters, as it will determine the direction the polygon is facing. See the more basic 
            # polygon examples for details on winding order and how tris and quads are defined.
            polygons += [c4d.CPolygon(a, mid, d, d), # A tri, the last index is repeated.
                         c4d.CPolygon(mid, b, c, d)] # A quad, all indices are unique.
            # UVWs a more or less mirroring the polygons, so we just have to repeat for them, what
            # we did above. Linearly interpolating the UVW coordinates is probably not the best way
            # to do this, but this is just a simple example, so it will do.
            if uvw:
                uva, uvb, uvc, uvd = uvw[eid:] + uvw[:eid]
                uvMid: c4d.Vector = c4d.utils.MixVec(uva, uvb, 0.5)
                uvwData += [[uva, uvMid, uvd, uvd], # the tri
                            [uvMid, uvb, uvc, uvd]] # the quad
        # The user is pressing Shift, so we create three tris instead of a quad and a tri.
        else:
            polygons += [c4d.CPolygon(a, mid, d, d), # These are all tris.
                         c4d.CPolygon(mid, b, c, c),
                         c4d.CPolygon(mid, c, d, d)]
            if uvw:
                uva, uvb, uvc, uvd = uvw[eid:] + uvw[:eid]
                uvMid: c4d.Vector = c4d.utils.MixVec(uva, uvb, 0.5)
                uvwData += [[uva, uvMid, uvd, uvd],
                            [uvMid, uvb, uvc, uvc],
                            [uvMid, uvc, uvd, uvd]]
                
        # Write our new edge selection data. Since we only have to select edges that are topologically
        # part of the old edge, we always only select two edges, no matter if we created two or three
        # polygons. We write the polygons index times four - (lastPid + n) * 4 - offset by the local
        # edge index. Since we shifted our polygons above, so that our edge in question is always
        # AB, we just add 0.
        newEdgeSelection += [(lastPid + 0) * 4 + 0, # Edge AB of the first polygon.
                             (lastPid + 1) * 4 + 0] # Edge AB of the second polygon.

    # --- Update the polygon object by writing our processed data back to it ---

    # Now we write our data back, wrapped into and undo to consolidate all the change we make.
    doc.StartUndo()

    # Write the new points and polygons back to the polygon object.
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, op)
    op.ResizeObject(len(points), len(polygons))
    op.SetAllPoints(points)
    for i, poly in enumerate(polygons):
        op.SetPolygon(i, poly)

    op.Message(c4d.MSG_UPDATE)

    # Now that we have updated the geometry, we can update the edge selection of the object. We can
    # just keep using the selection we accessed at the beginning.
    activeEdgeSelection.DeselectAll()
    for rawEdgeIndex in newEdgeSelection:
        activeEdgeSelection.Select(rawEdgeIndex)

    # We could sanity check our selection with this code. We are writing here a raw selection. So,
    # when there is are the polygons P and Q with a shared edge E, represented by the raw edges
    # Ea and Eb, and we would have only selected Ea, the object would now be in an illegal selection
    # state. Cinema 4D will usually just display an error when that happens, but when there are
    # somewhere unhandled sanity checks, it could crash. We can use our code below to sanity check
    # the selection, but that also costs performance, as we have to Neighbor index he full object. 
    # So, I will not use it here.

    # nbr: c4d.utils.Neighbor = c4d.utils.Neighbor()
    # nbr.Init(op)
    # op.SetSelectedEdges(nbr, activeEdgeSelection, c4d.EDGESELECTIONTYPE_SELECTION)

    # No we start updating the tags we support.

    # Update vertex maps by using our #pointData dictionary to write the linearly interpolated value
    # between the two points we split.
    mapTags: list[c4d.VariableTag] = [t for t in op.GetTags() if t.IsInstanceOf(c4d.Tvertexmap)]
    for tag in mapTags:
        # Use the abstract VariableTag interface to access the weight data.
        weights: list[float] = tag.GetAllHighlevelData()
        for mid, (a, b) in pointData.items():
            weights[mid] = c4d.utils.MixNum(weights[a], weights[b], 0.5)

        # Write the modified weights back to the tag and update it.
        doc.AddUndo(c4d.UNDOTYPE_CHANGE, tag)
        tag.SetAllHighlevelData(weights)
        tag.Message(c4d.MSG_UPDATE)

    # Same thing but for vertex colors. We could also use the abstract VariableTag interface
    # here, but since there is a special interface for vertex color tags in Python, we will use 
    # it instead.
    colorTags: list[c4d.VertexColorTag] = [t for t in op.GetTags() if t.IsInstanceOf(c4d.Tvertexcolor)]
    for tag in colorTags:
        doc.AddUndo(c4d.UNDOTYPE_CHANGE, tag)
        if not tag.IsPerPointColor():
            tag.SetPerPointMode(True)

        data: object = tag.GetDataAddressW()
        for mid, (a, b) in pointData.items():
            aColor: c4d.Vector = tag.GetColor(data, None, None, a)
            aAlpha: float = tag.GetAlpha(data, None, None, a)
            bColor: c4d.Vector = tag.GetColor(data, None, None, b)
            bAlpha: float = tag.GetAlpha(data, None, None, b)
            tag.SetColor(data, None, None, mid, c4d.utils.MixVec(aColor, bColor, 0.5))
            tag.SetAlpha(data, None, None, mid, c4d.utils.MixNum(aAlpha, bAlpha, 0.5))
        
        tag.Message(c4d.MSG_UPDATE)

    # And finally, we update the UVW data, here we just write what we have computed before.
    if uvwTag:
        doc.AddUndo(c4d.UNDOTYPE_CHANGE, uvwTag)
        for i, uvw in enumerate(uvwData):
            uvwTag.SetSlow(i, *uvw)

        # Update the tag and the object.
        uvwTag.Message(c4d.MSG_UPDATE)
        op.Message(c4d.MSG_UPDATE)

    # End the undo step and signal Cinema 4D that the viewport and GUI must be updated.
    doc.EndUndo()
    c4d.EventAdd()

if __name__ == '__main__':
    main()